Compare commits

..

2103 Commits

Author SHA1 Message Date
Agustin Rivera
ded626d171 fix(browser): thread ssrf policy through file-chooser hook click 2026-04-06 18:28:21 +00:00
Agustin Rivera
2c8697b694 fix(browser): run post-interaction SSRF guard unconditionally 2026-04-06 18:03:12 +00:00
Agustin Rivera
f3719cef5e fix(browser): thread ssrf policy through batches 2026-04-06 17:50:16 +00:00
Agustin Rivera
08019bbda0 fix(browser): guard interaction-driven navigations 2026-04-06 16:44:49 +00:00
Peter Steinberger
1880b104ed style: normalize test import order 2026-04-06 17:42:19 +01:00
Peter Steinberger
f7f861082a fix(ci): repair boundary guards 2026-04-06 17:42:19 +01:00
Peter Steinberger
51f77b5e04 Agents: align Claude fixture with rebased tests 2026-04-07 00:37:37 +08:00
Peter Steinberger
0f224724dc Agents: slim cli-runner test seams 2026-04-07 00:37:37 +08:00
Peter Steinberger
ec359f5942 Discord: trim monitor test import cost 2026-04-07 00:37:37 +08:00
Peter Steinberger
67520b6abf fix(ci): restore bundled channel loading 2026-04-06 17:35:47 +01:00
Peter Steinberger
0335a8783c perf(test): shard full vitest runs 2026-04-06 17:34:11 +01:00
Peter Steinberger
a47cb0a3b3 refactor: dedupe approval gateway resolver setup 2026-04-06 17:31:16 +01:00
Peter Steinberger
c7cc89904e fix: unblock claude docker live lanes 2026-04-06 17:31:11 +01:00
Peter Steinberger
e7e3f11b20 refactor: dedupe legacy private-network doctor contracts 2026-04-06 17:28:11 +01:00
Peter Steinberger
ce30557399 refactor(deadcode): remove orphaned core helpers 2026-04-06 17:26:25 +01:00
Peter Steinberger
591347113e refactor(deadcode): prune extension test shims 2026-04-06 17:26:25 +01:00
Peter Steinberger
943d7de240 refactor: dedupe doctor compatibility adapters 2026-04-06 17:25:36 +01:00
Vincent Koc
e7fe087677 fix(openai): normalize prompt overlay personality config 2026-04-06 17:24:51 +01:00
Vincent Koc
6dc3e1f770 perf(test): flatten async channel secret mocks 2026-04-06 17:24:38 +01:00
Peter Steinberger
5f906c926d refactor: remove qa-e2e compatibility facade 2026-04-06 17:23:35 +01:00
Peter Steinberger
350238d402 feat: add interactive qa lab suite runner 2026-04-06 17:23:35 +01:00
Peter Steinberger
e70168212d refactor: dedupe script and matrix send helpers 2026-04-06 17:21:52 +01:00
Vincent Koc
7af1def025 perf(test): isolate secret target registry docs checks 2026-04-06 17:21:49 +01:00
Peter Steinberger
7422e90053 fix(ci): restore shared test seams 2026-04-06 17:20:38 +01:00
huntharo
f2cd2c00b0 fix(plugin-sdk): add bundled entry error context
(cherry picked from commit 6099405ba1e8b98caa92cce4487808d212dc3544)
2026-04-06 12:19:05 -04:00
Peter Steinberger
d25491aa6d refactor: dedupe release git range helpers 2026-04-06 17:18:36 +01:00
Peter Steinberger
1c5cbad0a6 refactor: dedupe account conversation bindings 2026-04-06 17:18:36 +01:00
Peter Steinberger
1aee8c55ce refactor: dedupe channel doctor compat helpers 2026-04-06 17:18:36 +01:00
Peter Steinberger
a86fa3b211 refactor(deadcode): drop orphaned extension helpers 2026-04-06 17:18:03 +01:00
Peter Steinberger
ce87d5e242 refactor(deadcode): remove extension wrapper shims 2026-04-06 17:18:03 +01:00
Peter Steinberger
5d7a73380f fix(ci): repair tsgo test harnesses 2026-04-06 17:16:01 +01:00
Vincent Koc
c01b4981af test(memory-core): seed qmd manager provider registry 2026-04-06 17:10:18 +01:00
Peter Steinberger
bedfa576a3 fix(ci): clean poll-timeout test merge artifact 2026-04-06 17:06:22 +01:00
Peter Steinberger
645c331200 fix(ci): repair type and extension regressions 2026-04-06 17:06:22 +01:00
Vincent Koc
79a0c71874 chore(lint): drop stale transcript type import 2026-04-06 17:06:18 +01:00
Vincent Koc
a797068206 refactor(lint): tighten channel and config defaults 2026-04-06 17:06:18 +01:00
Peter Steinberger
5d0e8336ab perf(test): trim bundled channel bootstrap 2026-04-06 17:05:59 +01:00
Peter Steinberger
8b79cbcd06 build(plugins): align package versions to 2026.4.6 2026-04-06 17:05:30 +01:00
Peter Steinberger
860721f28d build(plugins): sync bundled versions to 2026.4.6 2026-04-06 17:05:30 +01:00
Peter Steinberger
220d10cad3 docs(changelog): add unreleased entries for 2026.4.6 2026-04-06 17:05:30 +01:00
Peter Steinberger
723c0ea2b7 test: speed up memory manager hotspot tests 2026-04-06 17:04:13 +01:00
Peter Steinberger
6f841ff121 test: cache memory manager helper imports 2026-04-06 17:04:13 +01:00
Peter Steinberger
e1a047c43f fix: repair gateway fingerprint callback 2026-04-06 17:02:10 +01:00
Peter Steinberger
a8436f0220 fix: resolve rebased type drift 2026-04-06 17:02:10 +01:00
Peter Steinberger
821a30981a test: refresh agent harness and latest-main type fixes 2026-04-06 17:02:10 +01:00
Peter Steinberger
2fef1ccbe7 fix: avoid leading spaces when stripping model tokens 2026-04-06 17:02:10 +01:00
Peter Steinberger
0ffceca50a test: align agent auth and model expectations 2026-04-06 17:02:10 +01:00
Peter Steinberger
1b9ec88d9c fix: centralize HTTP/1.1 SSRF dispatchers (#61777) (thanks @zozo123) 2026-04-06 17:02:10 +01:00
Yossi Eliaz
0f5919a4ba fix(ssrf): disable HTTP/2 for pinned SSRF-guard dispatchers (undici 8.0 compat)
Undici 8.0 defaults HTTPS clients to negotiate HTTP/2 via ALPN, which is
incompatible with the custom `connect.lookup` callback used for SSRF DNS
pinning. This caused `TypeError: fetch failed` in web_fetch/web_search.

Explicitly set `allowH2: false` on all dispatcher creation paths (Agent,
EnvHttpProxyAgent, ProxyAgent) to restore HTTP/1.1 behavior and keep the
pinned DNS lookup working reliably.

Closes #61738
2026-04-06 17:02:10 +01:00
Peter Steinberger
a65f9971b7 refactor(deadcode): remove duplicate barrels and helper shims 2026-04-06 17:00:40 +01:00
Peter Steinberger
0b36423f97 docs: reorder unreleased changelog entries 2026-04-06 16:58:28 +01:00
Vincent Koc
38c520acc3 chore(memory-core): type embedding test mocks 2026-04-06 16:58:14 +01:00
Vincent Koc
84c182deb2 fix(secrets): keep legacy x_search auth resolving 2026-04-06 16:57:23 +01:00
Vincent Koc
096d0cf412 chore(lint): type script and test helpers 2026-04-06 16:55:50 +01:00
Peter Steinberger
e79d2ecd9e fix(check): repair latest type drift on main 2026-04-06 16:54:34 +01:00
Peter Steinberger
21f59a0ad5 fix: suppress commentary history leaks (#61747) (thanks @afurm) 2026-04-06 16:54:34 +01:00
Peter Steinberger
672fcb187d refactor(plugins): move provider seams to owning extensions 2026-04-06 16:54:18 +01:00
Peter Steinberger
9100923395 fix(ci): repair tsgo regressions 2026-04-06 16:53:21 +01:00
Peter Steinberger
f2a710ce63 fix(ci): align stale test expectations 2026-04-06 16:53:21 +01:00
Vincent Koc
87b2a6a16a refactor(lint): type tool factories and runtime helpers 2026-04-06 16:53:02 +01:00
Vincent Koc
506b4decbd test(secrets): mock bundled channel secrets seam 2026-04-06 16:52:59 +01:00
Peter Steinberger
9c82974082 refactor: dedupe discord send target parsing 2026-04-06 16:52:42 +01:00
Peter Steinberger
93338ffbcc refactor: dedupe media generation action helpers 2026-04-06 16:52:42 +01:00
Peter Steinberger
c88870ac93 refactor: dedupe windows cmd runner helpers 2026-04-06 16:52:41 +01:00
Peter Steinberger
ad9481e2d1 refactor: dedupe auth and session helpers 2026-04-06 16:52:41 +01:00
Peter Steinberger
e8c7481fd2 refactor: dedupe outbound helpers 2026-04-06 16:52:41 +01:00
Peter Steinberger
4a84412b3a refactor: dedupe channel plugin helpers 2026-04-06 16:52:41 +01:00
Peter Steinberger
8aeee0dc6d refactor: dedupe plugin config helpers 2026-04-06 16:52:41 +01:00
Peter Steinberger
a830f4de4b test(commands): fix moved session store refs 2026-04-06 16:49:28 +01:00
Peter Steinberger
8a33a8d607 perf(test): trim runtime lookups and add changed bench 2026-04-06 16:49:28 +01:00
Peter Steinberger
8477f1841a refactor(deadcode): remove orphaned core wrappers 2026-04-06 16:47:03 +01:00
Peter Steinberger
d60149c655 test: move provider tests into owning extensions 2026-04-06 16:47:03 +01:00
Vincent Koc
c109a7623b refactor(lint): type shared runtime seams 2026-04-06 16:46:08 +01:00
Peter Steinberger
eef80f31cf Tests: fix stale expectations and weak token generation 2026-04-06 23:44:26 +08:00
Peter Steinberger
074e6d5047 fix(discord): use ws for gateway sockets 2026-04-06 16:43:47 +01:00
Vincent Koc
6775611c5d refactor(gateway): type inline tool auth helpers 2026-04-06 16:42:16 +01:00
Vincent Koc
9e41b2ffd6 style(contracts): normalize registry formatting 2026-04-06 16:40:54 +01:00
Vincent Koc
6b12e3ebf6 fix(contracts): stabilize bundled channel artifact loading 2026-04-06 16:40:54 +01:00
Vincent Koc
c3b19d204a perf(test): lazy-load bundled channel secrets 2026-04-06 16:40:41 +01:00
Peter Steinberger
349a1c58f9 refactor: re-duplicate auth and session helpers 2026-04-06 16:38:57 +01:00
Peter Steinberger
cdf321b320 refactor: re-duplicate outbound helpers 2026-04-06 16:38:57 +01:00
Peter Steinberger
9c24bda43b refactor: re-duplicate channel plugin helpers 2026-04-06 16:38:57 +01:00
Peter Steinberger
a6a379b37c refactor: re-duplicate plugin config helpers 2026-04-06 16:38:57 +01:00
Vincent Koc
00f256dd31 refactor(gateway): type tool resolution paths 2026-04-06 16:36:51 +01:00
Peter Steinberger
aa6f6135db fix: tighten TUI phase handling and heartbeat session guards (#61463) (thanks @100yenadmin) 2026-04-06 16:35:22 +01:00
Eva
2d481c9329 fix(heartbeat): add subagent guard to resolveHeartbeatSession production code 2026-04-06 16:35:22 +01:00
Eva
aaf5307638 fix(gateway): seq-based cursor pagination + sanitize SSE fast path
- Pagination now searches by message seq value instead of using
  cursorSeq-1 as array index. After sanitization drops rows, seqs
  become sparse and positional indexing breaks cursor traversal.
- SSE unbounded fast path now sanitizes incremental messages through
  sanitizeChatHistoryMessages before emitting, so NO_REPLY and
  directive messages are suppressed consistently with initial history.
2026-04-06 16:35:22 +01:00
Eva
22d8e47a50 fix(agents,gateway): adopt phase-aware assistant text extraction 2026-04-06 16:35:22 +01:00
Peter Steinberger
0b9993df95 fix(agents): keep phaseless OpenAI WS text buffered until phase resolves (#61968)
* fix(agents): gate WS text delta emission on valid phase value, not map key existence

When output_item.added arrives without phase metadata, outputItemPhaseById
stores undefined. The previous .has() check returned true for undefined
values, bypassing the buffering gate and leaking commentary as unphased
visible content.

Fix: change .has() to .get() !== undefined on both delta and done handlers.

Fixes #61477

* docs: note WS phase buffering fix (#61954) (thanks @100yenadmin)

* test(agents): cover phaseless WS output_text.done buffering (#61954)

* test(commands): fix session-store import path for tsgo (#61968)

---------

Co-authored-by: Eva <eva@100yen.org>
2026-04-06 16:35:16 +01:00
Vincent Koc
56136c83b7 refactor(plugins): type sync hook handlers 2026-04-06 16:35:11 +01:00
Peter Steinberger
c22372dec6 fix(ci): restore discord and feishu lifecycle tests 2026-04-06 16:32:41 +01:00
Peter Steinberger
de20d3a024 refactor(plugin-sdk): add simple completion runtime entrypoint 2026-04-06 16:29:43 +01:00
Peter Steinberger
7785dc21e6 fix(discord): drop generated thread title temperature 2026-04-06 16:29:43 +01:00
Peter Steinberger
6cc54e5059 fix(extensions): restore lint-safe xai imports 2026-04-06 16:27:38 +01:00
Peter Steinberger
7a5e65c71b test(channels): fix add and facade fixtures 2026-04-06 16:27:38 +01:00
Vincent Koc
44cd91b0a9 fix(feishu): load lifecycle mocks before card action imports 2026-04-06 16:26:48 +01:00
Mason
2d7d99f66e docs: quote plan title frontmatter (#61962) 2026-04-06 23:25:57 +08:00
jjjojoj
281ea15550 fix: narrow queryTokenHint guard to only auth-specific errors, remove overly broad connect failed check 2026-04-06 23:24:29 +08:00
jjjojoj
39c721d382 fix: detect ?token= and suggest #token= fragment syntax
When users visit the Control UI with ?token=<token>, they see
"device identity required" with no hint about the correct URL format.

This change:
- Detects when token is read from query string vs URL fragment
- Warns via console when ?token= is used
- Shows an inline hint in the overview error area directing users
  to use #token=<token> instead

Fixes #54842
2026-04-06 23:24:29 +08:00
Peter Steinberger
cfb7779584 refactor(deadcode): remove agent command shims 2026-04-06 16:24:12 +01:00
Peter Steinberger
d5bfc79112 fix(discord): preserve stack hints for empty gateway type errors 2026-04-06 16:20:36 +01:00
Vincent Koc
90d246959b fix(matrix): align forged mention test with route precheck 2026-04-06 16:19:13 +01:00
Vincent Koc
4ef8f4f53c docs: add media overview page and consolidate TTS duplicate 2026-04-06 16:18:45 +01:00
Peter Steinberger
41c700fe9e refactor(deadcode): remove command auth shims 2026-04-06 16:18:20 +01:00
Vincent Koc
d425aa0912 fix(feishu): await websocket startup in cleanup test 2026-04-06 16:16:53 +01:00
Peter Steinberger
514328a9ad style(repo): format touched helpers and tests 2026-04-06 16:16:10 +01:00
Peter Steinberger
9ca935720c style(preview): format dream diary preview files 2026-04-06 16:16:10 +01:00
Vincent Koc
ab564f8446 docs: add async task lifecycle to video and music generation 2026-04-06 16:15:57 +01:00
Peter Steinberger
0c5e6037b0 fix(openai): clarify auth routes in picker and docs 2026-04-06 16:14:51 +01:00
Peter Steinberger
2b6e08bbfa refactor: remove confirmed dead helpers 2026-04-06 16:13:26 +01:00
Peter Steinberger
d82644cdc8 chore(deadcode): fix knip scan config 2026-04-06 16:13:26 +01:00
Peter Steinberger
d7e3df5eaa perf(test): expand light lane routing 2026-04-06 16:13:21 +01:00
jjjojoj
c1c1c0f351 fix: increase padding-right to 70px to fully clear two action buttons 2026-04-06 23:11:13 +08:00
jjjojoj
c52d896ef0 fix: remove accidental log file and add has-copy class to chat bubbles
- Remove mistakenly committed openclaw-2026-04-03.log
- Add 'has-copy' CSS class to chat bubbles when copy button is present,
  so the .chat-bubble.has-copy padding-right rule actually applies
2026-04-06 23:11:13 +08:00
jjjojoj
a55d45de3c fix: prevent Canvas/Copy icons from overlapping chat bubble text
Increase right padding on .chat-bubble.has-copy from 36px to 62px to
accommodate both copy and canvas action buttons without obscuring text.

Fixes #61514
2026-04-06 23:11:13 +08:00
Peter Steinberger
16d0f0567e fix: preserve legacy replay phase boundaries (#61529) (thanks @100yenadmin) 2026-04-06 23:09:29 +08:00
Eva
a200a746fc fix(agents): correct phase-buffering test expectation for mid-stream deltas 2026-04-06 23:09:29 +08:00
Eva
a58726e1ed fix(agents): inherit message-level phase for untagged blocks during replay splitting
Fixes #61476

Untagged text blocks in mixed assistant messages were forced to undefined
phase when any sibling had an explicit textSignature phase. Now they
correctly inherit the message-level assistantMessagePhase, preventing
commentary leaks during history replay.

Removes the hasExplicitBlockPhase scan — untagged blocks always inherit
m.phase. Blocks with explicit textSignature.phase still use their own.

94/94 tests pass. Regression test added for mixed explicit/untagged blocks.
2026-04-06 23:09:29 +08:00
Vincent Koc
f94a018191 perf(test): slim secrets runtime coverage hotspot 2026-04-06 16:08:05 +01:00
Peter Steinberger
1fb44f0aad fix: separate selected session model resolution 2026-04-06 16:07:50 +01:00
jjjojoj
0f8480ca0b fix: add max-height, flex layout, and scrollable command preview for mobile approval card 2026-04-06 23:06:09 +08:00
jjjojoj
77f9f6112e fix: add bottom safe-area-inset for mobile approval overlay 2026-04-06 23:06:09 +08:00
Vincent Koc
eef20a87d0 refactor(lint): report unused disable directives in root oxlint 2026-04-06 16:02:38 +01:00
Vincent Koc
9c3d9c5c18 chore(lint): drop stale repo lint comments 2026-04-06 16:01:23 +01:00
Peter Steinberger
7f336aba56 fix(discord): normalize gateway fatal type errors 2026-04-06 15:59:56 +01:00
Vincent Koc
c7a562683a chore(agents): drop stale lint comments 2026-04-06 15:59:22 +01:00
Vincent Koc
cb770057b0 chore(lint): drop stale config and gateway lint comments 2026-04-06 15:57:32 +01:00
Vincent Koc
2537ae503d chore(plugins): drop stale core channel lint comments 2026-04-06 15:56:41 +01:00
Peter Steinberger
378b2c2f5c fix(check): absorb latest main lint drift 2026-04-06 15:56:02 +01:00
Peter Steinberger
d12029a15a fix(check): repair plugin runtime type drift batch 2026-04-06 15:54:12 +01:00
Vincent Koc
8fe7b3730f fix(check): restore gateway status tls mock typing 2026-04-06 15:53:16 +01:00
Lewis
1234c873bc fix(msteams): add SSRF validation to file consent upload URL (#23596)
* fix(msteams): add SSRF validation to file consent upload URL

The uploadToConsentUrl() function previously accepted any URL from the
fileConsent/invoke response without validation. A malicious Teams tenant
user could craft an invoke activity with an attacker-controlled uploadUrl,
causing the bot to PUT file data to arbitrary destinations (SSRF).

This commit adds validateConsentUploadUrl() which enforces:

1. HTTPS-only protocol
2. Hostname must match a strict allowlist of Microsoft/SharePoint
   domains (sharepoint.com, graph.microsoft.com, onedrive.com, etc.)
3. DNS resolution check rejects private/reserved IPs (RFC 1918,
   loopback, link-local) to prevent DNS rebinding attacks

The CONSENT_UPLOAD_HOST_ALLOWLIST is intentionally narrower than the
existing DEFAULT_MEDIA_HOST_ALLOWLIST, excluding overly broad domains
like blob.core.windows.net and trafficmanager.net that any Azure
customer can create endpoints under.

Includes 47 tests covering IPv4/IPv6 private IP detection, protocol
enforcement, hostname allowlist matching, DNS failure handling, and
end-to-end upload validation.

* fix(msteams): validate all DNS answers for consent uploads

* fix(msteams): restore changelog header

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-06 09:52:56 -05:00
Vincent Koc
c921a6ecad refactor(lint): report unused extension lint disables 2026-04-06 15:52:08 +01:00
Peter Steinberger
a010ce462f perf(test): split light vitest lanes and restore hooks 2026-04-06 15:51:00 +01:00
Vincent Koc
5765c4cb2a fix(check): repair latest command and stream type drift 2026-04-06 15:46:53 +01:00
Vincent Koc
4d405ac5ae chore(plugins): drop final dead test any suppressions 2026-04-06 15:46:31 +01:00
jjjojoj
b35b176837 fix: recognize api.grok.x.ai as xAI-native endpoint
Fixes #61377

The provider attribution code only recognized api.x.ai as the xAI-native
endpoint. Some users have api.grok.x.ai configured (or it appears in
certain DNS/config scenarios) which would not resolve as xAI-native,
causing web_search tool failures.

This change adds api.grok.x.ai as an alias for xAI-native endpoint
classification alongside api.x.ai.
2026-04-06 15:45:34 +01:00
Vincent Koc
6067f2d9ad chore(plugins): drop dead channel test any suppressions 2026-04-06 15:45:18 +01:00
Peter Steinberger
baf4119ae3 fix: harden local TLS gateway probes (#61935) (thanks @ThanhNguyxn07) 2026-04-06 15:44:30 +01:00
ThanhNguyxn07
b77964f704 fix(gateway-status): use local TLS probe targets with fingerprint
When gateway.tls.enabled is true, gateway status probes now target local loopback/tailnet over wss and pass the local TLS fingerprint for localLoopback probes. This avoids false unreachable results for healthy local TLS gateways.

Fixes #61767

Co-authored-by: ThanhNguyxn <thanhnguyentuan2007@gmail.com>
2026-04-06 15:44:30 +01:00
Vincent Koc
3ded10f52a chore(plugins): drop dead test any suppressions 2026-04-06 15:43:48 +01:00
Peter Steinberger
5a54005b4d fix(plugins): restore green runtime gates 2026-04-06 15:42:51 +01:00
Nimrod Gutman
6f566585d8 fix(ios): harden watch exec approval review (#61757)
* fix(ios): harden watch exec approval review

* fix(ios): address watch approval review feedback

* fix(ios): finalize watch approval background recovery

* fix(ios): finalize watch approval background recovery (#61757) (thanks @ngutman)
2026-04-06 17:42:42 +03:00
openperf
e777a2b230 fix(process ): migrate legacy command-queue singleton missing activeTaskWaiters
After a SIGUSR1 in-process restart following an npm upgrade from v2026.4.2
to v2026.4.5, the globalThis singleton created by the old code version
lacks the activeTaskWaiters field added in v2026.4.5.  resolveGlobalSingleton
returns the stale object as-is, causing notifyActiveTaskWaiters() to call
Array.from(undefined) and crash the gateway in a loop.

Add a schema migration step in getQueueState() that patches the missing
field on legacy singleton objects.  Add a regression test that plants a
v2026.4.2-shaped state object and verifies resetAllLanes() and
waitForActiveTasks() succeed without throwing.

Fixes #61905
2026-04-06 15:41:14 +01:00
Vincent Koc
a36bb119be test(secrets): reduce serial vitest drag 2026-04-06 15:40:35 +01:00
Vincent Koc
2815e8ecc0 chore(telegram): drop dead bot helper lint comments 2026-04-06 15:40:13 +01:00
Vincent Koc
e475f5cabf chore(llm-task): drop dead test lint comments 2026-04-06 15:38:56 +01:00
Vincent Koc
fdad227b92 fix(lint): route webhook tests through plugin helpers 2026-04-06 15:38:15 +01:00
Peter Steinberger
ff7fe37d17 refactor(cli): normalize route boundaries 2026-04-06 15:38:04 +01:00
Vincent Koc
e4fa414ed0 refactor(browser): remove remote tab harness any cast 2026-04-06 15:37:46 +01:00
Mason
9b0ea7c579 docs: relocalize stale locale links (#61796)
* docs: relocalize stale locale links

* docs: unify locale link postprocessing

* docs: preserve relocalized frontmatter

* docs: relocalize partial docs runs

* docs: scope locale link postprocessing

* docs: continue scoped relocalization

* docs: drain parallel i18n results

* docs: add i18n pipeline link regression tests

* docs: clarify i18n pipeline regression test intent

* docs: update provider references in i18n tests to use example-provider

* fix: note docs i18n link relocalization

* docs: rephrase gateway local ws sentence
2026-04-06 22:37:35 +08:00
Vincent Koc
7bb61a07db fix(check): repair plugin and secret type drift 2026-04-06 15:36:42 +01:00
Vincent Koc
a253dc44a3 refactor(plugins): remove production lint suppressions 2026-04-06 15:36:21 +01:00
Vincent Koc
586e5f7289 refactor(lint): guard extension runner coverage 2026-04-06 15:34:25 +01:00
Vincent Koc
d14121e648 refactor(lint): align extension runners with root config 2026-04-06 15:33:05 +01:00
Vincent Koc
6e443a20c8 fix(qqbot): remove dead tts config aliases 2026-04-06 15:32:05 +01:00
Peter Steinberger
8326349939 fix(test): stabilize docker claude cli live lane 2026-04-06 15:31:08 +01:00
Peter Steinberger
ac38f332c5 fix(anthropic): prefer claude cli over setup-token 2026-04-06 15:31:07 +01:00
Vincent Koc
b535d1e2b9 chore(lint): drop stale extension overrides 2026-04-06 15:30:30 +01:00
Vincent Koc
f92ef361ae fix(check): finish extension type cleanup 2026-04-06 15:30:17 +01:00
Peter Steinberger
fa67ab2358 fix: preserve gateway-bindable loader compatibility 2026-04-06 15:28:41 +01:00
HansY
c78defdc2f plugins: exclude runtimeSubagentMode from loader cache key
The plugin loader cache key included runtimeSubagentMode, which is
derived from allowGatewaySubagentBinding. Since different call sites in
the message processing pipeline pass different values for this flag,
each call produced a distinct cache key, triggering redundant
register() calls (40+ in 24 seconds after startup).

runtimeSubagentMode does not affect which plugins are loaded or how
they are configured — it is only metadata stored alongside the active
registry state. Removing it from the cache key lets all call sites
share the same cached registry regardless of their binding mode.

Fixes #61756
2026-04-06 15:28:41 +01:00
Peter Steinberger
f18a705d19 fix(test): remove duplicate nostr config import 2026-04-06 15:27:45 +01:00
Peter Steinberger
38543af3c4 fix(discord): classify current gateway fatal errors 2026-04-06 15:27:45 +01:00
Peter Steinberger
f8a97881d1 fix(check): repair extension type drift batch 2026-04-06 15:27:45 +01:00
Peter Steinberger
9e0d632928 fix(gateway): unify session history snapshots 2026-04-06 15:26:55 +01:00
Peter Steinberger
8838fdc916 refactor: share web provider runtime helpers 2026-04-06 15:26:32 +01:00
Peter Steinberger
58f4099a4f refactor: share plugin runtime load context 2026-04-06 15:26:32 +01:00
Gustavo Madeira Santana
9568cceee3 docs: clarify Matrix invite and config guidance 2026-04-06 10:25:47 -04:00
Vincent Koc
01f3959a60 refactor(plugins): share extension oxlint runner 2026-04-06 15:25:34 +01:00
Peter Steinberger
3599ab2e56 fix(agents): honor verbose defaults and trim process setup 2026-04-06 15:24:59 +01:00
Peter Steinberger
cd5b1653f6 feat: declare explicit media provider capabilities 2026-04-06 15:24:38 +01:00
Peter Steinberger
29df67c491 test(acp): prove lazy reset re-ensures bound sessions 2026-04-06 15:24:16 +01:00
Onur
b34fa9c868 ACP: reset bound sessions lazily 2026-04-06 15:24:16 +01:00
Vincent Koc
d3a35d7e95 ci(plugins): add bundled extension lint lane 2026-04-06 15:24:03 +01:00
Peter Steinberger
0337a0d7f8 fix(memory): warn cleanly on degraded vector recall 2026-04-06 15:23:30 +01:00
mainstay22
a224f59fe3 fix(memory): surface warning when sqlite-vec unavailable during index
When chunks_vec cannot be updated (sqlite-vec extension not loaded),
the memory index now emits an error-level warning instead of silently
reporting success.

Before this change: 'Memory index updated (hull).' was emitted even
when the vector index (chunks_vec) was not updated due to sqlite-vec
being unavailable. This masked silent vector recall degradation.

After this change:
- If vector.enabled=true and vector.available=false: emits
  'Memory index WARNING (agentId): chunks_vec not updated — sqlite-vec
  unavailable: <reason>. Vector recall degraded.'
- If vector is healthy: emits normal success message unchanged
- Per-file warning also emitted in writeChunks when chunks are written
  without vector embeddings

Fixes: HELM-0251 (local dist patch — this makes it update-safe)
Related: HELM-0252 (this PR)
2026-04-06 15:23:30 +01:00
Peter Steinberger
987bbe6545 test(browser): assert remote CDP retry timeouts correctly 2026-04-06 15:22:23 +01:00
ThanhNguyxn07
2a1a49bd41 fix(browser): retry remote CDP websocket readiness before failing
Remote browser profiles can pass HTTP reachability while Browser.getVersion on the CDP websocket is still warming up right after restart. Add one retry in ensureBrowserAvailable for remote CDP profiles and cover it with a regression test.

Fixes #57397

Co-authored-by: ThanhNguyxn <thanhnguyentuan2007@gmail.com>
2026-04-06 15:22:23 +01:00
Vincent Koc
620537914b fix(plugins): clean bundled extension lint tail 2026-04-06 15:21:46 +01:00
Peter Steinberger
07b3ee813a fix: clean up rebase follow-up regressions 2026-04-06 15:20:03 +01:00
Peter Steinberger
94b8ab0325 fix: resolve rebase check regressions 2026-04-06 15:20:03 +01:00
Peter Steinberger
8d095147b4 fix: restore check gate 2026-04-06 15:20:03 +01:00
Peter Steinberger
979c81d9dd test(auth): cover readonly runtime auth inheritance 2026-04-06 15:19:34 +01:00
Peter Steinberger
adb750fa63 perf(test): trim secrets runtime snapshot lane 2026-04-06 15:19:34 +01:00
Peter Steinberger
f264c2c7e4 style: format routed command helper 2026-04-06 15:18:55 +01:00
Peter Steinberger
91749930d4 fix: restore check-time path inference 2026-04-06 15:18:55 +01:00
Vincent Koc
da14745f2e fix(check): clean up extension rename fallout 2026-04-06 15:18:24 +01:00
Vincent Koc
e6df924a34 fix(plugins): clean matrix lint types 2026-04-06 15:17:15 +01:00
Peter Steinberger
878c208844 perf(test): restore scoped vitest routing 2026-04-06 15:16:17 +01:00
Vincent Koc
ac6f696baa fix(check): repair typed test and cli drift 2026-04-06 15:14:37 +01:00
Vincent Koc
9502642f47 fix(plugins): clean xai and qqbot lint 2026-04-06 15:14:20 +01:00
biefan
0f075e1b8a fix: restore terminal keyboard state on tui exit (#49130) (thanks @biefan) (#49130)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-06 15:14:08 +01:00
Peter Steinberger
15114a9279 fix(matrix): preserve multi-paragraph list items 2026-04-06 15:13:16 +01:00
Jakub Rusz
be5eebd3d4 fix(matrix): compact loose list HTML for consistent Element rendering
Loose lists (blank lines between items) produce <li><p>...</p></li> via
markdown-it, causing Element to render list numbers on separate lines
from their content. Fix by setting hidden=true on paragraph tokens
inside list items before rendering, mirroring what markdown-it already
does for tight lists.

Closes #60997. Thanks @gucasbrg.

Co-Authored-By: Claude claude-opus-4-6 <noreply@anthropic.com>
Signed-off-by: Jakub Rusz <jrusz@proton.me>
2026-04-06 15:13:16 +01:00
Peter Steinberger
26a5ab1c6f fix(utils): harden directive block placeholders 2026-04-06 15:11:44 +01:00
szx007
af7c21f207 fix: preserve code block indentation in normalizeDirectiveWhitespace
## Summary

- Problem: `normalizeDirectiveWhitespace` applied whitespace-collapsing regexes globally, including inside fenced code blocks (` ``` ` / `~~~`) and indent-code-blocks (4-space / tab), corrupting indentation in assistant replies that contain code snippets
- Why it matters: Any language where indentation is significant (Python, Go, YAML, etc.) or visually meaningful would render incorrectly after stripping inline directive tags
- What changed: Stash code blocks under a Unicode private-use sentinel (`\uE000`) before normalization, run the existing prose regexes on the masked text, then restore the original blocks verbatim
- What did NOT change: All prose normalization rules are retained as-is (`\r\n`, multi-space collapse, leading blank-line strip, trailing whitespace, 3+ newline fold)

## Change Type

- [x] Bug fix

## Scope

- [ ] Gateway / orchestration

## Root Cause

- Root cause: Prose whitespace regexes were applied to the full text string with no awareness of Markdown code block boundaries
- Missing detection / guardrail: No tests covered indented content inside fenced blocks
- Contributing context: Directive tag stripping (`[[reply_to_current]]`, `[[audio_as_voice]]`) is applied before delivery, making the normalization step a silent corruption point for code-heavy replies

## Regression Test Plan

- Coverage level that should have caught this:
  - [x] Unit test
- Target test or file: `src/utils/directive-tags.test.ts`
- Scenario the test should lock in: `parseInlineDirectives` with fenced/indent code blocks must preserve all leading whitespace inside those blocks
- Why this is the smallest reliable guardrail: Pure function with deterministic string in/out; no mocks needed
- If no new test is added, why not: 7 new unit tests added

## User-visible / Behavior Changes

Code blocks in assistant replies containing `[[reply_to_current]]` or `[[audio_as_voice]]` directives now retain correct indentation after the directive is stripped.

## Security Impact

- New permissions/capabilities? No
- Secrets/tokens handling changed? No
- New/changed network calls? No
- Command/tool execution surface changed? No
- Data access scope changed? No

## Compatibility / Migration

- Backward compatible? Yes
- Config/env changes? No
- Migration needed? No

Co-Authored-By: Codemax <codemax@binance.com>
2026-04-06 15:11:44 +01:00
Vincent Koc
1b309fff71 fix(plugins): clean tlon lint types 2026-04-06 15:08:39 +01:00
Peter Steinberger
04e360e7e8 build(lockfile): sync proxy-agent dependency 2026-04-06 15:06:54 +01:00
Peter Steinberger
c7c0550dc9 fix: seed SSE history state from one snapshot (#61855) (thanks @100yenadmin) 2026-04-06 15:05:33 +01:00
Eva
d519f39c6e fix(gateway): eliminate SSE history double-read race — derive sanitized and raw views from single transcript snapshot 2026-04-06 15:05:33 +01:00
Peter Steinberger
732c18cd06 fix(check): repair latest type drift batch 2026-04-06 15:03:55 +01:00
Peter Steinberger
380a396266 refactor: share ambient proxy agent helpers 2026-04-06 15:03:30 +01:00
ToToKr
2da95ca191 fix(tui): strip inbound metadata from command messages before rendering (#59985) (thanks @MoerAI) (#59985)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-06 15:02:59 +01:00
Vincent Koc
c9e2fbef92 fix(plugins): clean bundled extension lint batch 2026-04-06 15:01:05 +01:00
Mariano
ebad21c94d plugins: add bundled webhooks TaskFlow bridge (#61892)
Merged via squash.

Prepared head SHA: ca58fb77a8
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-04-06 15:59:47 +02:00
Peter Steinberger
f3cc8d12d6 perf: speed up local heavy checks 2026-04-06 14:56:53 +01:00
Peter Steinberger
b16e0df5f8 fix(channels): cover pinned registry helper fallback 2026-04-06 14:56:10 +01:00
王淼0668000666
0b198b8d0b fix(channels): use pinned registry as primary in listRegisteredChannelPluginEntries
Fixes issue #61358 where isGatewayMessageChannel intermittently rejects valid third-party channel plugins (openclaw-weixin, qqbot).

The pinned registry contains authoritative channel configurations for delivery, so it should be checked first before falling back to the active plugin registry.
2026-04-06 14:56:10 +01:00
Peter Steinberger
c817e6d388 fix(check): repair monitor and message tool types 2026-04-06 14:55:01 +01:00
Peter Steinberger
9fa5b413f0 style: fix acpx runtime lint types 2026-04-06 14:53:55 +01:00
Peter Steinberger
bfbe1c149c style: fix preflight test rebase fallout 2026-04-06 14:53:55 +01:00
Peter Steinberger
b3f31dee80 style: resolve lint after rebase 2026-04-06 14:53:55 +01:00
Peter Steinberger
af62a2c2e4 style: fix extension lint violations 2026-04-06 14:53:55 +01:00
Peter Steinberger
e8141716b4 build: lint bundled extensions 2026-04-06 14:53:55 +01:00
Martin Garramon
eede8f945f fix(agents): replace .* with \S* in interpreter heuristic regexes to prevent ReDoS
The inner `.*\s+` in `(?:[A-Za-z_][A-Za-z0-9_]*=.*\s+)*` creates
catastrophic backtracking because both `.*` and `\s+` can match
whitespace. When the exec tool processes commands with `VAR=value`
assignments followed by whitespace-heavy text (e.g. HTML heredocs),
the regex engine hangs permanently at 100% CPU.

Replace `.*` with `\S*` in all three instances. Shell prefix variable
assignments cannot contain unquoted whitespace in the value, so `\S*`
is semantically correct and eliminates the ambiguity.

Fixes #61881
2026-04-06 14:53:44 +01:00
Peter Steinberger
c63a4f0f13 refactor: share assistant visible text sanitizer profiles 2026-04-06 14:52:52 +01:00
Vincent Koc
0b32037e96 ci(plugins): add extension channel lint lane 2026-04-06 14:52:40 +01:00
Peter Steinberger
150c4018de refactor: share plugin cli registration helpers 2026-04-06 14:52:21 +01:00
Peter Steinberger
41905d9fd7 refactor: share cli command descriptor helpers 2026-04-06 14:52:20 +01:00
Onur Solmaz
154a7edb7c refactor: consume acpx runtime library (#61495)
* refactor: consume acpx runtime library

* refactor: remove duplicated acpx runtime files

* fix: update acpx runtime dependency

* fix: preserve acp runtime error codes

* fix: migrate legacy acpx session files

* fix: update acpx runtime dependency

* fix: import Dirent from node fs

* ACPX: repin shared runtime engine

* ACPX: repin runtime semantics fixes

* ACPX: repin runtime contract cleanup

* Extensions: repin ACPX after layout refactor

* ACPX: drop legacy session migration

* ACPX: drop direct ACP SDK dependency

* Discord ACP: stop duplicate direct fallback replies

* ACP: rename delivered text visibility hook

* ACPX: pin extension to 0.5.0

* Deps: drop stale ACPX build-script allowlist

* ACPX: add local development guidance

* ACPX: document temporary pnpm exception flow

* SDK: preserve legacy ACP visibility hook

* ACP: keep reset commands on local path

* ACP: make in-place reset start fresh session

* ACP: recover broken bindings on fresh reset

* ACP: defer fresh reset marker until close succeeds

* ACP: reset bound sessions fresh again

* Discord: ensure ACP bindings before /new

* ACP: recover missing persistent sessions
2026-04-06 15:51:08 +02:00
Vincent Koc
4b2d528345 fix(plugins): finish channel lint cleanup 2026-04-06 14:48:35 +01:00
황재원
c8298c5b0f fix: don't broadcast state:error on per-attempt lifecycle errors (#60043) (thanks @jwchmodx) (#60043)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-06 14:47:38 +01:00
Vincent Koc
a6c854f363 fix(commands): resolve provider auth choices from plugin runtime 2026-04-06 14:45:44 +01:00
Vincent Koc
029290c8d0 fix(plugins): clean fifth channel lint batch 2026-04-06 14:45:22 +01:00
Peter Steinberger
712479eea1 fix: unify assistant visible text sanitizers (#61729) 2026-04-06 14:44:09 +01:00
openperf
980439b9e6 fix(Gateway ): strip tool_call and tool_result XML blocks from assistant visible text 2026-04-06 14:44:09 +01:00
Peter Steinberger
f00c8c1b87 fix: add message tool read hint for thread reads 2026-04-06 14:42:51 +01:00
Vincent Koc
4d49c7b8a5 fix(plugins): clean fourth channel lint batch 2026-04-06 14:42:09 +01:00
Peter Steinberger
53f86745e1 test: improve parallels smoke diagnostics 2026-04-06 14:41:30 +01:00
Peter Steinberger
50082f91ff fix: harden windows dev update fallback 2026-04-06 14:41:29 +01:00
Peter Steinberger
00dcc1744e fix: narrow mattermost setup entry seam 2026-04-06 14:41:29 +01:00
Peter Steinberger
55f18f67e2 perf(test): split secrets runtime provider coverage 2026-04-06 14:40:35 +01:00
Peter Steinberger
3da7c8610f fix: preserve slack fallback thread classification (#61835) 2026-04-06 14:38:51 +01:00
Ken Shimizu
05b3d34a92 test(slack): pass isThreadReply in genuine-thread test scenario 2026-04-06 14:38:51 +01:00
Ken Shimizu
7f7cfc794f fix(slack): remove stale issue reference comment 2026-04-06 14:38:51 +01:00
Ken Shimizu
177b326354 fix(slack): remove backward-compat fallback that overrides isThreadReply with incomingThreadTs 2026-04-06 14:38:51 +01:00
Peter Steinberger
4a4741444e refactor(auth): remove codex cli parsing from core store 2026-04-06 14:36:50 +01:00
Vincent Koc
505b980f63 fix(plugins): clean third channel lint batch 2026-04-06 14:34:07 +01:00
Peter Steinberger
021e503a5f test: add raw plugin-schema defaults regression coverage (#61856) 2026-04-06 14:32:17 +01:00
supermario_leo
92ffb9af86 fix(config): restore applyDefaults:true for AJV plugin/channel schema validation 2026-04-06 14:32:17 +01:00
Peter Steinberger
318c0f2e89 fix(plugins): repair channel lint batch types 2026-04-06 14:32:02 +01:00
Vincent Koc
98f222a661 fix(plugins): clean second channel lint batch 2026-04-06 14:29:51 +01:00
Vincent Koc
c3edcfd46e fix(plugins): clean first channel lint batch 2026-04-06 14:27:45 +01:00
Peter Steinberger
9afcbbec5e refactor(auth): extract persisted auth store helpers 2026-04-06 14:25:06 +01:00
Peter Steinberger
bbc7a09aab fix(cli): erase routed definition union at dispatch 2026-04-06 14:24:18 +01:00
Peter Steinberger
0974f85d7e fix(cli): preserve routed command arg typing 2026-04-06 14:21:17 +01:00
Peter Steinberger
d378a504ac fix: restore claude cli guidance and doctor behavior 2026-04-06 14:21:11 +01:00
Peter Steinberger
445133b865 Revert "fix(openai): soften gpt-5 execution bias prompt"
This reverts commit 5875e270862490a75d23835017ba7770c54bb9a8.
2026-04-06 14:21:11 +01:00
Peter Steinberger
73a5504708 refactor: share cli command registrar engine 2026-04-06 14:16:04 +01:00
Peter Steinberger
f43aba40a2 refactor: share cli routing metadata 2026-04-06 14:16:03 +01:00
Peter Steinberger
f3dd9723e1 fix(test): type anthropic replay live transcript 2026-04-06 14:15:08 +01:00
Harold Hunt
0bd0097557 refactor: add xai plugin-sdk boundary canary (#61548)
* docs: plan real plugin-sdk workspace rollout

* build: add xai plugin-sdk boundary canary

* build: generate plugin-sdk package types

* build: hide plugin-sdk core export

* build: alias scoped plugin-sdk runtime imports

* build: repair plugin-sdk boundary drift

* fix(plugins): remove duplicated plugin-sdk entrypoints

* test(plugins): make tsc boundary canary portable

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-06 14:13:11 +01:00
Peter Steinberger
0430bab070 perf(test): split secrets runtime env coverage 2026-04-06 14:13:09 +01:00
Peter Steinberger
21c82ca623 perf(test): trim security audit wrapper coverage 2026-04-06 14:13:08 +01:00
Vincent Koc
18ed43cc9e fix(tui): align /status with shared session status 2026-04-06 14:10:59 +01:00
Peter Steinberger
191b7cb5e6 fix: preserve anthropic replay tool results 2026-04-06 14:08:04 +01:00
Peter Steinberger
ab495f4c90 test: align session status runtime and agent expectations 2026-04-06 14:05:01 +01:00
Peter Steinberger
a8a49d142f fix: mirror codex cli auth into runtime store 2026-04-06 14:05:01 +01:00
Peter Steinberger
c9e4b86c7e fix: tighten container bind defaults for landing (#61818) (thanks @openperf) 2026-04-06 14:02:20 +01:00
openperf
c857e93735 fix(gateway): auto-bind to 0.0.0.0 inside container environments 2026-04-06 14:02:20 +01:00
Peter Steinberger
4a91b4f3a5 test: fix rebased precheck routing fixture (#61651) 2026-04-06 14:01:21 +01:00
Peter Steinberger
a42ee69ad4 fix: harden tool-result overflow recovery (#61651) 2026-04-06 14:01:21 +01:00
Tak Hoffman
4917009ac7 Prefer recent aggregate tool-result truncation 2026-04-06 14:01:21 +01:00
Tak Hoffman
5e04b2d037 Fix mixed tool-result recovery truncation 2026-04-06 14:01:21 +01:00
Tak Hoffman
6822d828fe Add overflow recovery routing regressions 2026-04-06 14:01:21 +01:00
Tak Hoffman
222cd37e33 Use zero-floor recovery tool truncation 2026-04-06 14:01:21 +01:00
Tak Hoffman
66daafccae Refine cause-aware precheck overflow routing 2026-04-06 14:01:21 +01:00
Tak Hoffman
e55c82a7e7 Unify tool-result fallback notice with PI style 2026-04-06 14:01:21 +01:00
Tak Hoffman
a8fb094c5b Handle aggregate tool-result overflow fallback 2026-04-06 14:01:21 +01:00
Tak Hoffman
09b7c00dab Restore readable tool-result overflow fallback 2026-04-06 14:01:21 +01:00
Tak Hoffman
3e2a05f425 Restore reserve-based overflow precheck 2026-04-06 14:01:21 +01:00
Tak Hoffman
ceb686052b Align subagent truncation notice wording 2026-04-06 14:01:21 +01:00
Tak Hoffman
cbc2945117 remove openclaw-only tool overflow compatibility layer 2026-04-06 14:01:21 +01:00
Tak Hoffman
7fc1a74ee9 align tool-result truncation with pi semantics 2026-04-06 14:01:21 +01:00
Vincent Koc
5c1b1eb169 fix(check): repair current main type drift 2026-04-06 13:56:57 +01:00
Peter Steinberger
a8f4c50f18 fix(discord): tolerate carbon request option additions 2026-04-06 13:56:01 +01:00
Vincent Koc
d8226037c3 fix(media): lazy load file-type sniffing 2026-04-06 13:52:18 +01:00
Peter Steinberger
5edabf4776 refactor: share cli command registration policy 2026-04-06 13:51:51 +01:00
Peter Steinberger
a21709d041 refactor: share cli startup and routing helpers 2026-04-06 13:51:51 +01:00
Peter Steinberger
6ed33d29c8 fix(windows): disable native jiti setup loaders 2026-04-06 13:48:32 +01:00
Peter Steinberger
1c41987876 refactor(auth): split auth state from auth store 2026-04-06 13:42:44 +01:00
Vincent Koc
35af6cc49c fix(status): keep plain json off security audit path 2026-04-06 13:42:21 +01:00
Peter Steinberger
a2b065b090 fix(openai): soften gpt-5 execution bias prompt 2026-04-06 13:40:43 +01:00
Peter Steinberger
ef923805f5 Revert "refactor(cli): remove custom cli backends"
This reverts commit 6243806f7b.
2026-04-06 13:40:42 +01:00
Peter Steinberger
c39f061003 Revert "refactor(cli): remove bundled cli text providers"
This reverts commit 05d351c430.
2026-04-06 13:40:41 +01:00
Vincent Koc
5fa166ed11 fix(check): repair status report typing drift 2026-04-06 13:34:08 +01:00
Peter Steinberger
7e0e2f81e5 refactor(auth): isolate external oauth overlays 2026-04-06 13:30:25 +01:00
Peter Steinberger
49e3ecfe5e perf(test): isolate deep probe finding helper 2026-04-06 13:29:35 +01:00
Peter Steinberger
eb0570d593 perf(test): merge secrets runtime snapshot lanes 2026-04-06 13:29:34 +01:00
ForestDengHK
e79e25667a fix(telegram): restore outbound message splitting for long messages (#57816)
Merged via squash.

Prepared head SHA: 09f24ceba9
Co-authored-by: ForestDengHK <189603301+ForestDengHK@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-04-06 14:28:37 +02:00
Vincent Koc
d7086526b0 docs(video): describe mode-aware generation capabilities 2026-04-06 13:25:53 +01:00
Peter Steinberger
45875ed532 chore(deps): update dependencies 2026-04-06 13:25:17 +01:00
Evgeny Yakimov
68a4f91d5a feat(google): add support for Gemma 4 models and fix cross-provider resolution 2026-04-06 13:24:48 +01:00
Peter Steinberger
b04dd6d05c refactor: consolidate session history sanitization 2026-04-06 13:23:44 +01:00
Ted Li
23730229e1 fix(memory-core): ignore managed dreaming blocks during daily ingestion (#61720) (thanks @MonkeyLeeT) 2026-04-06 13:22:54 +01:00
Peter Steinberger
10554644aa perf(test): trim security gateway auth test path 2026-04-06 13:22:46 +01:00
Peter Steinberger
f0a0b98c8d perf(test): refine secrets runtime activation coverage 2026-04-06 13:22:45 +01:00
Peter Steinberger
0ab877bd13 refactor: share status report section builders 2026-04-06 13:22:23 +01:00
Peter Steinberger
143f501fe5 refactor: share status overview and json helpers 2026-04-06 13:22:23 +01:00
Neerav Makwana
ad2df63547 fix(agents): classify Anthropic extra-usage billing (#61608) (thanks @neeravmakwana) 2026-04-06 13:21:53 +01:00
Neerav Makwana
7df5f70242 fix(agents): skip redundant partial compaction summarization (#61603) (thanks @neeravmakwana) 2026-04-06 13:21:07 +01:00
Neerav Makwana
177e23801b fix(telegram): bound startup request timeouts (#61601) (thanks @neeravmakwana) 2026-04-06 13:20:15 +01:00
Vincent Koc
6b53a9aadb feat(video): add mode-aware generation capabilities 2026-04-06 13:19:51 +01:00
Neerav Makwana
9aaa000da0 fix(gateway): show /tts audio in Control UI webchat (#61598) (thanks @neeravmakwana) 2026-04-06 13:19:38 +01:00
foxtrot026
02c092e558 fix(model-ref): recompute suffix after @YYYYMMDD + add @8bit test 2026-04-06 13:18:59 +01:00
foxtrot026
5208a85afe fix(model-ref): treat LM Studio/Ollama @q*/@4bit suffixes as model-id 2026-04-06 13:18:59 +01:00
Peter Steinberger
d4da45c202 perf(test): split remaining security audit coverage 2026-04-06 13:14:52 +01:00
Peter Steinberger
dcaf8c47e3 perf(test): split secrets auth runtime coverage 2026-04-06 13:14:52 +01:00
Vincent Koc
e69cfc3e3b fix(plugin-sdk): restore compat auth helper exports 2026-04-06 13:14:02 +01:00
oliviareid-svg
089423bbaa fix(macos): strip commit hash from CLI version output (#61111)
Merged via squash.

Prepared head SHA: 6478de0b4e
Co-authored-by: oliviareid-svg <269669958+oliviareid-svg@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
Reviewed-by: @ImLukeF
2026-04-06 22:10:40 +10:00
Vincent Koc
a5d2e89d3d refactor(auth): drop provider auth storage switchboard 2026-04-06 13:08:58 +01:00
Vincent Koc
58409cd5c5 fix(zalo): lazy load webhook monitor surface 2026-04-06 13:06:41 +01:00
Peter Steinberger
f1b6b97df3 perf(test): split security audit coverage 2026-04-06 13:05:39 +01:00
Peter Steinberger
bc160c0613 perf(test): split secrets runtime coverage 2026-04-06 13:05:38 +01:00
Peter Steinberger
f0290b4732 docs(changelog): note discord forwarded reference recovery (#61670) 2026-04-06 13:01:51 +01:00
Peter Steinberger
7f11941134 fix(windows): preserve plugin loader alias resolution (#61832) (thanks @Zeesejo)
# Conflicts:
#	CHANGELOG.md
#	src/plugins/loader.ts
2026-04-06 13:01:51 +01:00
Peter Steinberger
d43ac5d14c fix(discord): restore carbon beta 2026-04-06 13:01:22 +01:00
Peter Steinberger
88aa814226 refactor: consolidate status runtime and overview helpers 2026-04-06 12:57:09 +01:00
Peter Steinberger
e8731589c0 refactor: share status scan and report helpers 2026-04-06 12:55:56 +01:00
XING
60dc6a22c9 fix(discord): restore snapshot forwarding helpers 2026-04-06 20:54:54 +09:00
XING
c5493b15d6 fix(discord): recover forwarded referenced message content
# Conflicts:
#	extensions/discord/src/monitor/message-utils.ts
2026-04-06 20:54:54 +09:00
Vincent Koc
2d75be0ea7 fix(onboard): move provider auth ids out of core types 2026-04-06 12:52:34 +01:00
Peter Steinberger
bbd0702c79 fix(agents): narrow phase-aware history hardening (#61829) (thanks @100yenadmin) 2026-04-06 20:52:27 +09:00
Eva
3d9c6affce gateway: fix bounded SSE sanitization and rawTranscriptSeq init
Apply sanitizeChatHistoryMessages before pagination in the bounded SSE
history refresh path, consistent with the unbounded path. Initialize
rawTranscriptSeq from the raw transcript's last __openclaw.seq value
instead of the sanitized history length, preventing seq drift when
sanitization drops messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:52:27 +09:00
Eva
029ed5d32a fix: harden phase-aware history sanitization 2026-04-06 20:52:27 +09:00
Eva
4bded29f2a fix(agents): address review feedback on #61481 phase-integrity hardening 2026-04-06 20:52:27 +09:00
Eva
b099427570 fix(gateway): sanitize bounded SSE refresh + deduplicate constant
- Bounded/cursor SSE refresh path now sanitizes through
  sanitizeChatHistoryMessages before paginating, matching the
  unbounded path and initial history load.
- Export DEFAULT_CHAT_HISTORY_TEXT_MAX_CHARS from chat.ts and
  import in sessions-history-http.ts instead of duplicating.
2026-04-06 20:52:27 +09:00
Eva
7634bdeb2c fix: restore required imports and fix SSE sequence tracking 2026-04-06 20:52:27 +09:00
Eva
6f95fd448f fix(agents): address review feedback on phase hardening 2026-04-06 20:52:27 +09:00
Eva
dea515e833 fix: sanitize SSE history fast path and preserve cursor paging 2026-04-06 20:52:27 +09:00
Eva
e7311334cb fix: harden phase-aware assistant visibility 2026-04-06 20:52:27 +09:00
Vincent Koc
e611761809 fix(plugins): move acpx config contracts into manifests 2026-04-06 12:33:20 +01:00
Yossi Eliaz
045d956111 fix(ollama): resolve per-provider baseUrl in createStreamFn
The createStreamFn callback hardcoded config.models.providers.ollama.baseUrl,
ignoring the actual provider ID from the context. When multiple Ollama providers
are configured on different ports (e.g. ollama on 11434, ollama2 on 11435), all
requests routed to the first provider's port.

Export resolveConfiguredOllamaProviderConfig from stream.ts and use it with the
ctx.provider parameter to dynamically look up the correct baseUrl per provider.

Closes #61678
2026-04-06 20:28:07 +09:00
Qinyao He
2989b78c12 fix: address lint curly rule and remove extra blank line 2026-04-06 20:27:28 +09:00
Qinyao He
8818184da0 fix: address review — broaden sonnet-4 check, deduplicate helper
- Use `sonnet-4` substring match instead of enumerating `sonnet-4-5`,
  `sonnet-4-6` explicitly. This is safe because legacy `claude-3-5-sonnet`
  does not contain `sonnet-4`, and it future-proofs for sonnet-4-7+.
- Export `shouldPreserveThinkingBlocks` from provider-replay-helpers.ts
  and import it in transcript-policy.ts instead of duplicating the logic.

Addresses review feedback from Greptile.
2026-04-06 20:27:28 +09:00
Qinyao He
88b4ebeaf6 test: fix provider-model-shared test expectations for Sonnet 4.6
The shared-helper tests still expected dropThinkingBlocks: true for
claude-sonnet-4-6. Updated to match the new behavior where Sonnet 4.6
preserves thinking blocks.
2026-04-06 20:27:28 +09:00
Qinyao He
7a3514664d fix: preserve thinking blocks for Claude Opus 4.5+/Sonnet 4.5+ to fix cache
Claude Opus 4.5+ and Sonnet 4.5+ preserve thinking blocks in model context
by default. Dropping them from prior turns (as was correct for Sonnet 3.7)
breaks Anthropic's prefix-based prompt cache matching, causing cache misses
after every thinking turn.

This change conditions dropThinkingBlocks on the model version:
- Preserve (no drop) for: opus-4.x, sonnet-4.5+, haiku-4.x, and future models
- Drop for: claude-3-7-sonnet and earlier

Fixes #61793

See: https://platform.claude.com/docs/en/build-with-claude/extended-thinking#differences-in-thinking-across-model-versions
2026-04-06 20:27:28 +09:00
Vincent Koc
2751874cbb test(memory-core): align short-term repair expectations 2026-04-06 12:26:45 +01:00
Vincent Koc
513c8587b8 fix(cli): keep status json startup path lean 2026-04-06 12:24:32 +01:00
Peter Steinberger
b4a5156bc3 fix: restore rebased main tsgo 2026-04-06 12:16:49 +01:00
Peter Steinberger
f3c29e840c fix: restore current main ci gates 2026-04-06 12:14:26 +01:00
Vincent Koc
4133e3bb1d fix(runtime): narrow bundled runtime startup surfaces 2026-04-06 12:12:53 +01:00
Vincent Koc
24eef3d6e3 fix(net): accept mutable dns lookup results 2026-04-06 12:12:53 +01:00
Ayaan Zaidi
c352fe8903 fix: filter heartbeat pairs before context shaping 2026-04-06 16:41:57 +05:30
Vincent Koc
209786bb2d fix(plugins): remove xai boundary leaks 2026-04-06 12:08:44 +01:00
David
57f9f0a08d fix: stop heartbeat transcript truncation races (#60998) (thanks @nxmxbbd) 2026-04-06 16:26:38 +05:30
Vincent Koc
4154bd707a test(contracts): route bundled contract tests through sdk facades 2026-04-06 11:35:40 +01:00
Vincent Koc
47f0dc3adb fix(net): normalize single-result SSRF lookups 2026-04-06 11:35:30 +01:00
Vincent Koc
29a56793a7 test(logging): share temp log path helper 2026-04-06 11:05:36 +01:00
Vincent Koc
6efbebefbf fix(runtime): drop legacy x_search auth shim 2026-04-06 11:03:58 +01:00
Vincent Koc
3ff606e490 test(logging): reuse suite temp root tracker in env logger tests 2026-04-06 11:01:53 +01:00
Vincent Koc
962e0139b8 test(media-understanding): reuse temp dir helper in video runner tests 2026-04-06 10:59:44 +01:00
Vincent Koc
95a0b47df6 test(media-understanding): reuse temp dir helper in misc attachment tests 2026-04-06 10:59:39 +01:00
Vincent Koc
f08f678dd2 test(logging): reuse suite temp root tracker in console capture tests 2026-04-06 10:59:32 +01:00
Vincent Koc
77472205d5 test(logging): reuse suite temp root tracker in timestamp tests 2026-04-06 10:56:41 +01:00
Vincent Koc
dd0ecf6d0f test(logging): reuse suite temp root tracker in log size cap tests 2026-04-06 10:56:35 +01:00
Vincent Koc
372df37df3 test(media): reuse suite temp root tracker in media redirect tests 2026-04-06 10:55:46 +01:00
Vincent Koc
62cc3a31ee test(media): reuse suite temp root tracker in web media tests 2026-04-06 10:55:25 +01:00
Vincent Koc
44c3572d40 test(media): reuse suite temp root tracker in outside-workspace media tests 2026-04-06 10:54:48 +01:00
Vincent Koc
ab93e9e30a test(media): reuse suite temp root tracker in media server tests 2026-04-06 10:53:31 +01:00
Vincent Koc
2e2a52dade test(config): reuse temp dir helper in config doc baseline tests 2026-04-06 10:52:12 +01:00
Vincent Koc
a1b6e679e4 test(config): reuse temp dir helper in config include tests 2026-04-06 10:50:44 +01:00
Vincent Koc
644a22af4b test(config): reuse suite temp root tracker in config write tests 2026-04-06 10:50:36 +01:00
Vincent Koc
2c06795afa test(config): reuse suite temp root tracker in session cache tests 2026-04-06 10:48:45 +01:00
Vincent Koc
9634a1c60c test(config): reuse shared temp dir helper in channel configured tests 2026-04-06 10:48:41 +01:00
@zimeg
f17f319fae docs(slack): suggest new apps use example manifests 2026-04-06 02:13:49 -07:00
Mason
1a270f81c3 fix(docs): remove zh-CN homepage redirect override (#61751)
Merged via squash.

Prepared head SHA: 4da417e159
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
2026-04-06 16:56:07 +08:00
Ayaan Zaidi
17573d097b fix: route mattermost bundled entry through plugin api 2026-04-06 14:17:32 +05:30
Ayaan Zaidi
03523c65d5 fix: refresh web tool and audit typing 2026-04-06 14:05:49 +05:30
Ayaan Zaidi
0bfe6710a2 fix: align gateway approval typings 2026-04-06 14:05:49 +05:30
Ayaan Zaidi
279f56e658 fix: restore status command typing after refactor 2026-04-06 14:05:49 +05:30
Ayaan Zaidi
31479023d6 test: align config gating mock return types 2026-04-06 14:05:49 +05:30
Ayaan Zaidi
2d49352e80 fix: restore subagents command typing after refactor 2026-04-06 14:05:49 +05:30
@zimeg
f7068a1349 docs(slack): move authorship scopes 2026-04-06 00:58:05 -07:00
Ayaan Zaidi
77497656bf fix: update changelog for exec node elevated routing (#61739) 2026-04-06 13:26:18 +05:30
Ayaan Zaidi
c0a0e295cb fix: preserve explicit node routing under elevated auto exec 2026-04-06 13:26:18 +05:30
Peter Steinberger
7bae391f33 perf(secrets): split runtime snapshot coverage 2026-04-06 08:18:40 +01:00
Peter Steinberger
2810a4f5b6 perf(test): split audit channel security coverage 2026-04-06 08:18:40 +01:00
@zimeg
ec20e33e36 docs(slack): add http request url example manifest 2026-04-06 00:18:08 -07:00
@zimeg
9bf465e54c docs(slack): use http request url term 2026-04-05 23:56:37 -07:00
Peter Steinberger
72dcf94221 refactor: consolidate status reporting helpers 2026-04-06 07:41:08 +01:00
Peter Steinberger
f7833376ea refactor: share command config resolution 2026-04-06 07:41:08 +01:00
Peter Steinberger
bb01e49192 refactor: share gateway auth and approval helpers 2026-04-06 07:41:08 +01:00
Peter Steinberger
1d8d2ddaa1 refactor: dedupe plugin and outbound helpers 2026-04-06 07:41:08 +01:00
Peter Steinberger
9d92de42cf perf(test): split security audit coverage 2026-04-06 07:32:12 +01:00
Peter Steinberger
73485c2300 perf(secrets): trim runtime import walls 2026-04-06 07:32:12 +01:00
Peter Steinberger
c9fc6f5a56 perf(test): split extra params wrapper coverage 2026-04-06 07:32:12 +01:00
Chunyue Wang
9631e4d449 fix(anthropic): restore OAuth guard in service-tier stream wrappers (#60356)
Merged via squash.

Prepared head SHA: 7d58befec8
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-04-05 22:50:39 -07:00
Peter Steinberger
0c63fccc1e perf(test): split extra params resolver coverage 2026-04-06 06:45:05 +01:00
Peter Steinberger
b432dc5af9 perf(test): trim secrets runtime coverage 2026-04-06 06:45:05 +01:00
Peter Steinberger
a1eb677241 perf(test): split subagent command coverage 2026-04-06 06:45:05 +01:00
Vincent Koc
8b8c9d4356 test(config): reuse shared temp dir helper in store read tests 2026-04-06 06:36:30 +01:00
Vincent Koc
7846492686 test(config): reuse shared temp dir helpers in sessions tests 2026-04-06 06:36:05 +01:00
Vincent Koc
aef06906dd test(config): reuse suite temp root tracker in store pruning integration tests 2026-04-06 06:34:55 +01:00
Vincent Koc
8bd089a620 test(config): reuse suite temp root tracker in session key normalization tests 2026-04-06 06:34:07 +01:00
Vincent Koc
fa9b3fb13a test(config): share session test fixture helper 2026-04-06 06:33:38 +01:00
Vincent Koc
c13352a7ef test(config): reuse temp dir helper in disk budget tests 2026-04-06 06:32:44 +01:00
Vincent Koc
4c748e0608 test(config): reuse temp dir helper in config surface tests 2026-04-06 06:32:38 +01:00
Vincent Koc
2d1d3b6ced test(infra): reuse suite temp root tracker in session cost tests 2026-04-06 06:28:27 +01:00
Vincent Koc
e7de00c363 test(e2e): reuse suite temp root tracker in docker setup tests 2026-04-06 06:27:48 +01:00
Vincent Koc
6f2c258011 test(core): reuse shared temp dir helper in logger tests 2026-04-06 06:27:34 +01:00
Vincent Koc
0b1496f876 test(infra): reuse suite temp root tracker in device pairing tests 2026-04-06 06:26:02 +01:00
Vincent Koc
795e7b2c8f test(infra): reuse temp dir helper in node pairing tests 2026-04-06 06:25:01 +01:00
Vincent Koc
908a96e242 test(core): reuse shared temp dir helpers in utils tests 2026-04-06 06:24:01 +01:00
Vincent Koc
691aa7e052 test(infra): reuse temp dir helper in clawhub tests 2026-04-06 06:23:56 +01:00
Vincent Koc
01feed6334 test(infra): reuse temp dir helper in global update tests 2026-04-06 06:23:05 +01:00
Vincent Koc
5cc3f0489b test(infra): reuse suite temp root tracker in startup checks 2026-04-06 06:21:35 +01:00
Vincent Koc
330ac96b96 test(infra): reuse suite temp root tracker in install tests 2026-04-06 06:20:04 +01:00
Vincent Koc
7f5fa0000e test(infra): reuse suite temp root tracker in provider auth tests 2026-04-06 06:19:42 +01:00
Vincent Koc
33e77b435e test(infra): reuse suite temp root tracker in update tests 2026-04-06 06:14:11 +01:00
Vincent Koc
7c629d3e8b test(infra): share suite temp root tracker in infra tests 2026-04-06 06:13:32 +01:00
Vincent Koc
138e85c88e test(infra): share sync temp dir helper in approval tests 2026-04-06 06:12:21 +01:00
Vincent Koc
bb2b6f6a68 test(infra): reuse temp dir helper in update status tests 2026-04-06 06:11:26 +01:00
Vincent Koc
2e497291e4 test(infra): reuse temp dir helper in fs safety tests 2026-04-06 06:10:02 +01:00
Vincent Koc
7ede6ec5dc test(infra): share tracked temp dirs in apns tests 2026-04-06 06:08:50 +01:00
Vincent Koc
b92ae04590 test(infra): share temp dir cleanup in git metadata tests 2026-04-06 06:08:30 +01:00
Vincent Koc
43c5206db4 test(infra): reuse temp dir helper in sentinel and provider tests 2026-04-06 06:07:05 +01:00
Vincent Koc
20229cba6e test(infra): reuse temp dir helper in state and watch tests 2026-04-06 06:06:30 +01:00
Vincent Koc
906533ed50 test(infra): reuse temp dir helper in node path tests 2026-04-06 06:06:30 +01:00
Peter Steinberger
639ba13ea9 test: remove legacy commands monolith 2026-04-06 06:03:25 +01:00
Peter Steinberger
7c160f2402 perf(test): trim subagent command imports 2026-04-06 06:03:25 +01:00
Peter Steinberger
104df3360e perf(test): split reply command coverage 2026-04-06 06:03:25 +01:00
Vincent Koc
c6611639ab test(infra): reuse temp dir helper in install path safety tests 2026-04-06 06:01:01 +01:00
Vincent Koc
7ad0b82816 test(infra): reuse temp dir helpers in install source tests 2026-04-06 06:00:38 +01:00
Vincent Koc
170a7e1a99 test(infra): reuse temp dir helper in run-node tests 2026-04-06 05:59:31 +01:00
Vincent Koc
3dfb086292 test(infra): reuse temp dir helper in utility file tests 2026-04-06 05:59:01 +01:00
Vincent Koc
0d23107f4f test(infra): reuse shared temp dir helpers in small file tests 2026-04-06 05:58:18 +01:00
Vincent Koc
6a3d5127ee test(plugins): reuse suite temp helper in bundle contract test 2026-04-06 05:56:45 +01:00
Vincent Koc
29d3571e79 test(plugins): reuse tracked temp helpers in package contract tests 2026-04-06 05:56:17 +01:00
Vincent Koc
c9bc0dbe05 test(plugins): reuse suite temp root helper in install fixture tests 2026-04-06 05:54:29 +01:00
Vincent Koc
c75cdf6b0b test(plugins): share suite temp root helper in install path tests 2026-04-06 05:53:53 +01:00
Vincent Koc
17d7483404 test(plugins): reuse tracked temp helpers in loader fixture tests 2026-04-06 05:52:27 +01:00
Vincent Koc
ddea9a6c01 test(plugins): share async temp helpers in marketplace tests 2026-04-06 05:52:10 +01:00
Vincent Koc
f3f42e6bbf test(plugins): reuse tracked temp helpers in fixture tests 2026-04-06 05:50:26 +01:00
Vincent Koc
26c34f816d test(plugins): reuse tracked temp helpers in path resolution tests 2026-04-06 05:50:26 +01:00
Vincent Koc
d85dbe1d4a test(plugins): reuse tracked temp helpers in runtime staging tests 2026-04-06 05:50:26 +01:00
Peter Steinberger
b3b5945bdc test: reset telegram dispatch mocks between cases 2026-04-06 05:49:04 +01:00
Vincent Koc
0f7acdfa22 test(unit): reuse temp dir helper in install-sh version tests 2026-04-06 05:46:27 +01:00
Vincent Koc
859c8133c0 test(tooling): reuse temp dir helpers in script tests 2026-04-06 05:45:36 +01:00
Vincent Koc
2cffbc4854 test(root): reuse temp dir helper in launcher e2e 2026-04-06 05:44:49 +01:00
Vincent Koc
0b658a9d5f test(root): reuse temp dir helper in scoped vitest config 2026-04-06 05:43:48 +01:00
Vincent Koc
ce50b97c86 test(root): share temp dir helper across root tests 2026-04-06 05:43:48 +01:00
ToToKr
d4c443bc1e fix(matrix): pass deviceId through health probe to prevent storage-meta overwrite (#61317) (#61581)
Merged via squash.

Prepared head SHA: b0495dc6ca
Co-authored-by: MoerAI <26067127+MoerAI@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-06 00:42:22 -04:00
Vincent Koc
728aee277f test(root): clean up pre-commit temp repos 2026-04-06 05:39:32 +01:00
Vincent Koc
bcf6e89e90 test(root): reuse temp repo helper in clawhub release tests 2026-04-06 05:39:32 +01:00
Peter Steinberger
b62badd8a3 fix: restore main ci type checks 2026-04-06 05:38:25 +01:00
Vincent Koc
319217a30d test(scripts): add async temp dir helper 2026-04-06 05:37:38 +01:00
Vincent Koc
2272eb9ffa test(scripts): reuse temp dir helpers in repo fixtures 2026-04-06 05:36:33 +01:00
Vincent Koc
b1ae35d602 test(scripts): reuse temp dir helpers in runtime tests 2026-04-06 05:35:38 +01:00
Vincent Koc
d77dbd699c test(scripts): share temp dir helpers 2026-04-06 05:35:00 +01:00
Vincent Koc
b2cc5ab636 docs: add contextInjection config key to reference 2026-04-06 05:29:34 +01:00
Vincent Koc
a896d5df0c test(memory-core): reuse workspace helper in temp dir tests 2026-04-06 05:28:18 +01:00
Vincent Koc
9ba97ceaed perf(agents): add continuation-skip context injection (#61268)
* test(agents): cover continuation bootstrap reuse

* perf(agents): add continuation-skip context injection

* docs(changelog): note context injection reuse

* perf(agents): bound continuation bootstrap scan

* fix(agents): require full bootstrap proof for continuation skip

* fix(agents): decide continuation skip under lock

* fix(commands): re-export subagent chat message type

* fix(agents): clean continuation rebase leftovers
2026-04-06 05:27:28 +01:00
Vincent Koc
39099b8022 test(memory-core): reuse workspace helper in dreaming tests 2026-04-06 05:27:17 +01:00
Vincent Koc
036b35e137 test(plugin-sdk): reuse temp dir helpers in facade tests 2026-04-06 05:26:33 +01:00
Vincent Koc
db7f4d3193 test(plugin-sdk): share temp dir test helper 2026-04-06 05:25:04 +01:00
Vincent Koc
f02f16db01 test(memory-core): reuse narrative workspace helper 2026-04-06 05:23:05 +01:00
Vincent Koc
b0f11f4eef test(memory-core): share workspace test helper 2026-04-06 05:21:45 +01:00
Vincent Koc
cd2f6746f9 test(memory-wiki): share plugin test helpers 2026-04-06 05:19:51 +01:00
Vincent Koc
33926ecef1 test(memory-core): align dreaming expectations 2026-04-06 05:17:49 +01:00
Chunyue Wang
b682202016 fix: stop emitting post-background exec updates (#61627) (thanks @openperf)
* fix(exec ): stop emitting tool updates after session is backgrounded

When an exec session is backgrounded (background: true), the owning
agent run resolves its tool-call promise and may finish.  The stdout
handler's emitUpdate() closure, however, kept invoking opts.onUpdate(),
delivering tool_execution_update events to a listener whose active run
had already ended.  This surfaced as an unhandled rejection and crashed
the gateway process.

Guard emitUpdate() with a session.backgrounded || session.exited check
so that post-background output is still captured via appendOutput() but
no longer forwarded to the (now-stale) agent-loop callback.

Fixes #61592

* style: trim exec backgrounding comments

* fix: stop emitting post-background exec updates (#61627) (thanks @openperf)

* fix: place exec changelog entry at end of fixes (#61627) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-06 09:47:30 +05:30
Peter Steinberger
e6d6b10470 build: refresh pnpm lockfile 2026-04-06 05:14:10 +01:00
Vincent Koc
1835493aa5 docs(memory): add promote-explain and rem-harness CLI reference 2026-04-06 05:10:15 +01:00
Vincent Koc
0fdf9e874b fix(config): normalize channel streaming config shape (#61381)
* feat(config): add canonical streaming config helpers

* refactor(runtime): prefer canonical streaming accessors

* feat(config): normalize preview channel streaming shape

* test(config): lock streaming normalization followups

* fix(config): polish streaming migration edges

* chore(config): refresh streaming baseline hash
2026-04-06 05:08:20 +01:00
Peter Steinberger
93ddcb37de chore: bump version to 2026.4.6 2026-04-06 05:04:44 +01:00
Peter Steinberger
57fae2e8fa fix: restore protocol and extension ci 2026-04-06 05:04:29 +01:00
Peter Steinberger
732cdaf408 style(reply): normalize subagent import order 2026-04-06 04:59:35 +01:00
Peter Steinberger
6ceb6e93ad refactor(reply): extract subagent text helper 2026-04-06 04:59:34 +01:00
Peter Steinberger
8796a82ce4 perf(reply): lazy load compact runtime 2026-04-06 04:59:34 +01:00
Peter Steinberger
b40e28f76e perf(test): split reply command coverage 2026-04-06 04:59:34 +01:00
Peter Steinberger
e47e72e3ca chore: update appcast for 2026.4.5 2026-04-06 04:58:26 +01:00
Vincent Koc
5716d83336 feat(memory-wiki): restore llm wiki stack 2026-04-06 04:56:52 +01:00
Gustavo Madeira Santana
9fc2a9feeb docs(matrix): clarify historyLimit default 2026-04-05 23:54:02 -04:00
Peter Steinberger
4f1cbcdcd9 feat(qa): add attachment understanding scenario 2026-04-06 04:46:28 +01:00
Peter Steinberger
2285bacd21 fix(qa): support image understanding inputs 2026-04-06 04:46:27 +01:00
Peter Steinberger
9f8900bb3c test: tighten allowlist fixture typing 2026-04-06 04:44:39 +01:00
Peter Steinberger
4aeabf95cc fix: stabilize contract loader seams 2026-04-06 04:40:47 +01:00
Peter Steinberger
4a690b452a fix(discord): narrow binding runtime imports 2026-04-06 04:38:52 +01:00
Gustavo Madeira Santana
12f3c36ba8 Docs: clarify Matrix autoJoin invite scope 2026-04-05 23:33:29 -04:00
Gustavo Madeira Santana
8d88c27f19 fix(matrix): harden startup auth bootstrap (#61383)
Merged via squash.

Prepared head SHA: d8011a9308
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-05 23:30:40 -04:00
Peter Steinberger
1373ac6c9e feat(qa): execute ten new repo-backed scenarios 2026-04-06 04:28:33 +01:00
Peter Steinberger
746b112dac fix(openai): allow qa image generation mock routing 2026-04-06 04:28:33 +01:00
Peter Steinberger
e29ebc0417 perf(test): split allowlist and models command coverage 2026-04-06 04:22:26 +01:00
Peter Steinberger
74b22440a6 test: fix subagent command result assertions 2026-04-06 04:20:07 +01:00
Peter Steinberger
f3d73628ad fix: install bun in npm release preflight 2026-04-06 04:19:20 +01:00
Peter Steinberger
2a5c355688 fix(ci): patch main regression surfaces 2026-04-06 04:17:52 +01:00
Peter Steinberger
82ad0f6b24 perf(test): split subagent command coverage 2026-04-06 04:11:44 +01:00
Peter Steinberger
3e72c0352d chore: release 2026.4.5 2026-04-06 04:04:21 +01:00
Peter Steinberger
f2ea42e8c2 fix(ci): stabilize control ui locale checks 2026-04-06 04:02:26 +01:00
Peter Steinberger
05fe841dcd fix: restore plugin boundary and ui locale ci gates 2026-04-06 03:53:32 +01:00
github-actions[bot]
3e6160f153 chore(ui): refresh pl control ui locale 2026-04-06 02:51:39 +00:00
github-actions[bot]
53d1280d91 chore(ui): refresh id control ui locale 2026-04-06 02:51:14 +00:00
github-actions[bot]
5815d6e5d4 chore(ui): refresh uk control ui locale 2026-04-06 02:51:01 +00:00
github-actions[bot]
0dac81f123 chore(ui): refresh tr control ui locale 2026-04-06 02:50:39 +00:00
Peter Steinberger
62b61e0703 test: capture windows npm debug tails in smoke logs 2026-04-06 03:50:20 +01:00
github-actions[bot]
ffafc884be chore(ui): refresh fr control ui locale 2026-04-06 02:50:09 +00:00
github-actions[bot]
daedfc9448 chore(ui): refresh ko control ui locale 2026-04-06 02:49:42 +00:00
github-actions[bot]
87bc3b09cb chore(ui): refresh ja-JP control ui locale 2026-04-06 02:49:33 +00:00
github-actions[bot]
3bafc83d74 chore(ui): refresh es control ui locale 2026-04-06 02:49:14 +00:00
Peter Steinberger
71d2eba0a6 test: add windows dev-update smoke lanes 2026-04-06 03:48:47 +01:00
Peter Steinberger
edab013e51 fix: support corepack cmd shim on windows 2026-04-06 03:48:47 +01:00
github-actions[bot]
307e7aee2b chore(ui): refresh de control ui locale 2026-04-06 02:48:39 +00:00
github-actions[bot]
381f233b16 chore(ui): refresh zh-TW control ui locale 2026-04-06 02:48:13 +00:00
github-actions[bot]
423f0fa80c chore(ui): refresh pt-BR control ui locale 2026-04-06 02:48:04 +00:00
github-actions[bot]
78db7a8ab7 chore(ui): refresh zh-CN control ui locale 2026-04-06 02:47:54 +00:00
Gustavo Madeira Santana
73a8dd43bf Matrix: clear undici test override after transport test 2026-04-05 22:46:29 -04:00
Vincent Koc
fb639fa3d5 fix(ci): harden control ui locale refresh rebases 2026-04-06 03:45:34 +01:00
Peter Steinberger
9918667804 perf(test): trim runReplyAgent misc mock imports 2026-04-06 03:43:46 +01:00
Vignesh
fc5f642e77 (chore): delete dream-diary-preview file 2026-04-05 19:41:59 -07:00
Peter Steinberger
813aa3551e fix: restore latest-main ci gates 2026-04-06 03:38:28 +01:00
Peter Steinberger
b1c98e8469 test: stabilize browser and provider ci shards 2026-04-06 03:38:28 +01:00
Ayaan Zaidi
332e7d9d7b style: trim facade fallback comment noise 2026-04-06 08:07:38 +05:30
Peter Steinberger
072e0795f8 chore: prepare 2026.4.6-beta.1 release 2026-04-06 03:33:55 +01:00
Chunyue Wang
1e9289f535 fix: resolve global bundled plugin facade fallback (#61297) (thanks @openperf)
* fix(gateway): resolve globally-installed bundled plugins in facade-runtime

* fix: resolve global bundled plugin facade fallback (#61297) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-06 08:03:18 +05:30
Peter Steinberger
a391e5723a perf(test): trim announce and sessions tool imports 2026-04-06 03:33:02 +01:00
Peter Steinberger
afe24a322b docs: add changelog note for async media delivery flag 2026-04-06 03:29:42 +01:00
Peter Steinberger
d8270ef181 fix: gate async media direct delivery behind config 2026-04-06 03:28:58 +01:00
Peter Steinberger
2cb057fcd9 fix: harden async media completion delivery 2026-04-06 03:28:57 +01:00
Gustavo Madeira Santana
427997f989 Matrix: recover from pinned dispatcher runtime failures (#61595)
Merged via squash.

Prepared head SHA: f9a2d9be7f
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-05 22:26:45 -04:00
Peter Steinberger
134d309571 fix(discord): raise default media cap 2026-04-06 03:22:20 +01:00
Peter Steinberger
c45f1ac8ce perf(agents): isolate subagent announce origin helper 2026-04-06 03:20:31 +01:00
Peter Steinberger
3ee823b229 perf(test): trim send-policy and abort hot paths 2026-04-06 03:10:40 +01:00
Peter Steinberger
7cd813139b fix: deliver async media generation results directly 2026-04-06 03:08:38 +01:00
Vincent Koc
547bd6f7d5 fix(ui): localize more control ui strings 2026-04-06 03:08:17 +01:00
Peter Steinberger
1f951897f6 test: fix reply dispatch mock contract 2026-04-06 03:07:25 +01:00
Peter Steinberger
9924627f49 test(auto-reply): isolate reply abort dispatch seams 2026-04-06 03:06:41 +01:00
Peter Steinberger
124c4c85ab fix(tasks): hide internal completion wake rows 2026-04-06 03:03:53 +01:00
Peter Steinberger
85b3203421 fix(agents): carry async media wake attachments structurally 2026-04-06 03:03:53 +01:00
Peter Steinberger
b0d9d1d2da fix(agents): extend subagent announce timeout 2026-04-06 03:03:53 +01:00
Peter Steinberger
06b154a6df fix: unblock comfy live plugin loading 2026-04-06 03:01:43 +01:00
Gustavo Madeira Santana
9c33b1097c fix(matrix): reuse raw default account key during onboarding promotion 2026-04-05 21:58:18 -04:00
Mariano
b167df78aa Lobster: harden embedded runtime integration (#61566)
Merged via squash.

Prepared head SHA: a6f48309fd
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-04-06 03:52:24 +02:00
Vincent Koc
e8f0f91d29 fix(ui): localize control ui strings 2026-04-06 02:52:02 +01:00
Peter Steinberger
38cb5aefc8 fix(cli): narrow post-update root 2026-04-06 02:50:38 +01:00
Mariano
20cbc11f1a memory: trim generic daily chunk headings (#61597)
* memory: trim generic daily chunk headings

* docs: tag dreaming heading cleanup changelog

* docs: attribute dreaming heading cleanup changelog
2026-04-06 03:47:36 +02:00
Peter Steinberger
0e96c82ce8 test(auto-reply): split ACP and reply-dispatch regressions 2026-04-06 02:45:09 +01:00
Vincent Koc
33b4b76a53 docs(web): clarify control ui language picker 2026-04-06 02:44:24 +01:00
Peter Steinberger
bf269e7b67 test(plugin-sdk): tighten ACP command dispatch guards 2026-04-06 02:43:14 +01:00
Peter Steinberger
7b47d27d0a perf(auto-reply): lazy-load TTS helpers on demand 2026-04-06 02:43:14 +01:00
Peter Steinberger
1ffe02e5ba fix(agents): prefer overflow compaction for fresh reads 2026-04-06 02:41:38 +01:00
Peter Steinberger
979409eab5 fix(qa): harden new scenario suite 2026-04-06 02:41:03 +01:00
Peter Steinberger
80c5df6bdc fix: prune staged feishu sdk types from npm pack 2026-04-06 02:40:46 +01:00
Peter Steinberger
bdf1f02154 fix: exit after package-to-git handoff 2026-04-06 02:39:53 +01:00
Peter Steinberger
c7e13cac71 test: use explicit node entrypoint in macos update smoke 2026-04-06 02:39:53 +01:00
Mariano
c7b7dc335e test: fix current-main prep blockers (#61582)
Merged via squash.

Prepared head SHA: 49f7b121aa
Reviewed-by: @mbelinky
2026-04-06 03:33:47 +02:00
Peter Steinberger
520500f007 docs: update changelog for read visibility fixes 2026-04-06 02:32:03 +01:00
Vincent Koc
4fdcacdb2c fix(agents): preserve latest read output during compaction 2026-04-06 02:25:47 +01:00
Peter Steinberger
92fa7ad42a fix(agents): ignore unsupported music generation hints 2026-04-06 02:22:00 +01:00
Peter Steinberger
9b2b22f350 feat: add vydra media provider 2026-04-06 02:21:51 +01:00
Vincent Koc
7d2dc7a9fb fix(agents): keep large read tool results visible 2026-04-06 02:19:38 +01:00
Peter Steinberger
a2cbeefd5f docs: update unreleased provider notes 2026-04-06 02:18:28 +01:00
Peter Steinberger
dd8525cacd fix(gateway): accept music generation internal events 2026-04-06 02:18:15 +01:00
Peter Steinberger
eba8fed94b fix: stop old cli after package-to-git switch 2026-04-06 02:17:20 +01:00
Mariano
4661bf66c4 memory: chunk daily dreaming ingestion (#61583)
Merged via squash.

Prepared head SHA: 88816a01ef
Co-authored-by: mbelinky <17249097+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-04-06 03:17:10 +02:00
Gustavo Madeira Santana
e02ef0710e Docs: clarify Matrix quiet push rules 2026-04-05 21:15:03 -04:00
Mariano
27d507e596 Gateway: bound websocket shutdown close (#61565)
Merged via squash.

Prepared head SHA: 9040dd5715
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-04-06 03:09:59 +02:00
Peter Steinberger
177ee54f05 refactor: remove comfy music tool shim 2026-04-06 02:03:14 +01:00
Peter Steinberger
a9f491310c fix: route comfy music through shared tool 2026-04-06 02:03:13 +01:00
Peter Steinberger
9dfa4db76b test: harden macos release-to-dev smoke verification 2026-04-06 02:03:04 +01:00
Peter Steinberger
26c9885832 fix: skip stale post-switch update follow-ups 2026-04-06 02:03:04 +01:00
Peter Steinberger
1cce18893f docs: reorder changelog highlights 2026-04-06 02:00:32 +01:00
Peter Steinberger
c5a310bf84 docs: improve music generation docs 2026-04-06 01:59:10 +01:00
Peter Steinberger
f4ffac6fe9 test: speed up dispatch-from-config thread fallback coverage 2026-04-06 01:54:26 +01:00
Vincent Koc
7c9108aaf7 fix(memory-qmd): streamline compatibility coverage 2026-04-06 01:52:01 +01:00
Peter Steinberger
f6dbcf4cda docs: document music generation async flow 2026-04-06 01:49:58 +01:00
Peter Steinberger
3027f0dde5 chore: remove stray finder metadata 2026-04-06 01:47:14 +01:00
Peter Steinberger
dc0ee2e178 feat: add music generation tooling 2026-04-06 01:47:14 +01:00
Peter Steinberger
3de91d9e01 fix: stabilize line and feishu ci shards 2026-04-06 01:46:25 +01:00
Peter Steinberger
aeb9ad52fa feat: add comfy workflow media support 2026-04-06 01:45:01 +01:00
Peter Steinberger
d37b97c2ff refactor(update): extract package manager bootstrap logic 2026-04-06 01:41:59 +01:00
wirjo
0793136c63 feat(bedrock-mantle): add IAM credential auth via @aws/bedrock-token-… (#61563)
* feat(bedrock-mantle): add IAM credential auth via @aws/bedrock-token-generator

Mantle previously required a manually-created API key (AWS_BEARER_TOKEN_BEDROCK).
This adds automatic bearer token generation from IAM credentials using the
official @aws/bedrock-token-generator package.

Auth priority:
1. Explicit AWS_BEARER_TOKEN_BEDROCK env var (manual API key from Console)
2. IAM credentials via getTokenProvider() → Bearer token (instance roles,
   SSO profiles, access keys, EKS IRSA, ECS task roles)

Token is cached in memory (1hr TTL, generated with 2hr validity) and in
process.env.AWS_BEARER_TOKEN_BEDROCK for downstream sync reads.

Falls back gracefully when package is not installed or credentials are
unavailable — Mantle provider simply not registered.

Closes #45152

* fix(bedrock-mantle): harden IAM auth

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-06 01:41:24 +01:00
Peter Steinberger
2985fc0e32 test: add irc runtime api smoke coverage 2026-04-06 01:38:15 +01:00
Vignesh Natarajan
7572f174e3 Dreaming: update multiphase stats and UI polish 2026-04-05 17:38:02 -07:00
Peter Steinberger
3600cecd4b test: seed channel setup contract registry in helper tests 2026-04-06 01:36:09 +01:00
Peter Steinberger
f9a8eb0387 test: speed up image tool auth-heavy coverage 2026-04-06 01:32:31 +01:00
Vincent Koc
098f4eeebb fix(memory-qmd): restore qmd compatibility defaults 2026-04-06 01:31:51 +01:00
Peter Steinberger
ca462fb928 fix(update): bootstrap pnpm for dev preflight 2026-04-06 01:31:27 +01:00
Peter Steinberger
e0354e71eb fix: skip old-process config writes after git switch 2026-04-06 01:29:33 +01:00
Peter Steinberger
6c4e06cd4f test: speed up sanitize session history coverage 2026-04-06 01:27:24 +01:00
Peter Steinberger
0affaf15ac refactor: narrow bundled channel entry surfaces 2026-04-06 01:26:02 +01:00
Peter Steinberger
f42a06b1a4 build: refresh lockfile for control ui deps 2026-04-06 01:25:39 +01:00
Peter Steinberger
7ae1fbec4b test: speed up sanitize session history policy smoke 2026-04-06 01:23:40 +01:00
Peter Steinberger
1f220587b1 test: speed up models config env provider coverage 2026-04-06 01:21:29 +01:00
Peter Steinberger
9b00008561 docs(openai): clarify gpt-5.4 fast mode 2026-04-06 01:20:52 +01:00
wirjo
699b2320a8 feat(memory): add Bedrock embedding provider for memory search (#61547)
* feat(memory): add Bedrock embedding provider for memory search

Add Amazon Bedrock as a native embedding provider for memory search.
Supports Titan Embed Text v1/v2 and Cohere Embed models via AWS SDK.

- New embeddings-bedrock.ts: BedrockRuntimeClient + InvokeModel
- Auth via AWS default credential chain (same as Bedrock inference)
- Auto-selected in 'auto' mode when AWS credentials are detected
- Titan V2: configurable dimensions (256/512/1024), normalization
- Cohere: native batch support with search_query/search_document types
- 16 new tests covering all model types, auth detection, edge cases

Closes #26289

* fix(memory): harden bedrock embedding selection

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-06 01:19:56 +01:00
Peter Steinberger
d945705d42 docs(faq): add gpt-5.4 fast mode entry 2026-04-06 01:19:53 +01:00
Vincent Koc
f4cd1a3782 docs: rewrite video generation docs for readability 2026-04-06 01:19:44 +01:00
Vignesh Natarajan
61e61ccc18 Dreaming: simplify sweep flow and add diary surface 2026-04-05 17:18:54 -07:00
Vignesh Natarajan
02f2a66dff memory-core: checkpoint mode-first dreaming refactor 2026-04-05 17:18:54 -07:00
Peter Steinberger
5a42355d54 refactor(video): share async task status helpers 2026-04-06 01:18:39 +01:00
Peter Steinberger
527215c343 docs: add changelog note for qa lab config fix 2026-04-06 01:18:09 +01:00
Gustavo Madeira Santana
1ee30dc70a docs: note Matrix persisted auth detection 2026-04-05 20:18:03 -04:00
Gustavo Madeira Santana
4031e4b92d matrix: align bundled channel metadata 2026-04-05 20:18:03 -04:00
Peter Steinberger
89c8a1c36a fix: restore qa lab config typing 2026-04-06 01:17:15 +01:00
Peter Steinberger
15f74b89c8 test: speed up openai tool id preservation replay coverage 2026-04-06 01:16:53 +01:00
Peter Steinberger
3a1be5cb93 test: reset guest git root before dev update 2026-04-06 01:16:23 +01:00
Peter Steinberger
20c84a2090 fix(qa): stop embedded control ui reload loop 2026-04-06 01:10:34 +01:00
Peter Steinberger
4cf9d5ff90 fix: restore green checks 2026-04-06 01:10:16 +01:00
Peter Steinberger
3e6a7b3169 test: trim slow agent web and lifecycle coverage 2026-04-06 01:07:16 +01:00
Peter Steinberger
fdc2f421e4 fix: restore pnpm check type safety 2026-04-06 01:04:34 +01:00
Peter Steinberger
a79984eacf feat(qa): improve qa lab debugger ui 2026-04-06 01:03:21 +01:00
Peter Steinberger
508024ae3b feat(qa): add live suite runner and harness 2026-04-06 01:03:21 +01:00
Peter Steinberger
4bb965e007 docs(providers): surface new video provider pages 2026-04-06 01:02:59 +01:00
Peter Steinberger
e5cfdf437f fix(video): guard active async generation tasks 2026-04-06 01:02:59 +01:00
Peter Steinberger
6cdf5a43f2 refactor: add metadata-first channel configured-state probes 2026-04-06 01:02:45 +01:00
Peter Steinberger
ad6c584ce7 fix: ignore unsupported video generation overrides 2026-04-06 01:02:10 +01:00
Peter Steinberger
c4cc557604 fix: clarify dirty dev update error 2026-04-06 00:58:19 +01:00
Peter Steinberger
379bc1c032 docs(video): document runway support 2026-04-06 00:50:32 +01:00
Peter Steinberger
f92ac83d88 feat(video): add runway provider 2026-04-06 00:50:32 +01:00
Peter Steinberger
3fcff952ba feat(agents): detach video generation completion 2026-04-06 00:50:32 +01:00
Vincent Koc
9fba0c6ac7 fix(openai): avoid em dashes in gpt-5 overlay (#61560) 2026-04-06 00:49:12 +01:00
Peter Steinberger
2693ae7ec3 test: optimize macos release-to-dev smoke lane 2026-04-06 00:46:56 +01:00
Peter Steinberger
be16cf2f0d fix: defer plugin sync after git switch 2026-04-06 00:46:56 +01:00
Peter Steinberger
3bc17fc823 test: speed up nodes camera coverage 2026-04-06 00:45:22 +01:00
Vincent Koc
0e85343b6c docs: update Lobster in-process mode and REM preview tooling 2026-04-06 00:40:21 +01:00
Peter Steinberger
94d3153817 test: split inline provider model coverage 2026-04-06 00:37:51 +01:00
Mariano
30dc24fbd8 Lobster: add managed TaskFlow mode (#61555) 2026-04-06 01:37:26 +02:00
Mariano
7f97fa6ed5 Lobster: run workflows in process (#61523)
* Lobster: run workflows in process

* docs: note in-process lobster runtime

* docs: add lobster changelog attribution
2026-04-06 01:30:47 +02:00
Peter Steinberger
989ea3e6df test(live): prefer google models over big-pickle 2026-04-06 00:28:38 +01:00
Peter Steinberger
85d41cd254 docs: document channel persisted auth metadata 2026-04-06 00:24:19 +01:00
Peter Steinberger
b0009ac340 fix: ignore unsupported image generation overrides 2026-04-06 00:17:32 +01:00
Peter Steinberger
1e90dd4258 test: fold xai extra params coverage into hot lane 2026-04-06 00:16:24 +01:00
Peter Steinberger
8cb85ff85f refactor: harden plugin metadata and bundled channel entry seams 2026-04-06 00:15:38 +01:00
Gustavo Madeira Santana
95079949c3 fix(discord): short-circuit bound thread self-loop drops 2026-04-05 19:13:02 -04:00
Peter Steinberger
40c499d489 feat(agents): track video generation tasks 2026-04-06 00:12:47 +01:00
Peter Steinberger
6d34a1c814 fix(video): queue fal provider jobs 2026-04-06 00:12:47 +01:00
Peter Steinberger
56b91e0cb2 docs: add discord native command changelog note 2026-04-06 00:12:09 +01:00
Peter Steinberger
510344a687 refactor: dedupe discord native command auth 2026-04-06 00:07:33 +01:00
Peter Steinberger
a35ac86c84 test: drop redundant openai extra params coverage 2026-04-06 00:05:46 +01:00
Gustavo Madeira Santana
5071f7cb3b test(config): fix markdown table mock typing 2026-04-05 19:04:55 -04:00
Gustavo Madeira Santana
62583e2235 fix(google): restore forward-compat provider hooks 2026-04-05 19:04:55 -04:00
Peter Steinberger
b5ade7b629 fix: surface normalized video durations 2026-04-05 23:57:48 +01:00
Peter Steinberger
09fe144e52 test: isolate gateway tool coverage 2026-04-05 23:57:32 +01:00
Peter Steinberger
ec2f0edd45 test: stabilize subagent persistence registry coverage 2026-04-05 23:57:31 +01:00
Peter Steinberger
579c50dd60 test: isolate openclaw plugin context coverage 2026-04-05 23:57:31 +01:00
Peter Steinberger
d13821f1c6 docs: add tahoe release-to-dev smoke lane 2026-04-05 23:53:52 +01:00
Peter Steinberger
47ccc3d9bb fix: bootstrap pnpm for git updates 2026-04-05 23:53:52 +01:00
Peter Steinberger
3528d0620e fix: honor discord allowlisted channels for native commands 2026-04-05 23:51:30 +01:00
Peter Steinberger
aa7c67e6a9 fix: harden video provider transports 2026-04-05 23:47:10 +01:00
Peter Steinberger
fdf381f1a7 fix: normalize video provider durations 2026-04-05 23:47:10 +01:00
Gustavo Madeira Santana
5cff2ff94b style(tests): normalize registry mock wrapping 2026-04-05 18:46:51 -04:00
Gustavo Madeira Santana
ac66507ccb test(config): align markdown tables with active registry 2026-04-05 18:46:51 -04:00
Gustavo Madeira Santana
3dec7f2596 test(contracts): drop removed claude cli auth export 2026-04-05 18:46:51 -04:00
Gustavo Madeira Santana
83f47a4d0a fix(google): restore gemini cli provider contract 2026-04-05 18:46:51 -04:00
Vincent Koc
a9dbaa1124 fix(memory): standardize DREAMS trail path 2026-04-05 23:35:44 +01:00
Vincent Koc
367f52f483 docs(memory): point dreaming trail docs to dreams.md 2026-04-05 23:35:44 +01:00
Vincent Koc
b371af76a3 fix(memory-core): preserve dated DREAMS trail 2026-04-05 23:35:44 +01:00
Peter Steinberger
3584d28141 refactor: harden plugin metadata and browser sdk seams 2026-04-05 23:35:02 +01:00
Peter Steinberger
c1b1d14218 test: fix abort cascade and workspace edit inputs 2026-04-05 23:33:23 +01:00
Mariano
79348f73c8 feat(memory-core): add REM preview and safe promotion replay (#61540)
* memory: add REM preview and safe promotion replay thanks @mbelinky

* changelog: note REM preview and promotion replay

---------

Co-authored-by: Vignesh <mailvgnsh@gmail.com>
2026-04-05 15:32:38 -07:00
Peter Steinberger
cef64f0b5a fix: prevent duplicate gateway watchers 2026-04-05 23:24:27 +01:00
Gustavo Madeira Santana
e91405ebf9 test(matrix): isolate migration snapshot seam 2026-04-05 18:24:09 -04:00
Gustavo Madeira Santana
bfa1fa1700 fix(matrix): restore cli metadata registrar 2026-04-05 18:24:09 -04:00
Gustavo Madeira Santana
54ad458267 fix(matrix): honor canonical private-network opt-in 2026-04-05 18:24:09 -04:00
Peter Steinberger
c6d3ee70e2 docs(providers): unify qwen docs 2026-04-05 23:23:58 +01:00
Gustavo Madeira Santana
8a841b531f fix(matrix): split partial and quiet preview streaming (#61450)
Merged via squash.

Prepared head SHA: 6a0d7d1348
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-05 18:23:07 -04:00
Peter Steinberger
1582bbbfc5 fix(qa): stabilize hermetic suite runtime 2026-04-05 23:21:56 +01:00
Peter Steinberger
4780788bbb feat(qa): add repo-backed qa suite runner 2026-04-05 23:21:56 +01:00
Peter Steinberger
eb6d0ce2c2 fix(qa): stabilize docker gateway bootstrap 2026-04-05 23:21:56 +01:00
Peter Steinberger
b5fc435bd5 fix(qa): restore embedded control ui gateway startup 2026-04-05 23:21:56 +01:00
Peter Steinberger
8e1c81e707 feat(qa): recreate qa lab docker stack 2026-04-05 23:21:56 +01:00
Peter Steinberger
17a324b0de chore: polish qa lab follow-ups 2026-04-05 23:21:56 +01:00
Peter Steinberger
bb60b53124 feat: add qa lab extension 2026-04-05 23:21:56 +01:00
Peter Steinberger
d7f75ee087 refactor: hide qa channels with exposure metadata 2026-04-05 23:21:56 +01:00
Peter Steinberger
b58f9c5258 feat: add qa channel foundation 2026-04-05 23:21:56 +01:00
Peter Steinberger
a234157337 docs(providers): link generation guides 2026-04-05 23:21:14 +01:00
Peter Steinberger
f30c087fdf docs(providers): add generation setup pages 2026-04-05 23:21:14 +01:00
Vincent Koc
1a3eb38aaf fix(ci): stabilize ui i18n and gateway watch checks 2026-04-05 23:20:17 +01:00
Dave Morin
2ed2dbba00 Memory: move dreaming trail to dreams.md (#61537)
* Memory: move dreaming trail to dreams.md

* docs(changelog): add dreams.md entry

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 23:19:31 +01:00
Peter Steinberger
48611ec40a test: speed up provider policy and auth suites 2026-04-05 23:14:39 +01:00
Peter Steinberger
471d056e2f refactor: move browser runtime seams behind plugin metadata 2026-04-05 23:13:14 +01:00
Peter Steinberger
1351bacaa4 docs(security): clarify localhost shared-auth trust model 2026-04-05 23:12:52 +01:00
Peter Steinberger
f7e76e31f3 fix(build): correct node require typing 2026-04-05 23:11:46 +01:00
Peter Steinberger
1703bdcaf6 Revert "fix(gateway): bound silent local pairing scopes"
This reverts commit 7f1b159c03.
2026-04-05 23:09:58 +01:00
Peter Steinberger
a62193c09e feat(video): add xai and alibaba providers 2026-04-05 23:07:04 +01:00
Peter Steinberger
5e0b58fbc6 docs: refine unreleased changelog 2026-04-05 23:05:10 +01:00
Peter Steinberger
4ed60d950d test: isolate agent runtime seams 2026-04-05 23:02:30 +01:00
Peter Steinberger
05f9dd7a01 fix: clean rebase leftovers 2026-04-05 22:58:29 +01:00
Peter Steinberger
d6d8d1716f fix: resolve repo check drift 2026-04-05 22:58:29 +01:00
Peter Steinberger
7f1b159c03 fix(gateway): bound silent local pairing scopes 2026-04-05 22:56:40 +01:00
Tyler Yust
6a57f5403d fix: prevent duplicate block reply delivery for text_end channels (#61530) 2026-04-05 14:53:48 -07:00
Peter Steinberger
53c52124b9 style: format remaining local edits 2026-04-05 22:50:46 +01:00
Vincent Koc
0655e173c4 fix(ci): narrow control ui locale refresh push runs 2026-04-05 22:48:25 +01:00
Peter Steinberger
dea3ab0aa9 fix: align models status provider auth reporting 2026-04-05 22:46:14 +01:00
Vincent Koc
94256ea1a0 revert(memory-wiki): back out llm wiki stack 2026-04-05 22:44:20 +01:00
Gustavo Madeira Santana
e29d370969 Gateway: keep outbound session metadata in owner store 2026-04-05 17:42:14 -04:00
Peter Steinberger
06f9677b5b fix(sandbox): harden EXDEV rename fallback 2026-04-05 22:40:35 +01:00
Peter Steinberger
beed40e918 test: isolate exec approval suite from bundled plugins 2026-04-05 22:40:24 +01:00
Vincent Koc
c73aeed929 feat(memory-wiki): generate dashboard report pages 2026-04-05 22:36:31 +01:00
Vincent Koc
a4a1cfc8c2 docs(memory-wiki): document shared recall and backlinks 2026-04-05 22:34:02 +01:00
Vincent Koc
39b05c4920 docs(memory-wiki): prefer shared corpus recall guidance 2026-04-05 22:34:02 +01:00
Vincent Koc
08492dfeee feat(memory-wiki): compile related backlinks blocks 2026-04-05 22:34:02 +01:00
Vincent Koc
2f72363984 feat(memory-core): bridge wiki corpus into memory tools 2026-04-05 22:34:02 +01:00
Vincent Koc
64f889cd4b feat(memory-wiki): allow per-call search corpus overrides 2026-04-05 22:34:02 +01:00
Vincent Koc
a2a9fa7f6f feat(memory-wiki): lint imported provenance gaps 2026-04-05 22:34:01 +01:00
Vincent Koc
cd564bf5a5 feat(memory-wiki): surface imported source provenance 2026-04-05 22:34:01 +01:00
Vincent Koc
c11e7a7420 feat(memory-wiki): add prompt supplement integration 2026-04-05 22:34:01 +01:00
Vincent Koc
00372508b5 feat(memory-wiki): add shared memory search bridge 2026-04-05 22:34:01 +01:00
Vincent Koc
ca94f02959 feat(memory-wiki): add import gateway methods 2026-04-05 22:34:01 +01:00
Vincent Koc
a2376462e9 docs(memory-wiki): add plugin readme 2026-04-05 22:34:01 +01:00
Vincent Koc
d66960206b feat(memory-wiki): extend gateway wiki controls 2026-04-05 22:34:01 +01:00
Vincent Koc
c2a8aac282 feat(memory-wiki): add gateway control methods 2026-04-05 22:34:01 +01:00
Vincent Koc
5a6d80da7f feat(memory-wiki): add wiki doctor diagnostics 2026-04-05 22:34:01 +01:00
Vincent Koc
afb89b439a feat(memory-wiki): add wiki apply cli commands 2026-04-05 22:34:01 +01:00
Vincent Koc
d624ec3a0b feat(memory-wiki): add wiki apply mutation tool 2026-04-05 22:34:01 +01:00
Vincent Koc
9ce4abfe55 feat(memory-wiki): add agent lint tool and issue categories 2026-04-05 22:34:01 +01:00
Vincent Koc
a213a580d5 feat(memory-wiki): auto-refresh indexes after imported sync 2026-04-05 22:34:01 +01:00
Vincent Koc
a78c4de737 feat(memory-wiki): make imported source sync incremental 2026-04-05 22:34:01 +01:00
Vincent Koc
7b62fcd87d feat(memory-wiki): add unsafe-local source sync 2026-04-05 22:34:01 +01:00
Vincent Koc
d1c7d9af80 feat(memory-sdk): add memory event journal bridge 2026-04-05 22:34:01 +01:00
Vincent Koc
fbbe2a1675 feat(memory-wiki): add bridge sync and obsidian cli adapter 2026-04-05 22:34:01 +01:00
Vincent Koc
82710f2add feat(memory-wiki): add wiki search and get surfaces 2026-04-05 22:34:01 +01:00
Vincent Koc
516a43f9f2 feat(memory-wiki): add ingest compile lint pipeline 2026-04-05 22:34:01 +01:00
Vincent Koc
57d1685a65 feat(memory-wiki): scaffold wiki vault plugin 2026-04-05 22:34:01 +01:00
Vincent Koc
b0c7bac9ce refactor(plugin-sdk): add memory host aliases 2026-04-05 22:34:01 +01:00
Vincent Koc
e7407f8178 test(signal): initialize mention helper for standalone suite 2026-04-05 22:34:01 +01:00
Vincent Koc
1033db4d31 fix(whatsapp): avoid setup barrel import cycle 2026-04-05 22:34:01 +01:00
Peter Steinberger
3a7a67b218 test: split memory flush tool context seam 2026-04-05 22:33:08 +01:00
Peter Steinberger
2176b68e50 fix: batch docker config writes 2026-04-05 22:31:11 +01:00
Peter Steinberger
b4e5d91941 test: inject web fetch dns lookup seams 2026-04-05 22:29:02 +01:00
Peter Steinberger
5586b3fd19 fix(agents): cap live tool result truncation 2026-04-05 22:28:53 +01:00
Peter Steinberger
d7f3af3b06 test: isolate bundled plugin env in exec approval tests 2026-04-05 22:25:14 +01:00
Peter Steinberger
d83dd9b536 test: split embedded runner cleanup seams 2026-04-05 22:20:02 +01:00
Peter Steinberger
d3e67a0de7 test: fix auth profile fallback regressions 2026-04-05 22:11:09 +01:00
Peter Steinberger
932194b7d5 feat(video): add provider support and discord fallback 2026-04-05 22:06:56 +01:00
Peter Steinberger
52146f8803 fix(gateway): watch nested source directories 2026-04-05 22:06:43 +01:00
Peter Steinberger
aa464f8573 test: decouple web fetch fallbacks from provider startup 2026-04-05 22:02:05 +01:00
Peter Steinberger
8279375bdf perf: avoid heavy ACP provider checks 2026-04-05 22:02:05 +01:00
Peter Steinberger
58f95b8000 fix: stabilize docker live and docker e2e harnesses 2026-04-05 22:00:56 +01:00
Peter Steinberger
8a43223014 fix(agents): preserve tool output during context guarding 2026-04-05 21:52:36 +01:00
Peter Steinberger
9b7002ee59 refactor(reply): type reply threading policy 2026-04-05 21:40:56 +01:00
Peter Steinberger
456ad889c7 docs: reorder unreleased changelog entries 2026-04-05 21:40:14 +01:00
Peter Steinberger
ce8492f9a0 chore: bump version to 2026.4.5 2026-04-05 21:33:04 +01:00
Peter Steinberger
a8e827856a refactor: split bundled channel config metadata 2026-04-05 21:24:02 +01:00
Peter Steinberger
9bc43b61bf refactor: share assistant phase helpers 2026-04-06 05:23:54 +09:00
Peter Steinberger
2a4eea58a9 fix: suppress commentary text in completed ws replies 2026-04-06 05:23:54 +09:00
Peter Steinberger
a4f16f572c fix: prefer final-answer text in web chat previews 2026-04-06 05:23:54 +09:00
Peter Steinberger
e92f55302b test(agents): split subagent persistence restart case 2026-04-05 21:18:58 +01:00
Peter Steinberger
8a987030c0 fix(types): repair post-rebase check drift 2026-04-05 21:15:55 +01:00
Peter Steinberger
6b627d4707 fix(discord): add batched reply mode 2026-04-05 21:15:29 +01:00
Peter Steinberger
8206328a94 refactor: tighten final boundary guardrails 2026-04-05 21:14:52 +01:00
Peter Steinberger
714ba48a6f perf(plugins): resolve setup providers lazily 2026-04-05 21:08:52 +01:00
Peter Steinberger
36080283e4 refactor: remove remaining contract path leaks 2026-04-05 20:59:56 +01:00
Peter Steinberger
97e1437803 fix: clarify exec node routing guidance 2026-04-05 20:55:04 +01:00
Peter Steinberger
466e17436d fix: restore agent verbose default typing 2026-04-05 20:48:40 +01:00
Peter Steinberger
d8abb287eb fix: align Windows onboard smoke timeout budget 2026-04-05 20:48:40 +01:00
Peter Steinberger
791c083c0a fix: extend Windows daemon onboarding health budget 2026-04-05 20:48:40 +01:00
Peter Steinberger
8806ef804e refactor: remove remaining channel and gateway boundary leaks 2026-04-05 20:48:10 +01:00
Peter Steinberger
5cadf069e9 test(image-tool): avoid pi-tools module reset churn 2026-04-05 20:39:36 +01:00
Peter Steinberger
ce1cd26fbc refactor: align agent tool params with upstream pi 2026-04-05 20:36:47 +01:00
Peter Steinberger
181a50e146 refactor: remove bundled channel discovery leaks 2026-04-05 20:36:24 +01:00
Peter Steinberger
604e16c765 fix(whatsapp): avoid setup api self-import cycle 2026-04-05 20:34:08 +01:00
Peter Steinberger
4da7b453b1 test(models-config): move merge coverage to fast helper seams 2026-04-05 20:34:08 +01:00
Peter Steinberger
6424b08772 fix: restore green contract and provider gates 2026-04-05 20:17:59 +01:00
Gustavo Madeira Santana
dcd0cf9f98 fix(matrix): align DM room session routing (#61373)
Merged via squash.

Prepared head SHA: 9529d2e161
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-05 15:15:46 -04:00
Peter Steinberger
55192e2d51 refactor: quarantine bundled plugin inventory 2026-04-05 20:11:22 +01:00
Peter Steinberger
a9125ec0b0 refactor: share OpenAI tool schema normalization 2026-04-05 20:05:05 +01:00
Peter Steinberger
31016c5ed9 refactor: derive plugin contracts from manifests 2026-04-05 20:03:00 +01:00
Gustavo Madeira Santana
cac40c01e9 fix(matrix): move avatar setup into account config (#61437)
Merged via squash.

Prepared head SHA: 4dd887a474
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-05 14:57:44 -04:00
Peter Steinberger
bcc0e3de2e refactor: remove core test extension leaks 2026-04-05 19:54:57 +01:00
Peter Steinberger
8cd9007ec1 fix: harden OpenAI strict tool fallback 2026-04-05 19:53:29 +01:00
Peter Steinberger
2ff29a33d0 refactor: split doctor runtime migrations and talk runtime tests 2026-04-05 19:44:34 +01:00
Gustavo Madeira Santana
0ef9383487 fix(approvals): make exec approval fallback guidance channel-specific (#61424)
Merged via squash.

Prepared head SHA: cb5d3c249c
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-05 14:26:46 -04:00
Peter Steinberger
84e76f7cce refactor(cli): remove stale cli provider leftovers 2026-04-05 19:11:34 +01:00
Mariano Belinky
b664541158 reply: make progress updates respect verbose 2026-04-05 20:08:15 +02:00
Peter Steinberger
1a47675e6c fix: restore check after CLI seam cleanup 2026-04-05 19:06:34 +01:00
Peter Steinberger
a01c4c3a0e test: split provider-shaped core test coverage 2026-04-05 19:05:44 +01:00
Peter Steinberger
318111286f fix: preserve strict final-answer delivery phases (#59643) (thanks @ringlochid) 2026-04-06 03:03:18 +09:00
Peter Steinberger
98ce1c2902 fix: hide commentary partial leaks until final answer (#59643) (thanks @ringlochid) 2026-04-06 03:03:18 +09:00
Leo Zhang
7bef5a7466 Fix commentary/final answer phase separation 2026-04-06 03:03:18 +09:00
Peter Steinberger
b8e2e5c251 test: genericize talk provider fixtures 2026-04-05 18:52:18 +01:00
Peter Steinberger
c71ee4d844 refactor: split doctor and gateway test helpers 2026-04-05 18:52:18 +01:00
Peter Steinberger
267ebc3ba5 fix: remove em dashes from prompt text 2026-04-05 18:51:33 +01:00
Peter Steinberger
dcfc1f16ed test: split ACP attachment resolution from dispatch flow 2026-04-05 18:51:13 +01:00
Peter Steinberger
b43d73b633 fix: persist generated reply media before delivery 2026-04-05 18:47:06 +01:00
Peter Steinberger
05d351c430 refactor(cli): remove bundled cli text providers 2026-04-05 18:46:36 +01:00
Peter Steinberger
79d6713d81 fix(changelog): remove merge artifact 2026-04-05 18:44:32 +01:00
Peter Steinberger
5790435975 feat(agents): add video_generate tool 2026-04-05 18:44:06 +01:00
Peter Steinberger
b5e87be7f0 ci(docs): retry publish sync pushes 2026-04-05 18:42:24 +01:00
Vincent Koc
a1c1598742 docs: rewrite dreaming docs for 3-phase architecture 2026-04-05 18:42:06 +01:00
Peter Steinberger
7fe5dc36f0 test: remove extension-shaped talk and cli test fixtures 2026-04-05 18:41:57 +01:00
Peter Steinberger
14dbcd0451 test: align vllm provider fixture with discovered models 2026-04-05 18:39:26 +01:00
github-actions[bot]
24d213e4bc chore(ui): refresh pl control ui locale 2026-04-05 17:38:43 +00:00
github-actions[bot]
33c0627c64 chore(ui): refresh id control ui locale 2026-04-05 17:38:40 +00:00
github-actions[bot]
4461432e31 chore(ui): refresh tr control ui locale 2026-04-05 17:38:32 +00:00
github-actions[bot]
4d57b69163 chore(ui): refresh ko control ui locale 2026-04-05 17:38:12 +00:00
github-actions[bot]
c02f72be37 chore(ui): refresh fr control ui locale 2026-04-05 17:38:09 +00:00
github-actions[bot]
387f47d19b chore(ui): refresh ja-JP control ui locale 2026-04-05 17:38:02 +00:00
github-actions[bot]
a68935e497 chore(ui): refresh es control ui locale 2026-04-05 17:37:55 +00:00
github-actions[bot]
b7e86f4d0d chore(ui): refresh zh-CN control ui locale 2026-04-05 17:37:31 +00:00
github-actions[bot]
595adfc2f6 chore(ui): refresh de control ui locale 2026-04-05 17:37:28 +00:00
github-actions[bot]
8ed5d0cf1e chore(ui): refresh zh-TW control ui locale 2026-04-05 17:37:24 +00:00
github-actions[bot]
881a343f34 chore(ui): refresh pt-BR control ui locale 2026-04-05 17:37:21 +00:00
Vincent Koc
cdb9f37989 chore(ui): rename dreaming locale labels 2026-04-05 18:35:54 +01:00
Vincent Koc
bb440b328f fix(memory-core): add dreaming rename artifacts 2026-04-05 18:35:54 +01:00
Vincent Koc
8ff41a6bc4 refactor(memory-core): rename sleep surface back to dreaming 2026-04-05 18:35:54 +01:00
Vincent Koc
848cc5e0ce refactor(memory-core): remove legacy dreaming host helpers 2026-04-05 18:35:54 +01:00
Vincent Koc
550872777e feat(memory-core): introduce sleep phases 2026-04-05 18:35:54 +01:00
Peter Steinberger
db0db3abdb test: make talk gateway fixtures provider agnostic 2026-04-05 18:34:14 +01:00
github-actions[bot]
962650f879 chore(ui): refresh id control ui locale 2026-04-05 17:34:06 +00:00
github-actions[bot]
5834ee54e8 chore(ui): refresh pl control ui locale 2026-04-05 17:34:03 +00:00
github-actions[bot]
80b1fb034a chore(ui): refresh tr control ui locale 2026-04-05 17:33:57 +00:00
github-actions[bot]
3a3c52c357 chore(ui): refresh es control ui locale 2026-04-05 17:33:10 +00:00
github-actions[bot]
85ce5022cc chore(ui): refresh fr control ui locale 2026-04-05 17:33:06 +00:00
github-actions[bot]
d943516838 chore(ui): refresh ja-JP control ui locale 2026-04-05 17:33:05 +00:00
github-actions[bot]
ee4695499a chore(ui): refresh ko control ui locale 2026-04-05 17:33:00 +00:00
Bob
3f6840230b fix: unify reply lifecycle across stop, rotation, and restart (#61267) (thanks @dutifulbob) 2026-04-05 19:32:27 +02:00
github-actions[bot]
bb494ea3ed chore(ui): refresh zh-TW control ui locale 2026-04-05 17:32:13 +00:00
github-actions[bot]
7d8190b588 chore(ui): refresh pt-BR control ui locale 2026-04-05 17:32:10 +00:00
github-actions[bot]
5127468494 chore(ui): refresh zh-CN control ui locale 2026-04-05 17:32:07 +00:00
github-actions[bot]
038bfd9652 chore(ui): refresh de control ui locale 2026-04-05 17:32:01 +00:00
Peter Steinberger
ffc1f7b337 feat(i18n): add Ukrainian docs and control UI locale 2026-04-05 18:31:02 +01:00
Peter Steinberger
c6bf955b0c fix(check): restore green pnpm check 2026-04-05 18:28:48 +01:00
Peter Steinberger
24d5494dbf fix: restore memory embedding provider runtime export 2026-04-05 18:25:37 +01:00
Peter Steinberger
5ad27fa25f fix: allow slower Windows gateway restart health 2026-04-05 18:21:47 +01:00
Peter Steinberger
6008ed6c24 fix: avoid memory embedding provider recursion 2026-04-05 18:21:46 +01:00
Peter Steinberger
3bf92944b2 fix: skip agent context eager warmup on import 2026-04-05 18:20:34 +01:00
Peter Steinberger
3126809cb0 refactor: clean bundled channel bootstrap boundaries 2026-04-05 18:18:59 +01:00
Peter Steinberger
cb76e5c899 fix(gateway): restart watch after child sigterm 2026-04-05 18:18:46 +01:00
github-actions[bot]
1a65c3b06d chore(ui): refresh pl control ui locale 2026-04-05 17:17:49 +00:00
github-actions[bot]
6642d4a341 chore(ui): refresh tr control ui locale 2026-04-05 17:16:43 +00:00
github-actions[bot]
218182aaca chore(ui): refresh id control ui locale 2026-04-05 17:16:31 +00:00
github-actions[bot]
4df05cae48 chore(ui): refresh fr control ui locale 2026-04-05 17:16:10 +00:00
Peter Steinberger
415a7efe8d test(exec): stabilize approval id suite 2026-04-05 18:15:58 +01:00
github-actions[bot]
57620654d1 chore(ui): refresh ko control ui locale 2026-04-05 17:15:21 +00:00
github-actions[bot]
c79306ba89 chore(ui): refresh ja-JP control ui locale 2026-04-05 17:14:14 +00:00
github-actions[bot]
6a43205299 chore(ui): refresh de control ui locale 2026-04-05 17:13:08 +00:00
github-actions[bot]
5ba562d147 chore(ui): refresh es control ui locale 2026-04-05 17:13:07 +00:00
github-actions[bot]
163c6f5e35 chore(ui): refresh pt-BR control ui locale 2026-04-05 17:12:16 +00:00
Peter Steinberger
5ed02c1097 fix: restore foundry model input repair 2026-04-05 18:12:10 +01:00
github-actions[bot]
eae0ac333d chore(ui): refresh zh-TW control ui locale 2026-04-05 17:11:55 +00:00
github-actions[bot]
ba69205f6e chore(ui): refresh zh-CN control ui locale 2026-04-05 17:11:08 +00:00
Vincent Koc
33ff535614 fix(ci): parallelize control ui locale refresh 2026-04-05 18:09:42 +01:00
Gustavo Madeira Santana
8d3e557fc4 Plugins: suppress trust warning noise in snapshot loads (#61427)
Merged via squash.

Prepared head SHA: a3f484bebd
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-05 13:08:43 -04:00
Peter Steinberger
fe93f29486 docs(anthropic): clarify api key and doctor recovery 2026-04-05 18:05:12 +01:00
Peter Steinberger
2d7157b424 refactor(cli): delete removed backend files 2026-04-05 18:04:48 +01:00
Peter Steinberger
6243806f7b refactor(cli): remove custom cli backends 2026-04-05 18:04:48 +01:00
Peter Steinberger
8f9b1ad48d fix: remove assistant replay canonicalization repair 2026-04-05 18:03:48 +01:00
Peter Steinberger
a74fb94fa3 fix(exec): remove host obfuscation gating 2026-04-05 18:01:41 +01:00
Peter Steinberger
adbcfbe2bb perf: skip acp runtime work for no-media and no-command turns 2026-04-05 17:58:38 +01:00
Gustavo Madeira Santana
2ce38dfc31 scripts: expose PR URL in review workflow output 2026-04-05 12:56:27 -04:00
Vincent Koc
7a14967f8e fix(ci): skip repo-wide hooks for locale refresh commits 2026-04-05 17:53:12 +01:00
Peter Steinberger
043d9d370f test: stabilize acp dispatch and dreaming typings 2026-04-05 17:52:15 +01:00
Vincent Koc
18d6d5b629 docs(changelog): resolve conflict markers and deduplicate 2026-04-05 17:49:19 +01:00
Peter Steinberger
846d2734e7 test: tighten provider catalog fixture types 2026-04-05 17:33:01 +01:00
Peter Steinberger
9b89fa3937 fix(agents): repair discord image generation delivery 2026-04-05 17:30:14 +01:00
Peter Steinberger
aee1f0b453 test: fix after-tool-call event mock 2026-04-05 17:27:29 +01:00
Vincent Koc
c3fd7fbbe7 fix(acpx): repair sdk dependency lockfile 2026-04-05 17:20:46 +01:00
Peter Steinberger
198083cde3 refactor: split doctor legacy normalizers and test ownership 2026-04-05 17:17:16 +01:00
Peter Steinberger
15aed55470 refactor: split provider config policy hooks 2026-04-05 17:17:16 +01:00
Peter Steinberger
acd78e0c2f refactor: split browser sdk seams 2026-04-05 17:17:16 +01:00
Nimrod Gutman
c3d8a6d270 docs(ios): document testflight release recovery 2026-04-05 19:09:25 +03:00
Gustavo Madeira Santana
dfae62616f Matrix: keep approval reaction hint anchored 2026-04-05 12:07:43 -04:00
Peter Steinberger
17521116db fix(dev): forward run-node wrapper signals 2026-04-05 17:05:20 +01:00
Peter Steinberger
9e8151f347 refactor: route models-config planning through provider seam 2026-04-05 17:04:02 +01:00
Peter Steinberger
de0d6efc6e test: reduce models-config temp-home churn 2026-04-05 17:04:02 +01:00
Peter Steinberger
eced1fa905 docs: refresh unreleased changelog 2026-04-05 16:56:42 +01:00
Peter Steinberger
7075da59bd feat: allow occasional emoji in friendly openai overlay 2026-04-05 16:56:25 +01:00
Nimrod Gutman
0047048179 fix(memory): avoid recursive provider discovery during register (#61402)
* fix(memory): avoid recursive provider discovery during register

* test(memory): remove resetModules from provider adapter regression

* fix: avoid recursive provider discovery during register (#61402) (thanks @ngutman)
2026-04-05 18:55:58 +03:00
Peter Steinberger
b169b2c977 refactor: move legacy config migrations under doctor 2026-04-05 16:55:10 +01:00
Peter Steinberger
2ade009901 refactor: remove provider-specific sdk shims from core 2026-04-05 16:55:10 +01:00
Peter Steinberger
a6d0ab1482 fix: swallow expired discord slash interactions 2026-04-05 16:50:11 +01:00
Peter Steinberger
df38bc2271 style(repo): normalize imports and formatting 2026-04-05 16:49:46 +01:00
Peter Steinberger
8405d86a8b test: speed up ollama provider discovery coverage 2026-04-05 16:38:40 +01:00
Engr. Arif Ahmed Joy
63fcc52520 fix: windows self-restart stale gateway cleanup (#60480) (thanks @arifahmedjoy)
* fix: implement Windows stale gateway process cleanup before restart

findGatewayPidsOnPortSync() returned [] immediately on Windows, causing
cleanStaleGatewayProcessesSync() to skip killing old gateway processes
during self-restart (triggerOpenClawRestart -> schtasks path). This led
to an infinite retry loop: 'gateway already running under schtasks;
waiting 5000ms before retrying startup'.

Changes:
- Extract Windows port/process helpers into shared windows-port-pids.ts
  to break the circular import between restart-stale-pids.ts and
  gateway-processes.ts, with configurable timeoutMs for poll compliance
- findGatewayPidsOnPortSync: discover + verify Windows gateway PIDs via
  readWindowsListeningPidsOnPortSync + readWindowsProcessArgsSync
- pollPortOnceWindows: use short POLL_SPAWN_TIMEOUT_MS (400ms) so a
  single slow PowerShell call cannot exceed the 2s polling budget
- terminateStaleProcessesSync: add terminateStaleProcessesWindows using
  taskkill.exe (graceful /T first, then /F force-kill)

Fixes the Windows gateway restart infinite loop caused by the schtasks
supervisor detecting a port conflict it cannot resolve.

* fix: tighten windows stale gateway cleanup

* fix: preserve windows restart probe failures

* refactor: unify windows gateway pid verification

* fix: preserve windows argv probe failures

* fix: windows self-restart stale gateway cleanup (#60480) (thanks @arifahmedjoy)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-05 21:01:17 +05:30
Peter Steinberger
ff6fd18629 test: speed up minimax auth provenance fixtures 2026-04-05 16:22:32 +01:00
Peter Steinberger
a32a3e2331 fix(discord): honor explicit reply tags in delivery 2026-04-05 16:20:15 +01:00
Peter Steinberger
d25609bc06 fix: default OpenAI personality overlay to friendly 2026-04-05 16:15:08 +01:00
Peter Steinberger
7e4c5294ae test: speed up stepfun and minimax provider fixtures 2026-04-05 16:14:59 +01:00
Peter Steinberger
37b3acad34 test: update legacy config doctor expectations 2026-04-05 16:12:45 +01:00
Peter Steinberger
97878b853a refactor: move legacy config migration behind doctor 2026-04-05 16:12:45 +01:00
Peter Steinberger
7a3443e9ac docs(changelog): resolve unreleased merge 2026-04-05 16:12:05 +01:00
Peter Steinberger
82ce30b789 feat(plugins): add reply dispatch hook 2026-04-05 16:11:31 +01:00
Peter Steinberger
511e6c4189 test: untangle provider tests from extension internals 2026-04-05 16:09:55 +01:00
Vincent Koc
f64a058348 docs(changelog): add dreaming aging controls entry 2026-04-05 16:09:12 +01:00
Peter Steinberger
6e3155ca84 feat(memory-core): add dreaming aging controls 2026-04-05 15:59:06 +01:00
Vincent Koc
c1bba98e88 docs(changelog): sort unreleased by user interest and fix attribution 2026-04-05 15:57:54 +01:00
Peter Steinberger
3a4b96bfbf fix: normalize plugin SDK aliases on Windows 2026-04-05 15:57:47 +01:00
Peter Steinberger
65f18d6e24 fix: guard bundled channel discovery reentry 2026-04-05 15:57:47 +01:00
Peter Steinberger
003f52db98 fix: add Windows fallback for atomic JSON writes 2026-04-05 15:57:47 +01:00
Peter Steinberger
5eb551ccfa fix: harden Windows Parallels smoke install and onboarding 2026-04-05 15:57:47 +01:00
Peter Steinberger
b723b30def test: flatten provider catalog integration hotspots 2026-04-05 15:51:18 +01:00
Peter Steinberger
9408f682f6 test(memory-core): expand dreaming edge coverage 2026-04-05 15:47:26 +01:00
Peter Steinberger
f7670bde7e fix(memory-core): align dreaming promotion 2026-04-05 15:47:25 +01:00
Peter Steinberger
40ffada812 refactor: keep plugin legacy repair in doctor 2026-04-05 15:44:53 +01:00
Peter Steinberger
6f2f840e97 refactor: collapse plugin sdk extension shims 2026-04-05 15:44:53 +01:00
Peter Steinberger
eb8f0e1bf2 fix(ci): restore plugin sdk exports and ACP typing 2026-04-05 15:44:43 +01:00
Peter Steinberger
575371b6f7 test: trim provider compatibility cold starts 2026-04-05 15:44:29 +01:00
Peter Steinberger
3d3ef6f65f docs(changelog): remove self-thanks from acpx entry (#61319) 2026-04-05 23:42:29 +09:00
Peter Steinberger
33363ab922 docs(changelog): note embedded acpx runtime (#61319) 2026-04-05 23:42:29 +09:00
Peter Steinberger
8e51207626 test(acp): type agent override map 2026-04-05 15:40:12 +01:00
Peter Steinberger
69466cec2f test(acp): record bind live typing fix 2026-04-05 15:40:12 +01:00
Peter Steinberger
c66ff16c59 test(acp): fix bind live test typing (follow-up) 2026-04-05 15:40:12 +01:00
Peter Steinberger
0b38916c5e test(acp): fix bind live test typing 2026-04-05 15:40:11 +01:00
Peter Steinberger
5a5b2b1764 test(acp): harden embedded bind live coverage 2026-04-05 15:40:11 +01:00
Peter Steinberger
1f912482e5 fix(acpx): honor ACP probe and session reuse invariants 2026-04-05 15:40:11 +01:00
Peter Steinberger
fb61986767 refactor(acpx): embed ACP runtime in plugin 2026-04-05 15:40:11 +01:00
Vincent Koc
1a537fcfcf fix(ci): pin control ui locale translation settings 2026-04-05 15:35:06 +01:00
Vincent Koc
7b7d645193 fix(ui): make control locale batching fail faster 2026-04-05 15:34:02 +01:00
Vincent Koc
692671f377 docs(changelog): re-sort Fixes section 2026-04-05 15:31:51 +01:00
Gustavo Madeira Santana
0aaf753148 matrix: add exec approval reaction shortcuts (#60931)
Merged via squash.

Prepared head SHA: a34e8248b0
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-05 10:30:33 -04:00
Peter Steinberger
934641df86 perf(auto-reply): defer ACP runtime imports 2026-04-05 15:27:43 +01:00
Vincent Koc
7b05253bed fix(anthropic): strip host otel env from claude cli 2026-04-05 15:27:29 +01:00
Peter Steinberger
dfd39a81d8 feat(openai): add opt-in GPT personality 2026-04-05 15:25:06 +01:00
Peter Steinberger
71fa5f481d test: split cli backend coverage by ownership 2026-04-05 15:20:15 +01:00
Peter Steinberger
de4344a23a perf: bypass setup registry for provider policy hooks 2026-04-05 15:19:12 +01:00
Vincent Koc
4399aaebbb fix(ui): harden control ui locale translation retries 2026-04-05 15:16:59 +01:00
Peter Steinberger
42abcf9886 test: isolate openai codex transport coverage 2026-04-05 15:14:06 +01:00
Peter Steinberger
9f2b760d33 refactor: move media generation runtimes into core 2026-04-05 15:13:20 +01:00
Peter Steinberger
5da21bc2f7 refactor: route runtime seams through plugin sdk facades 2026-04-05 15:13:19 +01:00
Peter Steinberger
7ff7a27f61 feat(memory-core): add dreaming verbose logging 2026-04-05 15:10:59 +01:00
Vincent Koc
bcd0a492a4 fix(cli): preserve claude cache creation tokens 2026-04-05 15:09:27 +01:00
Peter Steinberger
b0f4af3bad test: trim slow provider auth marker coverage 2026-04-05 15:07:19 +01:00
Peter Steinberger
79d722e922 fix: tighten group chat reply spacing guidance 2026-04-05 15:06:09 +01:00
Vincent Koc
8143b9a23e fix(doctor): add claude-cli health checks 2026-04-05 15:03:48 +01:00
Vincent Koc
9320efd9db docs(agents): simplify i18n generated-output guidance 2026-04-05 15:01:33 +01:00
Vincent Koc
7cd015b203 fix(agents): rotate claude cli bindings on reset 2026-04-05 14:54:25 +01:00
Peter Steinberger
21270c2586 fix: resolve post-rebase typecheck drift 2026-04-05 14:53:53 +01:00
Peter Steinberger
629baf5fa7 refactor: move plugin setup and memory capabilities to registries 2026-04-05 14:53:53 +01:00
Peter Steinberger
695c9c887b test: speed up openai codex provider cases 2026-04-05 14:53:21 +01:00
Peter Steinberger
e1142f4197 build: refresh tool display snapshot 2026-04-05 14:47:46 +01:00
Vincent Koc
88e9268399 docs(changelog): remove duplicates and re-sort unreleased entries A-Z 2026-04-05 14:46:12 +01:00
Vincent Koc
c9471f08d5 chore(ui): regenerate control ui locale bundles 2026-04-05 14:38:27 +01:00
Vincent Koc
1ef6bada36 feat(ui): add tr id and pl control ui locales 2026-04-05 14:38:27 +01:00
Peter Steinberger
7e29e84fa4 docs: add it tr id pl publish locales 2026-04-05 14:37:59 +01:00
Peter Steinberger
1d2d70a8fd perf: trim provider policy runtime lookups 2026-04-05 14:37:51 +01:00
Tak Hoffman
d28b02a7b1 fix: preserve Foundry image capability through runtime 2026-04-05 08:36:11 -05:00
Vincent Koc
84eb617a79 fix(agents): persist claude cli session ids 2026-04-05 14:35:34 +01:00
Nimrod Gutman
28955a36e7 feat(ios): add exec approval notification flow (#60239)
* fix(auth): hand off qr bootstrap to bounded device tokens

* feat(ios): add exec approval notification flow

* fix(gateway): harden approval notification delivery

* docs(changelog): add ios exec approval entry (#60239) (thanks @ngutman)
2026-04-05 16:33:22 +03:00
Peter Steinberger
98bac6a0e4 docs: generate full locale nav during publish sync 2026-04-05 14:29:54 +01:00
Peter Steinberger
9a0d88a868 refactor: move talk config contract under plugin 2026-04-05 14:26:35 +01:00
Peter Steinberger
d842251ef8 fix(acp): guard missing delivery channel config 2026-04-05 14:24:01 +01:00
Vincent Koc
2780980a28 docs(changelog): re-sort unreleased entries 2026-04-05 14:23:09 +01:00
Peter Steinberger
ca1da659e4 fix: restore tool display checks 2026-04-05 14:20:31 +01:00
Peter Steinberger
89e8c8672c fix: break bundled channel bootstrap cycles 2026-04-05 14:20:31 +01:00
Peter Steinberger
4fedc5c105 test(config): guard optional tools schema parse result 2026-04-05 14:18:30 +01:00
Peter Steinberger
01c5dde6d1 fix(agents): add update_plan display metadata 2026-04-05 14:18:30 +01:00
Vincent Koc
9efc033434 fix(anthropic): seed claude-cli model switches 2026-04-05 14:18:02 +01:00
Peter Steinberger
d893ae341c fix(auto-reply): remove direct working status updates 2026-04-05 14:14:51 +01:00
Peter Steinberger
e8cbc1ee8a fix(discord): support carbon ratelimit signature drift 2026-04-05 14:11:43 +01:00
Peter Steinberger
9ddc3576d1 refactor: move elevenlabs talk config into plugin 2026-04-05 14:11:10 +01:00
Peter Steinberger
a705845e18 feat(agents): add experimental structured plan updates 2026-04-05 14:08:43 +01:00
Peter Steinberger
c731c1e61f fix(discord): sync proxy request client with carbon 2026-04-05 14:07:32 +01:00
Peter Steinberger
760c4be438 feat(agents): add provider-owned system prompt contributions 2026-04-05 14:05:41 +01:00
Vincent Koc
1a7c2a9bc8 fix(auth): persist claude-cli login profiles 2026-04-05 14:03:36 +01:00
Vincent Koc
e6f1a59e67 docs(changelog): note multilingual control ui locales 2026-04-05 14:03:36 +01:00
Vincent Koc
70d77f5425 fix(ci): reuse mintlify translation secret for control ui 2026-04-05 14:00:38 +01:00
Peter Steinberger
ed1734a7c7 test: stabilize provider normalization lanes 2026-04-05 13:56:52 +01:00
Peter Steinberger
388f82f22f test: stabilize provider auth discovery cases 2026-04-05 13:56:52 +01:00
Peter Steinberger
4e550a873e fix(agents): restore tool display summary typing 2026-04-05 13:55:43 +01:00
Vincent Koc
79e5101a88 fix(ci): trigger control ui locale refresh on main changes 2026-04-05 13:55:20 +01:00
Vincent Koc
1840611fe6 fix(ui): make locale generation formatter-stable 2026-04-05 13:55:20 +01:00
Vincent Koc
fd0ffec4e4 chore(ui): refresh generated locale bundles 2026-04-05 13:55:20 +01:00
Vincent Koc
e681cc057b chore(ci): reuse shared locale translation secrets 2026-04-05 13:55:20 +01:00
Vincent Koc
ac2ca8b2ca chore(ui): regenerate control ui locale bundles 2026-04-05 13:55:20 +01:00
Vincent Koc
ee4fe4fb1e feat(ui): add control ui locale sync pipeline 2026-04-05 13:55:20 +01:00
Peter Steinberger
21d5f7a915 fix(test): restore signal test api boundary 2026-04-05 13:52:33 +01:00
Vincent Koc
8bfbc2ba5d docs(changelog): re-sort unreleased entries 2026-04-05 13:43:42 +01:00
Ayaan Zaidi
75752a862d fix(plugins): add required bedrock mantle config schema 2026-04-05 18:11:37 +05:30
Ayaan Zaidi
e4206007cc fix(memory): stabilize manager runtime lazy import 2026-04-05 18:11:37 +05:30
Peter Steinberger
d70162864a chore(deps): update direct dependencies 2026-04-05 13:33:16 +01:00
Peter Steinberger
31f5463a1c refactor(agents): enrich tool descriptions 2026-04-05 13:31:57 +01:00
Peter Steinberger
987f4bba80 test: make vitest worker caps deterministic 2026-04-05 13:31:15 +01:00
Peter Steinberger
c74b222ec1 test: keep chutes env discovery on test harness 2026-04-05 13:31:15 +01:00
Peter Steinberger
9f7aaa8ad7 test: route vitest through node launcher 2026-04-05 13:31:15 +01:00
Peter Steinberger
074af3f40e test: speed up vitest launcher startup 2026-04-05 13:31:15 +01:00
Peter Steinberger
6f5ba51f74 docs: update IRC host examples 2026-04-05 13:27:04 +01:00
Peter Steinberger
1dc3da6eda refactor(agents): use structured tool definitions 2026-04-05 13:26:34 +01:00
Vincent Koc
7343d1b2ad fix(runtime): guard import-time side effects 2026-04-05 13:20:06 +01:00
Peter Steinberger
aca488d5be fix(gateway): keep watch restarts in-process 2026-04-05 13:16:22 +01:00
Peter Steinberger
2f5d6c859d style(test): normalize group policy helper export 2026-04-05 13:15:22 +01:00
Peter Steinberger
c039675054 refactor(test): split channel contract helpers by policy 2026-04-05 13:15:22 +01:00
Peter Steinberger
e9bf9fde06 test: split legacy pi-tools schema shards 2026-04-05 13:10:16 +01:00
Peter Steinberger
a060b89e3f fix(ci): remove duplicate grok test provider inference 2026-04-05 13:07:22 +01:00
Vincent Koc
fc9648b620 docs: add Bedrock inference profiles and Bedrock Mantle provider coverage, re-sort changelog 2026-04-05 13:04:47 +01:00
Vincent Koc
35b132c7eb fix(config): lazy bootstrap markdown table defaults 2026-04-05 13:04:19 +01:00
Peter Steinberger
227a13bd55 fix: pin defu to 6.1.5 2026-04-05 13:03:30 +01:00
Peter Steinberger
88ea0751a9 fix(test): add lightweight whatsapp group-policy seam 2026-04-05 12:57:58 +01:00
Peter Steinberger
81c095d945 fix(test): break zalo group-policy import cycle 2026-04-05 12:57:58 +01:00
Peter Steinberger
2635e07bf0 fix(openai): add multilingual gpt ack prompts 2026-04-05 12:57:41 +01:00
Peter Steinberger
76da484bed fix: infer synthetic provider auth in implicit tests 2026-04-05 12:54:09 +01:00
Peter Steinberger
21ef63d9f2 test: use threads for core vitest projects 2026-04-05 12:54:09 +01:00
Peter Steinberger
19c081d4a2 test: relax vitest host throttle on big machines 2026-04-05 12:54:09 +01:00
Peter Steinberger
41d08a6feb test: restore thread-first vitest defaults 2026-04-05 12:54:08 +01:00
wirjo
dbac5fa258 feat(bedrock): add Bedrock Mantle (OpenAI-compatible) provider (#61296)
* feat(bedrock): add Bedrock Mantle (OpenAI-compatible) provider

New amazon-bedrock-mantle extension that provides auto-discovery and
authentication for Amazon Bedrock Mantle endpoints.

Mantle (bedrock-mantle.<region>.api.aws) is Amazon Bedrock's OpenAI-
compatible API surface, separate from the existing bedrock-runtime
(ConverseStream) endpoint. It has its own model catalog including
models not available via ConverseStream (e.g. openai.gpt-oss-120b,
mistral.devstral-2-123b).

Extension structure:
- discovery.ts: Model discovery via GET /v1/models (OpenAI format),
  bearer token resolution, implicit provider configuration
- register.sync.runtime.ts: Provider registration with catalog,
  error classification (rate limits, context overflow)
- openclaw.plugin.json: Plugin manifest, enabledByDefault

Auth support:
- Long-lived Bedrock API key (AWS_BEARER_TOKEN_BEDROCK env var)
  created from the AWS Console → used directly as Bearer token
- Pre-generated SigV4-derived tokens (via aws-bedrock-token-generator)
  set in AWS_BEARER_TOKEN_BEDROCK → works transparently

Provider config (auto-resolved when AWS_BEARER_TOKEN_BEDROCK is set):
  api: "openai-completions"
  baseUrl: "https://bedrock-mantle.<region>.api.aws/v1"
  auth: "api-key" (bearer token)

Available in 12 regions: us-east-1, us-east-2, us-west-2,
ap-northeast-1, ap-south-1, ap-southeast-3, eu-central-1,
eu-west-1, eu-west-2, eu-south-1, eu-north-1, sa-east-1

Tests: 15 passing (13 discovery + 2 plugin registration)

* chore(bedrock): clarify mantle bearer auth scope

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 12:53:54 +01:00
Peter Steinberger
deb212d3b0 fix(openai): tighten gpt chat action turns 2026-04-05 12:53:35 +01:00
Peter Steinberger
259338565a test: normalize minimax provider timeout formatting 2026-04-05 12:53:24 +01:00
Peter Steinberger
59a243e46b test: stabilize provider discovery matrix cases 2026-04-05 12:53:24 +01:00
Peter Steinberger
d0afdb56ce fix: honor minimax api host during provider discovery 2026-04-05 12:53:23 +01:00
Peter Steinberger
bc7f845714 test: speed up focused pi-tools tool tests 2026-04-05 12:53:15 +01:00
Peter Steinberger
dbcd35f6c2 test: decouple pi-tools params test imports 2026-04-05 12:53:15 +01:00
wirjo
78fe96f2d4 feat(bedrock): add inference profile discovery and region injection (#61299)
* feat(bedrock): add inference profile discovery and region injection

Inference profiles (cross-region and application) work with ConverseStream
but require the SDK client region to match the profile region. Without
this, users get "The provided model identifier is invalid" errors when
using cross-region profiles like us.anthropic.claude-sonnet-4-6.

Changes:

1. Inference profile discovery (discovery.ts):
   - Call ListInferenceProfiles alongside ListFoundationModels (parallel)
   - Inference profiles INHERIT capabilities from their underlying
     foundation model (modalities, reasoning, context window, cost)
   - resolveBaseModelId() maps profile → foundation model:
     "us.anthropic.claude-sonnet-4-6" → "anthropic.claude-sonnet-4-6"
     Application ARNs → extract model ID from models[].modelArn
   - Graceful degradation if IAM lacks bedrock:ListInferenceProfiles
   - Provider filter applies to profiles via underlying model ARNs

2. Region injection (register.sync.runtime.ts):
   - Extract region from provider baseUrl or bedrockDiscovery.region
   - Pass through to pi-ai options.region in wrapStreamFn
   - Ensures SDK client connects to correct regional endpoint

3. Inference profile model detection (anthropic-family-cache-semantics.ts):
   - isAnthropicBedrockModel() now recognizes application inference
     profile ARNs (arn:aws:bedrock:...:application-inference-profile/*)

4. Tests (discovery.test.ts):
   - New: inference profile inheritance test (4 models: 1 foundation +
     3 profiles, verifies capability inheritance, inactive filtering)
   - New: graceful AccessDeniedException handling test
   - Updated: all existing tests for dual-API discovery pattern

Fixes #55642

* fix(bedrock): preserve inference profile model lookup

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 12:52:03 +01:00
Vincent Koc
5f6ba749ff fix(test): restore thread-first vitest defaults 2026-04-05 12:37:39 +01:00
Peter Steinberger
b9d26fd1a4 docs: add arabic locale scaffolding 2026-04-05 12:37:22 +01:00
Peter Steinberger
996dccb19c feat(agents): add structured execution item events 2026-04-05 12:36:33 +01:00
Peter Steinberger
3b7e6152d1 fix: retry reasoning-required compaction with minimal thinking 2026-04-05 12:35:50 +01:00
Vincent Koc
c9e505f54a docs(changelog): sort and verify new entries 2026-04-05 12:32:16 +01:00
Peter Steinberger
8ad6dd92d7 ci: retrigger stalled main workflow 2026-04-05 12:19:24 +01:00
Mariano
4175caee6e fix(agents): suppress commentary-phase output leaks (#61282)
Merged via squash.

Prepared head SHA: e392904f73
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-04-05 13:04:12 +02:00
Peter Steinberger
1bf5339b98 fix(gateway): pin startup channel registry 2026-04-05 12:01:03 +01:00
Peter Steinberger
283a6c65b7 ci: clone docs mirror without checkout api 2026-04-05 11:55:24 +01:00
Peter Steinberger
7fc3b22f53 fix: update changelog for Slack DM live reply routing (#59030) (thanks @afurm) 2026-04-05 19:53:23 +09:00
Andrii Furmanets
379f0d78e6 Slack: route live DM replies to channel 2026-04-05 19:53:23 +09:00
Peter Steinberger
25da786c68 docs: add generated locale picker support 2026-04-05 11:53:02 +01:00
Vincent Koc
cfe66c6e02 test(contracts): guard config footprint regressions 2026-04-05 11:48:40 +01:00
Peter Steinberger
fef155cdbc fix: tighten file tool schemas for openai 2026-04-05 11:46:34 +01:00
Vincent Koc
63db3443f1 fix(plugin-sdk): prefer canonical private-network opt-in 2026-04-05 11:45:09 +01:00
Peter Steinberger
0f58cef75e fix: finalize facade re-entry landing cleanup (#61180) (thanks @adam91holt) 2026-04-05 11:42:29 +01:00
Peter Steinberger
66c047ddc3 fix: refresh facade re-entry landing delta (#61180) (thanks @adam91holt) 2026-04-05 11:40:51 +01:00
Adam Holt
3a957cfe8b fix: resolve circular re-entry in facade module loading
resolveTrackedFacadePluginId triggers config loading (plugin auto-enable,
channel discovery) which can re-enter loadBundledPluginPublicSurfaceModuleSync
for the same module. Because the sentinel was still empty at that point,
re-entrant callers saw undefined exports (e.g. shouldNormalizeGoogleProviderConfig).

Move Object.assign(sentinel, loaded) before the plugin ID resolution so any
re-entrant lookup through the cached sentinel finds the real exports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 11:40:32 +01:00
Vincent Koc
c5f69111ce docs(changelog): sort unreleased entries alphabetically 2026-04-05 11:38:04 +01:00
Vincent Koc
440428889e docs: add missing contributor attribution to Unreleased changelog entries 2026-04-05 11:33:58 +01:00
Peter Steinberger
dbfd96f4ec docs: move ja-JP output to publish repo 2026-04-05 11:32:55 +01:00
Peter Steinberger
495ebd28a4 fix(ci): route zai endpoint constants through plugin sdk 2026-04-05 11:30:58 +01:00
Peter Steinberger
064f474ed7 test: isolate anthropic vertex provider env cases 2026-04-05 11:28:59 +01:00
Vincent Koc
fd0cc90427 fix(plugin-sdk): resolve facade post-load re-entry (#61286) 2026-04-05 11:25:36 +01:00
Peter Steinberger
4559ece355 fix(ci): align test fixtures with current runner types 2026-04-05 11:23:51 +01:00
Chunyue Wang
8c1ca1f245 fix(cron): remove OpenAPI 3.0 incompatible JSON Schema keywords from cron tool (#61221)
The cron tool schema used type arrays (['string','null']), the 'not'
keyword, and 'const' — all unsupported by the OpenAPI 3.0 subset that
Gemini-backed providers (e.g. GitHub Copilot) enforce. This caused
HTTP 400 for every request when cron was enabled.

Replace type arrays with scalar types, remove not/const from
CronFailureAlertSchema, and add 'not' to the Gemini unsupported
keywords list as defense-in-depth.

Fixes #61206
2026-04-05 11:21:45 +01:00
Peter Steinberger
359be4eb48 test: simplify runtime cleanup setup imports 2026-04-05 11:19:06 +01:00
Peter Steinberger
2d7ec1b641 refactor: split zai config sdk seam 2026-04-05 11:19:05 +01:00
Peter Steinberger
be526d6423 refactor: split provider stream sdk seams 2026-04-05 11:19:05 +01:00
Peter Steinberger
0a21eebf56 fix(openai): keep gpt chat replies concise 2026-04-05 11:16:28 +01:00
Peter Steinberger
af81ee9fee fix(agents): add embedded item lifecycle events 2026-04-05 11:16:28 +01:00
Peter Steinberger
1ad5695aa4 ci: trigger zh-CN refresh on release 2026-04-05 11:16:00 +01:00
Vincent Koc
f02e435188 fix(google): support gemini cli personal oauth (#61260)
* fix(google): support gemini cli personal oauth

* Apply suggestion from @greptile-apps[bot]

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(google): prefer gemini settings over auth env fallback

* chore(changelog): format rebased gemini entry

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-05 11:12:54 +01:00
Peter Steinberger
fd917a471c test: scope implicit provider discovery harness 2026-04-05 11:12:41 +01:00
Vincent Koc
be5a2611b9 test(anthropic): reuse wizard prompter helper (#61280) 2026-04-05 11:09:00 +01:00
Peter Steinberger
f2dc241e9d docs: harden zh-CN translation flow 2026-04-05 11:02:39 +01:00
Vincent Koc
3b84884793 fix(agents): harden host-managed claude-cli auth path (#61276) 2026-04-05 11:02:18 +01:00
Peter Steinberger
afca9540bf fix: add openai responses phase support 2026-04-05 10:58:49 +01:00
Vincent Koc
852e8f7a2a docs: update Claude CLI backend docs for MCP bridge, streaming, and auth changes 2026-04-05 10:54:11 +01:00
Vincent Koc
1d736dcbbc fix(ci): drop unused google prompt cache type 2026-04-05 10:49:51 +01:00
Peter Steinberger
e3eb615da8 docs: salvage english docs from translation backlog 2026-04-05 10:45:08 +01:00
Vincent Koc
3fa70f3044 fix(google): support gemini cli 2.5 model ids (#61261)
* fix(google): realign gemini cli model defaults

* fix(google): keep gemini cli defaults while adding 2.5 support

* fix(google): preserve gemini template reasoning flags

* fix(google): fall back to cli templates for gemini 2.5 ids

* fix(google): keep gemini cli 3.1 clones local
2026-04-05 10:43:20 +01:00
Vincent Koc
d609f71c9b fix(feishu): gate reasoning previews to stream sessions (#61271) 2026-04-05 10:40:22 +01:00
Vincent Koc
64cf52ca20 fix(tool-display): generate swift snapshot from core config 2026-04-05 10:34:02 +01:00
Peter Steinberger
e468da1040 fix: improve gpt execution flow and visibility 2026-04-05 10:32:58 +01:00
Peter Steinberger
219afbc2cc docs: tighten docs i18n source workflow 2026-04-05 10:30:29 +01:00
Vincent Koc
4954d025e2 fix(telegram): gate reasoning previews to stream sessions (#61266) 2026-04-05 10:22:26 +01:00
Peter Steinberger
b3d73b648b test: fix hook-alias runtime coverage after rebase (#61234) 2026-04-05 18:19:33 +09:00
Peter Steinberger
1fb0b4f557 fix: avoid stale claude-cli auth fallback (#61234) (thanks @darkamenosa) 2026-04-05 18:19:33 +09:00
Tuyen
7e724c6140 Anthropic: seed claude-cli runtime auth on setup 2026-04-05 18:19:33 +09:00
Tuyen
72ba7c8995 Anthropic: address claude-cli review feedback 2026-04-05 18:19:33 +09:00
Tuyen
cd348659ce Anthropic: fix claude-cli runtime auth 2026-04-05 18:19:33 +09:00
Peter Steinberger
9d315cdf42 test: default vitest lanes to isolated forks 2026-04-05 10:11:33 +01:00
Onur
d4e06d1249 Revert "[codex] Reproduce session stall and restart drain bugs (#61225)" (#61265)
This reverts commit 83d29dae2b.
2026-04-05 11:10:20 +02:00
Vincent Koc
d5cde2171b fix(agents): surface disk full session write errors (#61264) 2026-04-05 10:09:42 +01:00
Vincent Koc
ef3a185225 fix(ci): keep bedrock config compat inside the extension 2026-04-05 10:08:47 +01:00
Peter Steinberger
84fb62170a docs: clarify anthropic cli fallback guidance 2026-04-05 10:06:32 +01:00
Bob
83d29dae2b [codex] Reproduce session stall and restart drain bugs (#61225)
* Tests: reproduce session stall and drain bugs

* Docs: add reply lifecycle unification plan

* Docs: lock down reply lifecycle plan

* Delete docs/experiments/plans/reply-lifecycle-unification.md

---------

Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 10:05:40 +01:00
wesley
1030b498de fix(acpx): retry persisted resume ids cleanly (#52209)
* fix(acpx): store agent session ID when session/load fails

When an ACP agent (e.g. Gemini CLI) rejects the acpx-generated session
ID via session/load and falls back to session/new, the agent-returned
session ID was previously discarded. This caused identity stuck at
pending forever, multi-turn failures, lost completion events, and
persistent reconcile warnings.

- Parse ACP protocol stream in runTurn() to capture agent session IDs
- Flip resolveRuntimeResumeSessionId() to prefer agentSessionId
- Add createIdentityFromHandleEvent() for handle-sourced identity
- Layer handle event identity before status in reconcile
- Add regression tests for load fallback and restart resume

Closes #52182

* ACPX: prefer decoded session ids

* ACPX: refresh runtime handle state from status

---------

Co-authored-by: Wesley <imwyvern@users.noreply.github.com>
2026-04-05 10:01:59 +01:00
Vincent Koc
cc09171929 fix(ci): align sanitize session history tests with transcript types 2026-04-05 10:01:29 +01:00
Vincent Koc
f1f8fd5970 fix(plugins): drop dead runtime helper 2026-04-05 09:59:50 +01:00
Vincent Koc
2489913ede refactor(tlon): align internal network naming 2026-04-05 09:59:50 +01:00
Peter Steinberger
4a85810091 fix: migrate bedrock discovery config in doctor 2026-04-05 09:55:55 +01:00
Peter Steinberger
19de5d1b56 refactor: move provider discovery config into plugins 2026-04-05 09:55:55 +01:00
Vincent Koc
4613f121ad fix(agents): preserve native Anthropic replay tool ids (#61254)
* fix(agents): preserve native Anthropic replay tool ids

* docs(changelog): note native Anthropic replay ids

* fix(agents): preserve native Anthropic replay ids selectively
2026-04-05 09:53:52 +01:00
Peter Steinberger
a9c52dd935 test(gateway): cover claude cli bootstrap injection 2026-04-05 17:51:41 +09:00
Peter Steinberger
3d952aa35d fix(agents): preserve claude cli backend defaults 2026-04-05 17:51:41 +09:00
Daev Mithran
03be4c2489 fix(plugin-sdk): export missing context-engine types (#61251)
* fix(plugin-sdk): export missing context-engine types

Signed-off-by: DaevMithran <daevmithran1999@gmail.com>

* build(plugin-sdk): refresh api baseline hash

* docs(changelog): note context engine sdk exports

---------

Signed-off-by: DaevMithran <daevmithran1999@gmail.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 09:49:19 +01:00
Vincent Koc
19e97193d3 fix(ci): make discord doctor loading bundler-safe 2026-04-05 09:48:11 +01:00
Peter Steinberger
48653c2031 fix: recover launchd restart and restore prompt-cache gate 2026-04-05 17:47:07 +09:00
Peter Steinberger
48f0d9aaf7 fix: normalize telegram announce thread targets 2026-04-05 09:44:20 +01:00
Peter Steinberger
a3f6e58928 docs: move zh-CN output to publish repo 2026-04-05 09:44:05 +01:00
Vincent Koc
3a9569ff38 fix(ci): keep self-hosted setup out of plugin internals 2026-04-05 09:43:35 +01:00
Vincent Koc
de04eeab76 docs: remove duplicate Unreleased changelog entries 2026-04-05 09:40:16 +01:00
Peter Steinberger
647fc7bfec refactor(plugins): unify explicit provider ownership loading 2026-04-05 09:38:04 +01:00
Peter Steinberger
f9f44b9b96 fix(models): restore anthropic cli auth login 2026-04-05 09:36:47 +01:00
Vincent Koc
50ed91a589 fix(ci): wire heartbeat busy-lane test reply mock 2026-04-05 09:35:55 +01:00
Peter Steinberger
1afa076cfa refactor: simplify plugin auto-enable structure 2026-04-05 09:34:16 +01:00
Peter Steinberger
22db77d2b6 fix: avoid eager web provider config reads 2026-04-05 09:34:01 +01:00
Vincent Koc
0b8336f49d fix(config): align bluebubbles network schema 2026-04-05 09:32:27 +01:00
Vincent Koc
2d6e75ccd5 fix(ci): align google prompt cache stream typing 2026-04-05 09:31:39 +01:00
Vincent Koc
bca6faf11d docs: sync CLI and prompt-caching reference with code 2026-04-05 09:30:31 +01:00
Peter Steinberger
455c642acb feat: add implicit discovery toggles 2026-04-05 09:27:48 +01:00
Peter Steinberger
bff55b55cb style: normalize import ordering and wrapping 2026-04-05 09:26:39 +01:00
Peter Steinberger
fb77c8ce4e chore: ignore local artifacts workspace 2026-04-05 09:26:39 +01:00
Eunho Lee (Tony)
5b9cdb8975 fix(heartbeat): skip busy session lane wake delivery (#40526)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 09:25:45 +01:00
rstar327
43fe68f9ef fix(exec): keep notifyOnExit heartbeat wakes on exec-event (#41479)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 09:25:12 +01:00
Peter Steinberger
8be017fae6 refactor: remove plugin sdk facade generator 2026-04-05 09:23:55 +01:00
Vincent Koc
a4b767c89b docs: sync config reference with unreleased changes 2026-04-05 09:23:18 +01:00
imechZhangLY
0e61a1d0ca fix: windows restart fallback when scheduled task is unregistered (#58943) (thanks @imechZhangLY)
* fix(infra): windows-task-restart fallback to startup entry when schtasks task is unregistered

* fix code style problem

* use /min for startup fallback and assert schtasks pre-check in test

* fix: windows restart fallback when scheduled task is unregistered (#58943) (thanks @imechZhangLY)

---------

Co-authored-by: Luyao Zhang <zhangluyao@microsoft.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-05 13:51:57 +05:30
Peter Steinberger
5ac07b8ef0 fix: normalize huggingface refs and discovery timeout 2026-04-05 09:18:17 +01:00
Vincent Koc
b5f8cd4fcf fix(google): add managed gemini prompt caching 2026-04-05 09:17:51 +01:00
Peter Steinberger
aa497e9c52 refactor: extract daemon launchd recovery helper 2026-04-05 09:16:44 +01:00
Ayaan Zaidi
92c498cf7b build(android): fix flavored release bundle prep 2026-04-05 13:44:44 +05:30
Ayaan Zaidi
90fcc1f551 fix(android): correct App Actions prompt parameter typing 2026-04-05 13:44:44 +05:30
Vincent Koc
c6e117897f test(nextcloud-talk): keep send runtime mock aligned 2026-04-05 09:14:12 +01:00
Peter Steinberger
41e39eb46f refactor: register channel bootstrap capabilities 2026-04-05 09:13:48 +01:00
Vincent Koc
a5b6b71468 test(gateway): align current response and callback types 2026-04-05 09:12:49 +01:00
Vincent Koc
4c11a520a8 fix(plugins): carry workspaceDir in runtime state 2026-04-05 09:10:09 +01:00
Peter Steinberger
3038079c2f fix: clamp pi embedded fenced chunk splits 2026-04-05 09:09:26 +01:00
Peter Steinberger
b57372d665 refactor: route capability runtime through channel stores 2026-04-05 09:07:33 +01:00
Peter Steinberger
1903be5401 refactor: remove generated plugin sdk facades 2026-04-05 09:07:33 +01:00
Peter Steinberger
fd968bfb2d fix: recover unloaded macOS launch agents (#43766) 2026-04-05 17:06:22 +09:00
Peter Steinberger
07e7b7177f test: stabilize pi embedded text-end hooks 2026-04-05 09:06:15 +01:00
Jamil Zakirov
ffb5b99114 fix: propagate workspaceDir to snapshot plugin loads (#61138)
* plugins: include resolved workspaceDir in provider hook cache keys

resolveProviderPluginsForHooks, resolveProviderPluginsForCatalogHooks, and
resolveProviderRuntimePlugin used the raw params.workspaceDir for cache keys
and plugin-id discovery while resolvePluginProviders already fell back to
the active registry workspace. Resolve workspaceDir once at the top of each
function so cache keys, candidate filtering, and loading all use the same
workspace root.

* fix(plugins): inherit runtime workspace for snapshot loads

* test(gateway): stub runtime registry seam

* fix(plugins): restore workspace fallback after rebase

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 09:03:54 +01:00
Peter Steinberger
e28e83f4e4 test: load browser fixture for sandbox policy 2026-04-05 09:00:42 +01:00
Peter Steinberger
3728cfbe29 test: use stable sandbox denied tool assertions 2026-04-05 08:58:55 +01:00
Peter Steinberger
70b8ce72df test: refresh simple completion provider fallback 2026-04-05 08:57:04 +01:00
Vincent Koc
69b74476d7 fix(contracts): lock runtime seam regressions 2026-04-05 08:52:20 +01:00
Peter Steinberger
23275edef1 refactor: simplify web provider plugin discovery 2026-04-05 08:50:01 +01:00
Vincent Koc
c863ee1b86 fix(config): migrate bundled private-network aliases (#60862)
* refactor(plugin-sdk): centralize private-network opt-in semantics

* fix(config): migrate bundled private-network aliases

* fix(config): add bundled private-network doctor adapters

* fix(config): expose bundled channel migration hooks

* fix(config): prefer canonical private-network key

* test(config): refresh rebased private-network outputs
2026-04-05 08:49:44 +01:00
Vincent Koc
87b8680ded fix(cache): order stable project context before heartbeat (#61236)
* fix(cache): order stable project context before heartbeat

* docs(changelog): note project context cache ordering

* Update CHANGELOG.md
2026-04-05 08:49:20 +01:00
Vincent Koc
2d2824874e fix(contracts): align provider and sdk inventories 2026-04-05 08:44:35 +01:00
Peter Steinberger
07c2f81392 fix: preserve explicit Ollama apiKey during discovery 2026-04-05 08:43:50 +01:00
Peter Steinberger
377ccbcf1d test: stabilize gateway chat and method suites 2026-04-05 08:43:21 +01:00
Peter Steinberger
3635b2b8d6 test: split gateway session utils coverage 2026-04-05 08:43:21 +01:00
Vincent Koc
49f52ddf36 fix(fetch): honor mocked global fetch with dispatchers 2026-04-05 08:42:37 +01:00
wzfmini01
ef5f47bd39 fix(google-gemini-cli-auth): detect bundled npm installs (#60486) (#60486)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 08:41:43 +01:00
Peter Steinberger
acd8966ff0 test: refresh agent model expectation fixtures 2026-04-05 08:33:54 +01:00
Peter Steinberger
31d8b022eb fix: treat inline buttons as native approval ui 2026-04-05 08:33:54 +01:00
Peter Steinberger
d91d3cc0f0 fix: respect custom env snapshots for vertex auth 2026-04-05 08:33:54 +01:00
Peter Steinberger
c9c7271f4f test: keep mocked fetch active with guarded dispatchers 2026-04-05 08:33:54 +01:00
Ted Li
b474e098d1 docs: correct overstated prompt-cache comments from #58036 #58037 #58038 (#60633)
* docs: correct overstated prompt-cache comments from #58036 #58037 #58038

* docs: restore purpose context in MCP tool sort comment

* docs: drop misleading 'legacy' framing from image-prune comments

* docs: restore useful context stripped from image-prune comments

* docs: restore 'deterministically' in MCP tool sort comment

* docs: restore 'idempotent' at attempt.ts callsite

* docs: restore 'provider prompt cache' in context-guard comment
2026-04-05 08:32:51 +01:00
Peter Steinberger
c2bf2cc2b7 test: stabilize gateway config.apply cases 2026-04-05 08:31:08 +01:00
wirjo
019a25e35c Fix/bedrock aws sdk apikey injection (#61194)
* fix(bedrock): stop injecting fake apiKey marker for aws-sdk auth when no env vars exist

When the Bedrock provider uses auth: "aws-sdk" and no AWS environment
variables are set (EC2 instance roles, ECS task roles, etc.),
resolveAwsSdkApiKeyVarName() fell back to "AWS_PROFILE" unconditionally.
This string was injected as apiKey in the provider config during
normalisation, which poisoned the downstream auth resolver — it treated
the marker as a literal key and failed with "No API key found".

The fix:
- resolveAwsSdkApiKeyVarName() now returns undefined (not "AWS_PROFILE")
  when no AWS env vars are present
- resolveBedrockConfigApiKey() (extension) gets the same fix
- resolveMissingProviderApiKey() guards both the providerApiKeyResolver
  and direct aws-sdk branches: if the resolver returns nothing, the
  provider config is returned unchanged (no apiKey injected)
- The aws-sdk credential chain then resolves credentials at request time
  via IMDS/ECS task role/etc. as intended

When AWS env vars ARE present (AWS_ACCESS_KEY_ID, AWS_PROFILE,
AWS_BEARER_TOKEN_BEDROCK), the marker is still injected correctly.

Closes #49891
Closes #50699
Fixes #54274

* test(bedrock): update resolveBedrockConfigApiKey test for undefined return on empty env

The test previously expected "AWS_PROFILE" when no env vars are set.
Now expects undefined (matching the fix), and adds a separate assertion
that AWS_PROFILE is returned when the env var is actually present.

* fix(bedrock): lock aws-sdk env marker behavior

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 08:24:05 +01:00
狼哥
eb130aa4e9 fix(google): disable pinned dns for image generation (#59873)
* fix(google): restore proxy-safe image generation (#59873)

* fix(ssrf): preserve transport policy without pinned dns

* fix(ssrf): use undici fetch for dispatcher requests

* fix(ssrf): type dispatcher fetch path

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 08:23:22 +01:00
Peter Steinberger
9238b98a7a fix: fall back to resolved agent dir for btw command 2026-04-05 08:21:52 +01:00
Peter Steinberger
2aafa8fb7d refactor: remove ollama sdk facades 2026-04-05 08:15:39 +01:00
Vincent Koc
155f4300ba fix(voice-call): use full config for realtime transcription (#61224)
* fix(voice-call): use full config for realtime transcription

* fix(changelog): note voice-call transcription regression

* Update CHANGELOG.md
2026-04-05 08:14:41 +01:00
Vincent Koc
42bc411c46 fix(gateway): catch invalid cron session targets 2026-04-05 08:10:29 +01:00
André Santos
eb0f367e00 fix(cache): enable prompt cache retention for Anthropic Vertex AI (#60888)
* fix(cache): enable prompt cache retention for Anthropic Vertex AI

* fix(cache): add anthropic-vertex to isAnthropicFamilyCacheTtlEligible

* fix(cache): use hostname parsing for long-TTL endpoint eligibility

* docs(changelog): note anthropic vertex cache ttl fix

---------

Co-authored-by: affsantos <andreffsantos91@gmail.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 08:07:02 +01:00
Peter Steinberger
a6894a5238 test: harden live model skip handling 2026-04-05 08:04:56 +01:00
Peter Steinberger
68851f2e97 fix(config): cap generated schema export types 2026-04-05 07:58:02 +01:00
Peter Steinberger
20803dac14 fix: fail closed for invalid persisted cron targets 2026-04-05 07:57:16 +01:00
Peter Steinberger
b7a08c6bad fix: preserve catalog metadata for allowlisted models 2026-04-05 07:56:31 +01:00
Peter Steinberger
20b08f1a85 fix: enforce paired scope baselines on reconnect 2026-04-05 07:53:57 +01:00
Vincent Koc
19b7fbaa73 fix(memory): honor mocked batch fetch clients 2026-04-05 07:48:03 +01:00
Peter Steinberger
a65ab607c7 fix(gateway): use launchd KeepAlive restarts 2026-04-05 07:43:37 +01:00
Peter Steinberger
d655a8bc76 feat: add Fireworks provider and simplify plugin setup loading 2026-04-05 07:43:14 +01:00
Ayaan Zaidi
f842f518cd fix: update embedded runner transport override (#61214)
* fix: update embedded runner transport override

* fix: update embedded runner transport override (#61214)

* fix: update embedded runner transport override (#61214)

* fix: update embedded runner transport override (#61214)
2026-04-05 12:12:50 +05:30
Peter Steinberger
bf226be64a test: keep cli backend coverage on core seams 2026-04-05 07:40:46 +01:00
Peter Steinberger
c9029503fd fix: honor mocked guarded fetch implementations 2026-04-05 07:39:43 +01:00
Vincent Koc
c09bf9812a fix(build): restore main build on current agent api 2026-04-05 07:38:09 +01:00
Vincent Koc
005766671e fix(ci): use agent transport property 2026-04-05 07:34:45 +01:00
Vincent Koc
cb1bf28526 build(a2ui): allow sparse core builds 2026-04-05 07:34:33 +01:00
Vincent Koc
2a999bf9c9 refactor(memory): invert memory host sdk dependency 2026-04-05 07:34:33 +01:00
Peter Steinberger
f59da4557c test: refresh gateway talk and scope fixtures 2026-04-05 07:31:30 +01:00
Peter Steinberger
332afa2fda refactor: narrow claude cli fallback seams 2026-04-05 07:29:32 +01:00
Vincent Koc
3da235bf39 fix(telegram): force paginated commands callbacks 2026-04-05 07:28:47 +01:00
Vincent Koc
61fc4a16b7 docs(changelog): remove duplicate Unreleased entries 2026-04-05 07:23:04 +01:00
Vincent Koc
db1d62b784 test(ci): cover bare default provider inference 2026-04-05 07:19:52 +01:00
Peter Steinberger
a084e46536 fix: use undici runtime fetch for dispatcher flows 2026-04-05 07:18:33 +01:00
Peter Steinberger
757fe86309 test: lock whatsapp session migration keys 2026-04-05 07:18:15 +01:00
Peter Steinberger
657c6f6788 fix: stabilize docker e2e lanes 2026-04-05 07:15:24 +01:00
Peter Steinberger
e5023cc141 fix(agents): invalidate stale cli sessions on auth changes 2026-04-05 07:14:52 +01:00
Peter Steinberger
903cb3c48c test: align bash exec mocks with reset modules 2026-04-05 07:10:49 +01:00
Peter Steinberger
37cc06f1fd fix: normalize claude cli fallback config 2026-04-05 07:09:13 +01:00
Ayaan Zaidi
f039bbf2aa fix: resolve acpx plugin root from shared chunks 2026-04-05 11:37:05 +05:30
Peter Steinberger
e25693315e fix: stabilize embedded runner transport and channel state 2026-04-05 07:04:18 +01:00
Peter Steinberger
749ed86fe3 test: stabilize gateway canvas and session cleanup 2026-04-05 07:04:18 +01:00
Peter Steinberger
5e0e50b12e test: stabilize gateway wizard e2e flow 2026-04-05 07:04:18 +01:00
Ayaan Zaidi
4cfb990382 fix: restore whatsapp doctor contract surface 2026-04-05 11:31:12 +05:30
Peter Steinberger
e9fa9f7822 test: reload transcript policy smoke module 2026-04-05 06:59:55 +01:00
Peter Steinberger
cb31c4813b test: mock models config planner in write serialization 2026-04-05 06:54:40 +01:00
Peter Steinberger
f5da2360a2 test: scope models config write serialization spy 2026-04-05 06:51:08 +01:00
Peter Steinberger
7f6e8c0645 test: reload gateway status command under mocks 2026-04-05 06:46:47 +01:00
Peter Steinberger
055428019e test: harden bash tool async exec coverage 2026-04-05 06:42:26 +01:00
Peter Steinberger
b63557679e test: harden models-config write serialization timing 2026-04-05 06:10:30 +01:00
Peter Steinberger
058fde2d88 test: reload runtime plugins module per test 2026-04-05 06:06:12 +01:00
Peter Steinberger
74416c5b33 test: force real timers for exec foreground timeout 2026-04-05 06:01:21 +01:00
Peter Steinberger
f7a32cd25e test: reset imessage facade runtime before each test 2026-04-05 05:58:02 +01:00
Peter Steinberger
15d5878d91 test: update telegram paginated commands expectations 2026-04-05 05:53:42 +01:00
Peter Steinberger
50b5c483ee fix: canonicalize legacy whatsapp group sessions 2026-04-05 05:47:04 +01:00
tarouca
bf0f4d93f0 fix: restore Telegram DM voice-note transcription (#61008) (thanks @manueltarouca)
* fix(telegram): enable voice-note transcription in DMs

The preflight transcription condition only triggered for group chats
(isGroup && requireMention), so voice notes sent in direct messages
were never transcribed -- they arrived as raw <media:audio> placeholders.

This regression was introduced when the Telegram channel was moved from
src/telegram/ to extensions/telegram/, losing the fix from c15385fc94.

Widen the condition to fire whenever there is audio and no accompanying
text, regardless of chat type. Group-specific guards (requireMention,
disableAudioPreflight, senderAllowedForAudioPreflight) still apply
only in group contexts.

* fix: restore Telegram DM voice-note transcription (#61008) (thanks @manueltarouca)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-05 09:49:44 +05:30
Peter Steinberger
0a71ac5d3c fix: keep discord open-policy allowlist nested 2026-04-05 05:04:10 +01:00
Peter Steinberger
1392a78c75 fix: infer configured provider for bare default models 2026-04-05 05:04:10 +01:00
Ayaan Zaidi
87a0390666 fix: write nested plugin wizard config paths (#61159) 2026-04-05 08:59:12 +05:30
Ayaan Zaidi
69be9c4a6f fix: widen path utils root contract 2026-04-05 08:59:12 +05:30
Ayaan Zaidi
9af48d9c10 fix: write nested plugin wizard config paths 2026-04-05 08:59:12 +05:30
Ayaan Zaidi
11e6c9de2e fix: cancel in-flight Android talk playback on stop (#61164) 2026-04-05 08:53:36 +05:30
Ayaan Zaidi
a746ba2dcb test(android): cover playback disable idempotency 2026-04-05 08:53:36 +05:30
Ayaan Zaidi
8d1f9ab5b8 fix(android): cancel in-flight talk playback on stop 2026-04-05 08:53:36 +05:30
OfflynAI
f0c970fb43 fix: skip sandbox skill copy junk (#61090) (thanks @joelnishanth)
* fix(skills): exclude .git and node_modules when copying skills to workspace (#60879)

* fix(skills): cover sync copy exclusions

* fix: skip sandbox skill copy junk (#61090) (thanks @joelnishanth)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-05 08:42:58 +05:30
Peter Steinberger
a235f5ed64 test: stabilize gateway control ui auth suites 2026-04-05 12:11:29 +09:00
Peter Steinberger
2636cc261c fix: scope discord doctor allowFrom alias migration 2026-04-05 04:06:31 +01:00
Ayaan Zaidi
8355f24652 test: fix talk config gate regression 2026-04-05 08:34:33 +05:30
Peter Steinberger
54a360a33e fix: stabilize shared auth and sessions send tests 2026-04-05 12:03:30 +09:00
Peter Steinberger
cad1b89b26 fix: keep core gateway tool invokes on shipped tools 2026-04-05 12:03:30 +09:00
Peter Steinberger
740d096009 test: stabilize config apply gateway suite 2026-04-05 12:03:30 +09:00
Peter Steinberger
1811e54920 test: fix plugin stream typing assertions 2026-04-05 12:03:30 +09:00
Peter Steinberger
6596e64a68 fix: stabilize gateway auth fallback tests 2026-04-05 12:03:30 +09:00
Vincent Koc
2246e8f0a9 fix(ci): sanitize providerless model warning 2026-04-05 12:02:05 +09:00
Vincent Koc
d23a81baa1 fix(ci): add no-wait completion reply option 2026-04-05 12:00:41 +09:00
Vincent Koc
19ef298678 fix(ci): skip reply wait for non-message subagents 2026-04-05 11:59:16 +09:00
Vincent Koc
7d34c1dc4c test(ci): cover non-waiting subagent completion 2026-04-05 11:58:47 +09:00
Cathryn Lavery
7587e4cac3 fix: ensure bypassPermissions when custom CLI backend args override defaults (#61114)
* fix: ensure bypassPermissions on custom CLI backend args

When users override cliBackends.claude-cli.args (e.g. to add --verbose
or change --output-format), the override array replaces the default
entirely. The normalization step only re-added --permission-mode
bypassPermissions when the legacy --dangerously-skip-permissions flag
was present — if neither flag existed, it did nothing.

This causes cron and heartbeat runs to silently fail with "exec denied:
Cron runs cannot wait for interactive exec approval" because the CLI
subprocess launches in interactive permission mode.

Fix: always inject --permission-mode bypassPermissions when no explicit
permission-mode flag is found in the resolved args, regardless of
whether the legacy flag was present.

* test(anthropic): add claude-cli permission normalization coverage

* fix(test-utils): include video generation providers

* fix: preserve claude-cli bypassPermissions on custom args (#61114) (thanks @cathrynlavery)

---------

Co-authored-by: Shadow <hi@shadowing.dev>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-04 21:55:14 -05:00
Ayaan Zaidi
91ddf3857a fix: use talk.speak for Android replies (#60954) 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
e4fe853439 fix(android): fall back on legacy talk errors 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
dd6b160707 fix(android): tighten compressed talk playback 2026-04-05 08:20:47 +05:30
Michael Faath
628fc21192 Android: stop reply speaker on voice teardown 2026-04-05 08:20:47 +05:30
Michael Faath
5942b1062e Android: route voice replies through reply speaker 2026-04-05 08:20:47 +05:30
Michael Faath
b4f0e5ae2c Android: fix mic capture queue race 2026-04-05 08:20:47 +05:30
Michael Faath
a4ada035d8 Gateway: use runtime config for talk.speak 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
3f67a52d52 docs(talk): update android playback docs 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
aae3ab152a chore(protocol): regenerate swift talk models 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
db13a29bbf test(android): cover talk.speak playback helpers 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
98d5939564 feat(android): add talk.speak playback path 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
b558610ef3 fix(elevenlabs): pass talk latency override 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
823ce7957d fix(gateway): harden talk.speak responses 2026-04-05 08:20:47 +05:30
Peter Steinberger
fb580b551e fix: restore provider and config compatibility checks 2026-04-05 03:47:57 +01:00
Neerav Makwana
22175faaec fix: trim menu descriptions before dropping commands (#61129) (thanks @neeravmakwana)
* fix(telegram): trim menu descriptions before dropping commands

* fix: note Telegram command menu trimming (#61129) (thanks @neeravmakwana)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-05 08:05:16 +05:30
Vincent Koc
f217e6b72d fix(test-utils): include video generation providers 2026-04-05 11:09:51 +09:00
Vincent Koc
b56517b0ee refactor(providers): tighten family outlier contracts 2026-04-05 11:09:26 +09:00
scoootscooob
6ab1b43081 fix(dotenv): load gateway.env compatibility fallback (#61084)
* fix(dotenv): load gateway env fallback

* fix(dotenv): preserve legacy cli env loading

* fix(dotenv): keep gateway fallback scoped to default profile
2026-04-04 18:24:29 -07:00
scoootscooob
9860db5cea fix(memory): allow Gemini multimodal fallback before registry hydration (#61085)
* fix(memory): allow Gemini multimodal fallback

* docs(memory): clarify multimodal fallback
2026-04-04 18:24:20 -07:00
Andy Tien
dca21563c6 fix(cli): set non-zero exit code on argument errors (#60923)
Merged via squash.

Prepared head SHA: 0de0c43111
Co-authored-by: Linux2010 <35169750+Linux2010@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-05 03:17:51 +03:00
Altay
f299bb812b test(agents): stabilize announce cleanup assertions (#61088)
* test(plugin-sdk): use telegram public config seam

* test(agents): stabilize announce cleanup assertions
2026-04-05 03:09:28 +03:00
Altay
04b64e40d4 test(plugin-sdk): type telegram command config mock 2026-04-05 02:37:16 +03:00
Altay
2ba3484d10 fix(plugin-sdk): avoid telegram config import side effects (#61061)
* fix(plugin-sdk): avoid telegram config import side effects

* fix(plugin-sdk): address telegram contract review

* test(plugin-sdk): tighten telegram contract guards
2026-04-05 02:32:04 +03:00
Altay
d37e4a6c3a fix(contracts): align Teams guard and MiniMax loader (#61068) 2026-04-05 02:13:46 +03:00
Peter Steinberger
2781897d2c fix: restore provider policy fallbacks 2026-04-04 23:43:47 +01:00
Altay
2b3e89c6d4 fix(ci): remove anthropic auth parse error 2026-04-05 01:40:25 +03:00
Altay
ccc7549afe fix(ci): break facade runtime init cycle (#61053)
* fix(ci): break facade runtime init cycle

* style(config): normalize provider schema imports
2026-04-05 01:31:59 +03:00
Kim
fd71bc04ec fix(skills): unify runtime inclusion and available_skills exposure policy (#60852)
Merged via squash.

Prepared head SHA: 2b48b3a455
Co-authored-by: KimGLee <150593189+KimGLee@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-05 01:30:22 +03:00
coolramukaka-sys
70015be8b5 fix(msteams): replace deprecated HttpPlugin with httpServerAdapter (#60939)
Merged via squash.

Prepared head SHA: 7fe7f3c6bb
Co-authored-by: coolramukaka-sys <271658891+coolramukaka-sys@users.noreply.github.com>
Co-authored-by: BradGroux <3053586+BradGroux@users.noreply.github.com>
Reviewed-by: @BradGroux
2026-04-04 17:21:45 -05:00
Peter Steinberger
37301cbc3b docs: clarify anthropic extra usage billing 2026-04-05 07:14:35 +09:00
Peter Steinberger
b4216d197d fix: restore anthropic setup-token auth flow 2026-04-05 07:14:35 +09:00
Xi Qi
334c4be73e style(UI): improve mobile chat layout (#60220)
* UI: improve mobile chat layout

* change .chat-group-messages min-width: from 604 to 602

* UI: fix chat-group-messages overflow in split-view and mobile layouts

* UI: revert chat.css import order in styles.css and components.css

* UI: simplify mobile chat layout overrides in grouped.css

* ui: move .chat and .chat-thread styles to chat/layout.css

* fix: document mobile chat layout improvements

* fix: improve narrow mobile chat width

---------

Co-authored-by: Altay <altay@uinaf.dev>
2026-04-05 01:05:48 +03:00
zhuzm
9d7fe7cdd2 Control UI: use a dedicated loading style for the Cron refresh button (#60394)
Merged via squash.

Prepared head SHA: f7757b9e34
Co-authored-by: coder-zhuzm <63866641+coder-zhuzm@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-05 00:59:07 +03:00
bbddbb
96aea0a6d6 fix(ui): prevent overview access grid layout overlap on resize (#56924)
Merged via squash.

Prepared head SHA: ab327093fc
Co-authored-by: bbddbb1 <75060417+bbddbb1@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-05 00:39:25 +03:00
Hanna
8b06ca205a fix(avatar): check ui.assistant.avatar in resolveAvatarSource (#60778)
Merged via squash.

Prepared head SHA: df8d953a14
Co-authored-by: hannasdev <4538260+hannasdev@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-05 00:36:02 +03:00
Peter Steinberger
63cabcb524 test: stabilize forked gateway suites 2026-04-05 06:33:07 +09:00
Peter Steinberger
801b5d4afa fix: stabilize isolated gateway test runtime 2026-04-05 06:33:07 +09:00
Peter Steinberger
329fbc3f89 docs: refresh claude-cli migration cli mirror 2026-04-04 22:22:04 +01:00
Peter Steinberger
bc910942e2 docs: refresh history sanitization tag mirrors 2026-04-04 22:21:26 +01:00
Peter Steinberger
eee868452f docs: refresh claude-cli model ref mirrors 2026-04-04 22:19:07 +01:00
Peter Steinberger
896928d8c0 docs: refresh slack secretref status mirrors 2026-04-04 22:14:15 +01:00
Peter Steinberger
6de100d4e2 docs: refresh claude-cli naming mirrors 2026-04-04 22:11:45 +01:00
Peter Steinberger
8ea5b1ddc0 docs: refresh anthropic token compatibility mirrors 2026-04-04 22:09:21 +01:00
@zimeg
13b6a48991 fix(slack): import plugin secret input config 2026-04-04 14:09:00 -07:00
Peter Steinberger
66a0ab3752 docs: refresh anthropic auth mirror refs 2026-04-04 22:07:08 +01:00
Peter Steinberger
9eb3718438 docs: refresh token auth command mirrors 2026-04-04 22:05:05 +01:00
Peter Steinberger
5c5c82dfaa docs: refresh anthropic oauth defaults refs 2026-04-04 22:01:16 +01:00
Peter Steinberger
f14f7b9fde docs: refresh silent token guidance mirrors 2026-04-04 21:58:12 +01:00
Altay
0089eb28fa fix(pnpm-workspace): add acpx to minimumReleaseAgeExclude (#61032) 2026-04-04 23:57:34 +03:00
Peter Steinberger
102f7f34e1 docs: refresh silent token semantics mirrors 2026-04-04 21:56:30 +01:00
Peter Steinberger
3d65b14019 docs: refresh NO_REPLY history mirrors 2026-04-04 21:55:11 +01:00
Peter Steinberger
adfdde5cb3 docs: refresh chat history sanitization mirrors 2026-04-04 21:53:25 +01:00
Peter Steinberger
291afbbb95 docs: refresh transcript sanitization mirrors 2026-04-04 21:52:15 +01:00
Mulualem
de918c282c fix(ui): tighten cron row overflow constraints 2026-04-04 15:50:03 -05:00
Mulualem
69cc35c9bd fix(ui): constrain cron job entries within list boundary 2026-04-04 15:50:03 -05:00
Peter Steinberger
b83c5fb8e0 docs: refresh provider runtime fallback refs 2026-04-04 21:48:41 +01:00
Peter Steinberger
38e54f488a docs: refresh native approval ui mirrors 2026-04-04 21:44:30 +01:00
Peter Steinberger
4f9804ec24 docs: refresh config schema and gateway tool mirrors 2026-04-04 21:43:09 +01:00
Peter Steinberger
746a57a2af docs: refresh sdk provider hook inventory refs 2026-04-04 21:39:18 +01:00
Peter Steinberger
93f11ff9f7 docs: refresh provider hook inventory coverage refs 2026-04-04 21:38:01 +01:00
Peter Steinberger
73d50fba28 docs: refresh provider hook inventory refs 2026-04-04 21:33:31 +01:00
Peter Steinberger
0fb53f1b90 docs: refresh provider transport and synthetic auth refs 2026-04-04 21:31:50 +01:00
Peter Steinberger
dd5439dd5b docs: refresh tool catalog mirrors 2026-04-04 21:28:05 +01:00
Peter Steinberger
4324eac5e9 docs: refresh config schema metadata mirrors 2026-04-04 21:26:51 +01:00
Peter Steinberger
849dbc58b1 docs: refresh gateway overview mirrors 2026-04-04 21:25:40 +01:00
Peter Steinberger
4b5146921c docs: refresh bridge removal mirrors 2026-04-04 21:24:09 +01:00
Peter Steinberger
79e8edc7bd docs: expand gateway protocol method and event refs 2026-04-04 21:20:11 +01:00
Peter Steinberger
8bc59eceb7 docs: refresh gateway feature registry mirrors 2026-04-04 21:18:02 +01:00
Chinar Amrutkar
e419989c34 docs: add PR limits to contribution guide (#60910)
Add PR limits section explaining:
- 10 open PRs per author cap
- r: too-many-prs label auto-close mechanism
- How to get exception via #clawtributors Discord

Fixes: #38283
2026-04-05 05:17:10 +09:00
@zimeg
28e1142a24 revert(slack): use packaged thread status method 2026-04-04 13:15:57 -07:00
@zimeg
68b84980cc refactor(slack): use packaged thread status method 2026-04-04 13:14:06 -07:00
Peter Steinberger
b60eee6017 docs: refresh memory status summary refs 2026-04-04 21:13:49 +01:00
Peter Steinberger
e2b841d7d0 docs: refresh shared-secret default mirrors 2026-04-04 21:11:16 +01:00
Peter Steinberger
0738ed8d19 docs: refresh control-ui shared-secret mirrors 2026-04-04 21:05:12 +01:00
Peter Steinberger
90387d4a88 docs: refresh hosted shared-secret auth mirrors 2026-04-04 21:04:17 +01:00
Peter Steinberger
7678917c49 docs: refresh exposed bind auth mirrors 2026-04-04 21:01:34 +01:00
Peter Steinberger
1ae356c40c docs: refresh sandbox bind security refs 2026-04-04 20:57:37 +01:00
@zimeg
86aa24b7a5 docs(slack): move typing status indicator to reaction fallback 2026-04-04 12:56:54 -07:00
Peter Steinberger
3b4bed7c38 docs: refresh model-scoped cooldown mirrors 2026-04-04 20:54:05 +01:00
Peter Steinberger
59a6bf7569 docs: refresh auth probe reason-code refs 2026-04-04 20:51:43 +01:00
Peter Steinberger
136a5ad2eb docs: refresh background process tool refs 2026-04-04 20:49:15 +01:00
Peter Steinberger
4b993ba6e4 docs: refresh config schema mirror refs 2026-04-04 20:45:46 +01:00
Peter Steinberger
e336300e60 docs: refresh failover and compaction pattern refs 2026-04-04 20:43:58 +01:00
Peter Steinberger
97a587ddca docs: refresh qwen auth-choice mirrors 2026-04-04 20:40:31 +01:00
Peter Steinberger
0ef29325ed docs: refresh config schema mirror refs 2026-04-04 20:38:15 +01:00
Peter Steinberger
420f2191f5 docs: refresh whatsapp helper sdk refs 2026-04-04 20:36:30 +01:00
Peter Steinberger
ba8eb4af38 docs: refresh config schema output refs 2026-04-04 20:35:01 +01:00
Peter Steinberger
976bc47458 docs: refresh gateway rpc safe-flow mirrors 2026-04-04 20:32:28 +01:00
Peter Steinberger
13396fa99e docs: refresh gateway tool safe-edit mirrors 2026-04-04 20:31:34 +01:00
Peter Steinberger
2bd91a0f02 docs: refresh browser existing-session mirror refs 2026-04-04 20:29:31 +01:00
Peter Steinberger
8efb0801a0 docs: refresh browser existing-session limit refs 2026-04-04 20:26:30 +01:00
Peter Steinberger
bbdc429dcb docs: refresh voice-call streaming transcription refs 2026-04-04 20:23:28 +01:00
Peter Steinberger
46cb292c2a docs: refresh Firecrawl and web_fetch config refs 2026-04-04 20:21:16 +01:00
Peter Steinberger
33f8ca6cb0 docs: refresh reserved sdk helper refs 2026-04-04 20:17:52 +01:00
Peter Steinberger
8eb1ea5b2e docs: refresh config schema mirrors 2026-04-04 20:14:33 +01:00
@zimeg
c2027d9de2 docs(slack): remove text streaming scope requirements 2026-04-04 12:13:25 -07:00
Peter Steinberger
4453b51e2c docs: refresh session tool inventory refs 2026-04-04 20:10:44 +01:00
Hiroshi Tanaka
3f1b369f4a feat(config): add rich description fields to JSON Schema output [AI-assisted] (#60067)
Merged via squash.

Prepared head SHA: a98b971924
Co-authored-by: solavrc <145330217+solavrc@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-04 22:10:08 +03:00
Peter Steinberger
92aed3168a docs: refresh core tool catalog mirrors 2026-04-04 20:09:24 +01:00
Peter Steinberger
da8a4131fe docs: refresh manifest contract examples 2026-04-04 20:07:01 +01:00
Peter Steinberger
ccd45bd9f0 fix(agents): refresh runtime tool and subagent coverage 2026-04-04 20:06:32 +01:00
Peter Steinberger
496df07804 fix(extensions): align provider helper surfaces 2026-04-04 20:06:32 +01:00
Peter Steinberger
6d5e2c7e6b docs: refresh legacy manifest contract refs 2026-04-04 20:04:11 +01:00
Peter Steinberger
c488becf43 docs: refresh plugin overview mirrors 2026-04-04 20:03:17 +01:00
Peter Steinberger
67d6fc8847 chore(plugins): sync versions to 2026.4.4 2026-04-04 20:03:01 +01:00
Peter Steinberger
7c1c4daa4e docs: refresh realtime transcription capability refs 2026-04-04 20:02:14 +01:00
Peter Steinberger
7988b5962a docs: refresh plugin capability inventory refs 2026-04-04 20:01:19 +01:00
Peter Steinberger
43acbcd283 docs: refresh plugin capability registration refs 2026-04-04 19:59:50 +01:00
Peter Steinberger
36b64969bc docs: refresh qwen sdk helper refs 2026-04-04 19:58:29 +01:00
Peter Steinberger
e0ef3855ca docs: refresh video generation config refs 2026-04-04 19:56:54 +01:00
Peter Steinberger
62dd299af1 docs: refresh qwen video generation refs 2026-04-04 19:55:39 +01:00
Peter Steinberger
28946635aa docs: refresh provider runtime hook refs 2026-04-04 19:52:56 +01:00
Peter Steinberger
4650b972b9 docs: refresh provider scoped failover refs 2026-04-04 19:49:54 +01:00
Peter Steinberger
a29755615e docs: refresh configured provider fallback refs 2026-04-04 19:48:46 +01:00
Peter Steinberger
3b47c0af28 docs: refresh video generation plugin refs 2026-04-04 19:47:30 +01:00
Peter Steinberger
c3ee8c611d docs: refresh video generation sdk refs 2026-04-04 19:45:59 +01:00
Peter Steinberger
879d45a56c docs: refresh qwen media and config refs 2026-04-04 19:42:13 +01:00
Peter Steinberger
b1279b0db3 docs: refresh untrusted file wrapper refs 2026-04-04 19:39:09 +01:00
Peter Steinberger
eaef4ee1b1 docs: refresh heartbeat skip-reason refs 2026-04-04 19:36:34 +01:00
Peter Steinberger
ca200eb480 fix(providers): stabilize runtime normalization hooks 2026-04-04 19:34:56 +01:00
Peter Steinberger
e06e36d41a docs(qwen): fix front matter formatting 2026-04-04 19:34:56 +01:00
Peter Steinberger
889ddb5edf docs(qwen): refresh provider and endpoint guides 2026-04-04 19:34:56 +01:00
Peter Steinberger
df7693027c style(qwen): apply formatter follow-ups 2026-04-04 19:34:56 +01:00
Peter Steinberger
e3ac0f43df feat(qwen): add qwen provider and video generation 2026-04-04 19:34:56 +01:00
Peter Steinberger
759373e887 docs: refresh multi-agent sandbox recall mirror 2026-04-04 19:33:48 +01:00
Peter Steinberger
d0d57ea435 docs: refresh session recall sanitization mirrors 2026-04-04 19:33:13 +01:00
Peter Steinberger
9aec55f0a2 docs: refresh text runtime sdk refs 2026-04-04 19:31:32 +01:00
Peter Steinberger
c329dd8250 docs: refresh subagent completion fallback refs 2026-04-04 19:29:58 +01:00
Peter Steinberger
f94645dfe5 docs: refresh session recall sanitization refs 2026-04-04 19:26:37 +01:00
Peter Steinberger
fd222d3f07 docs: refresh chat history scaffolding refs 2026-04-04 19:23:55 +01:00
Peter Steinberger
3b109c3419 docs: refresh push-based orchestration refs 2026-04-04 19:22:39 +01:00
Peter Steinberger
e42deea653 docs: refresh sessions_history safety refs 2026-04-04 19:20:34 +01:00
Peter Steinberger
39d9ded2e5 docs: refresh chat history display mirrors 2026-04-04 19:17:58 +01:00
Vincent Koc
6f2e804182 fix(agents): prefer background completion wake over polling (#60877)
* fix(agents): prefer completion wake over polling

* fix(changelog): note completion wake guidance

* fix(agents): qualify quiet exec completion wake

* fix(agents): qualify disabled exec completion wake

* fix(agents): split process polling from control actions
2026-04-05 03:17:10 +09:00
Peter Steinberger
0c3ec064f1 docs: refresh OpenResponses file input refs 2026-04-04 19:13:44 +01:00
Peter Steinberger
852d3a742c docs: refresh gateway probe warning mirrors 2026-04-04 19:10:31 +01:00
Peter Steinberger
3bf538d720 docs: refresh gateway status deep mirrors 2026-04-04 19:06:30 +01:00
Peter Steinberger
f3ce1bdb4f docs: refresh platform discovery mirrors 2026-04-04 19:03:20 +01:00
Peter Steinberger
40f958a953 fix(ci): narrow runtime seams and partial mocks 2026-04-04 19:03:00 +01:00
Peter Steinberger
83fe8efe3d fix(test): isolate ollama runtime test seams 2026-04-04 19:03:00 +01:00
oliviareid-svg
7ff90c516a fix: strip leaked outbound tool-call scaffolding (#60619)
Co-authored-by: Frank Yang <frank.ekn@gmail.com>
2026-04-05 02:02:36 +08:00
Peter Steinberger
0cf9c6ec95 docs: refresh discovery TXT mode refs 2026-04-04 19:01:18 +01:00
Peter Steinberger
e6f054ac76 docs: refresh gateway probe and discovery refs 2026-04-04 19:00:09 +01:00
Peter Steinberger
ac5d1de13a docs: refresh status deep health mirrors 2026-04-04 18:56:46 +01:00
Peter Steinberger
65bb1e772b docs: refresh remote gateway ssh mirrors 2026-04-04 18:56:08 +01:00
Mason
09016db731 fix: wrap untrusted file inputs (#60277)
Merged via squash.

Prepared head SHA: 56ce545786
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-04-05 01:54:48 +08:00
Peter Steinberger
9d45f4b4e9 docs: refresh gateway health snapshot refs 2026-04-04 18:54:04 +01:00
Peter Steinberger
72b59231a3 docs: refresh channels status probe mirrors 2026-04-04 18:52:01 +01:00
Peter Steinberger
6d89b363a2 docs: refresh setup-code bootstrap scope mirrors 2026-04-04 18:48:26 +01:00
Peter Steinberger
a10ba044bc docs: refresh approval error helper refs 2026-04-04 18:47:15 +01:00
Peter Steinberger
8fd53cdf86 docs: refresh bootstrap scope role-prefix refs 2026-04-04 18:46:30 +01:00
Peter Steinberger
131a78d3f3 docs: refresh setup runtime helper refs 2026-04-04 18:45:12 +01:00
Peter Steinberger
2b548aa2b1 docs: refresh elevated config mirror refs 2026-04-04 18:40:14 +01:00
Peter Steinberger
4db910698a docs: refresh sandbox and security elevated refs 2026-04-04 18:39:12 +01:00
Peter Steinberger
f1d8786a96 docs: refresh exec host and elevated refs 2026-04-04 18:38:10 +01:00
Peter Steinberger
9fbbdc62c8 docs: refresh shared native approval auto-enable refs 2026-04-04 18:34:29 +01:00
Peter Steinberger
4154aa8b0f docs: refresh discord native approval approver refs 2026-04-04 18:33:39 +01:00
Peter Steinberger
414e834c26 docs: refresh matrix and slack native approval refs 2026-04-04 18:31:47 +01:00
Peter Steinberger
f81d55d7ea docs: refresh native approval routing refs 2026-04-04 18:28:23 +01:00
Tak Hoffman
3bf1b69ece CI: make bad-barnacle bypass PR auto-response 2026-04-04 12:28:03 -05:00
Peter Steinberger
a08449b83f docs: refresh approval fallback refs 2026-04-04 18:27:27 +01:00
Peter Steinberger
2a80b7f30b docs: refresh silent cron delivery refs 2026-04-04 18:26:28 +01:00
Peter Steinberger
2ab8acb2c9 docs: refresh chat thinking and compaction refs 2026-04-04 18:25:13 +01:00
Gustavo Madeira Santana
e627f53d24 core: dedupe approval not-found handling (#60932)
Merged via squash.

Prepared head SHA: 108221fdfe
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-04 13:23:58 -04:00
Ayaan Zaidi
ef7c84ae92 style: trim live model switch comment noise 2026-04-04 22:42:30 +05:30
Ayaan Zaidi
e4bd4b8b49 style(agents): trim exec routing comments 2026-04-04 22:41:22 +05:30
Ayaan Zaidi
0817bf446f fix: keep NO_REPLY detection case-insensitive 2026-04-04 22:38:59 +05:30
Ayaan Zaidi
cde1e2d3a1 fix: preserve compaction split after trailing tool results 2026-04-04 22:34:05 +05:30
Ayaan Zaidi
3f7bd3bd7b fix: split before unfinished compaction tool turns 2026-04-04 22:30:27 +05:30
Tak Hoffman
3017a71bb7 ui: add chat thinking selector 2026-04-04 11:51:45 -05:00
wangchunyue
f463256660 fix: suppress NO_REPLY direct cron leaks (#45737) (thanks @openperf)
* fix(cron): suppress NO_REPLY sentinel in direct delivery path

* fix: set deliveryAttempted on filtered NO_REPLY to prevent timer fallback

* fix: mark silent NO_REPLY direct deliveries as delivered

* fix(cron): unify silent direct delivery handling

* fix: suppress NO_REPLY direct cron leaks (#45737) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 22:16:20 +05:30
wangchunyue
08992e1dbc fix: keep tool calls paired during compaction (#58849) (thanks @openperf)
* fix(compaction): keep tool_use and toolResult together when splitting messages

* fix: keep displaced tool results in compaction chunks

* fix: keep tool calls paired during compaction (#58849) (thanks @openperf)

* fix: avoid stalled compaction splits on aborted tool calls (#58849) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 22:12:43 +05:30
Ayaan Zaidi
77509024b8 fix: restore exec host=node routing (#60788) (thanks @openperf) 2026-04-04 21:59:57 +05:30
openperf
d98eaba4c3 fix(agents): resolve exec host=node routing regression and elevated gateway override 2026-04-04 21:59:57 +05:30
wangchunyue
17f086c021 fix: handle subagent live model switches (#58178) (thanks @openperf)
* fix(agents): handle LiveSessionModelSwitchError in subagent execution

Add retry loop for cross-provider model switches in the subagent
command path, mirroring the existing logic in agent-runner-execution.ts.

- Wrap runWithModelFallback in a while(true) loop inside agentCommandInternal
- Catch LiveSessionModelSwitchError and update provider, model,
  fallbackProvider, fallbackModel, providerForAuthProfileValidation,
  sessionEntry.authProfileOverride, and storedModelOverride before retrying
- Guard storedModelOverride update: only set when the model genuinely
  changed (compared before mutation) or a session override already existed
- Reset lifecycleEnded flag so the retried iteration can emit lifecycle events
- Add comprehensive tests covering retry success, error propagation,
  lifecycle reset, auth-profile forwarding, and fallback override state

Fixes #57998

* fix(agents): include provider change in storedModelOverride guard

* fix(agents): validate allowlist and clear stale compaction count on live model switch

* fix(agents): remove broken allowlist guard on live model switch

* fix(agents): address security review — bound retry loop, validate allowlist, redact error in lifecycle events

* fix(agents): restore error observability in lifecycle events using err.message

* fix(agents): sanitize log inputs and shallow-copy sessionEntry on live model switch

* fix(agents): enforce allowlist on empty set and sanitize error message

* fix: handle subagent live model switches (#58178) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 21:56:11 +05:30
@zimeg
beee44ba47 docs(slack): reorder sections of introduced concepts 2026-04-04 08:54:31 -07:00
Onur
7de3a16ab4 ACPX: bump pinned version to 0.4.1 (#60918)
* ACPX: bump pinned version to 0.4.1

* ACPX: refresh lockfile for 0.4.1
2026-04-04 17:37:17 +02:00
Altay
ae460eff84 fix(failover): scope openrouter-specific matchers (#60909) 2026-04-04 18:24:03 +03:00
Peter Steinberger
fba6e194bd docs: refresh provider stream export refs 2026-04-04 16:23:00 +01:00
Peter Steinberger
c4205c7aae docs: refresh provider stream family refs 2026-04-04 16:21:21 +01:00
Peter Steinberger
a7b1a3140f docs: refresh skills cli stream refs 2026-04-04 16:19:34 +01:00
Peter Steinberger
bcaff8c208 docs: refresh failover generic error refs 2026-04-04 16:18:07 +01:00
Peter Steinberger
6067fe59d8 docs: refresh mcp config refs 2026-04-04 16:15:11 +01:00
Peter Steinberger
89535f9313 docs: refresh pairing locality refs 2026-04-04 16:13:04 +01:00
Aaron Zhu
983909f826 fix(agents): classify generic provider errors for failover (#59325)
* fix(agents): classify generic provider errors for failover

Anthropic returns bare 'An unknown error occurred' during API instability
and OpenRouter wraps upstream failures as 'Provider returned error'. Neither
message was recognized by the failover classifier, so the error surfaced
directly to users instead of triggering the configured fallback chain.

Add both patterns to the serverError classifier so they are classified as
transient server errors (timeout) and trigger model failover.

Closes #49706
Closes #45834

* fix(agents): scope unknown-error failover by provider

* docs(changelog): note provider-scoped unknown-error failover

---------

Co-authored-by: Aaron Zhu <aaron@Aarons-MacBook-Air.local>
Co-authored-by: Altay <altay@uinaf.dev>
2026-04-04 18:11:46 +03:00
Peter Steinberger
8a6da9d488 docs: refresh gateway auth handshake refs 2026-04-04 16:09:53 +01:00
Altay
5012b52780 fix(cli): route skills list output to stdout when --json is active (#60914)
* fix(cli): route skills list output to stdout when --json is active

runSkillsAction used defaultRuntime.log() which goes through console.log.
The --json preAction hook calls routeLogsToStderr(), redirecting console.log
to stderr. Switch to defaultRuntime.writeStdout() which writes directly to
process.stdout, consistent with how other --json commands (e.g. skills search)
already emit their output.

Fixes #57599

* test(cli): add skills JSON stdout regression coverage

* test(cli): refine skills CLI stream coverage

* fix(cli): add changelog entry for skills JSON stdout fix

---------

Co-authored-by: Aftabbs <aftabbs.wwe@gmail.com>
2026-04-04 18:09:44 +03:00
Peter Steinberger
db0b514e45 docs: refresh typebox protocol samples 2026-04-04 16:06:40 +01:00
Peter Steinberger
bc21e3c83d docs: refresh typebox protocol registry refs 2026-04-04 16:04:25 +01:00
Peter Steinberger
3470a80b36 docs: expand gateway protocol method inventory 2026-04-04 16:02:21 +01:00
Peter Steinberger
beb3740bb7 docs: expand gateway protocol rpc refs 2026-04-04 16:01:15 +01:00
Peter Steinberger
b944da561c docs: refresh sdk inventory refs 2026-04-04 15:57:06 +01:00
Peter Steinberger
5633495c19 docs: refresh provider sdk family refs 2026-04-04 15:53:24 +01:00
Peter Steinberger
b3cfedf312 docs: refresh registration mode mirror refs 2026-04-04 15:48:27 +01:00
Peter Steinberger
4c6b7a3a77 docs: refresh setup entrypoint import refs 2026-04-04 15:47:26 +01:00
Peter Steinberger
eb4c5890ab docs: refresh optional setup helper refs 2026-04-04 15:45:28 +01:00
Peter Steinberger
3b502882b9 docs: refresh setup runtime and promotion refs 2026-04-04 15:43:34 +01:00
Peter Steinberger
226b12d7b5 docs: refresh provider tool compat refs 2026-04-04 15:39:17 +01:00
Peter Steinberger
4dbc66b1ed fix: remove bundled channel startup reentry 2026-04-04 15:39:12 +01:00
Peter Steinberger
b9201e8333 refactor: share announce test runtime seams 2026-04-04 23:38:36 +09:00
Peter Steinberger
5584af7ac3 docs: refresh proxy provider runtime refs 2026-04-04 15:37:20 +01:00
Peter Steinberger
f5cc6a101b style: reflow system prompt tool summary 2026-04-04 23:36:46 +09:00
Peter Steinberger
a4fc1200de style: normalize provider formatting 2026-04-04 23:36:46 +09:00
Peter Steinberger
1ca1ce85ee docs: refresh xai and zai provider refs 2026-04-04 15:34:57 +01:00
Peter Steinberger
de001d0e07 docs: refresh subagent completion delivery refs 2026-04-04 15:32:45 +01:00
Vincent Koc
1f2e068e6b test(providers): require plugin-boundary family coverage 2026-04-04 23:30:28 +09:00
Peter Steinberger
d06633c618 docs: refresh device management authz refs 2026-04-04 15:28:36 +01:00
Peter Steinberger
3dda70a578 docs: refresh gemini cli oauth setup refs 2026-04-04 15:27:42 +01:00
Peter Steinberger
fb8e20ddb6 fix: harden paired-device management authz (#50627) (thanks @coygeek) 2026-04-04 23:27:05 +09:00
Peter Steinberger
9ac9edff43 docs: refresh gateway operator scope refs 2026-04-04 15:25:57 +01:00
Vincent Koc
cb1c2e8f86 test(providers): cover xai and zai stream hooks 2026-04-04 23:24:18 +09:00
Vincent Koc
e277c01953 test(providers): cover openrouter replay family 2026-04-04 23:23:02 +09:00
hugh.li
9dd449045a fix(google-gemini-cli-auth): fix Gemini CLI OAuth failures on Windows (#40729)
* fix(google-gemini-cli-auth): fix Gemini CLI OAuth failures on Windows

Two issues prevented Gemini CLI OAuth from working on Windows:

1. resolveGeminiCliDirs: the first candidate `dirname(dirname(resolvedPath))`
   can resolve to an unrelated ancestor directory (e.g. the nvm root
   `C:\Users\<user>\AppData\Local\nvm`) when gemini is installed via nvm.
   The subsequent `findFile` recursive search (depth 10) then picks up an
   `oauth2.js` from a completely different package (e.g.
   `discord-api-types/payloads/v10/oauth2.js`), which naturally does not
   contain Google OAuth credentials, causing silent extraction failure.

   Fix: validate candidate directories before including them — only keep
   candidates that contain a `package.json` or a `node_modules/@google/
   gemini-cli-core` subdirectory.

2. resolvePlatform: returns "WINDOWS" on win32, but Google's loadCodeAssist
   API rejects it as an invalid Platform enum value (400 INVALID_ARGUMENT),
   just like it rejects "LINUX".

   Fix: use "PLATFORM_UNSPECIFIED" for all non-macOS platforms.

* test(google-gemini-cli-auth): keep oauth regressions portable

* chore(changelog): add google gemini cli auth fix note

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-04 23:22:36 +09:00
Peter Steinberger
eddb94555a docs: refresh heartbeat task batching refs 2026-04-04 15:22:01 +01:00
Vincent Koc
3f9e93fd28 test(providers): cover opencode replay family hooks 2026-04-04 23:21:41 +09:00
Joe LaPenna
bb82fe8f19 fix: constrain device bootstrap scope checks by role prefix (#57258) (thanks @jlapenna) (#57258)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-04 23:21:01 +09:00
Vincent Koc
a2e0a094c1 test(providers): cover stream family plugin hooks 2026-04-04 23:20:28 +09:00
Vincent Koc
fa34f3a9d5 fix(ci): restore provider runtime seams 2026-04-04 23:19:23 +09:00
Peter Steinberger
c09e128587 fix(gateway): include talk secrets in CLI pairing defaults (#56481) (thanks @maxpetrusenko) 2026-04-04 23:18:54 +09:00
Max P
8262078ee5 fix(agents): inherit completion announce delivery target (#56481) 2026-04-04 23:18:54 +09:00
Vincent Koc
4fe21de3ce test(providers): cover xai tool compat seam 2026-04-04 23:18:31 +09:00
Vincent Koc
20d14745cf refactor(providers): flatten passthrough provider hooks 2026-04-04 23:16:53 +09:00
Peter Steinberger
ea2f56b4e8 docs: refresh bundled channel naming mirrors 2026-04-04 15:16:11 +01:00
Vincent Koc
1e7f9e8746 test(providers): cover transport family matrix 2026-04-04 23:14:02 +09:00
Peter Steinberger
4be01a5cd5 docs: refresh onboarding channel mirrors 2026-04-04 15:13:14 +01:00
Peter Steinberger
772ee1f81f docs: refresh bundled channel ownership refs 2026-04-04 15:11:20 +01:00
Peter Steinberger
b7e6a3bc9e ci: retrigger workflow shell retry 34 2026-04-04 15:09:50 +01:00
Peter Steinberger
edc51b1fa4 ci: retrigger workflow shell retry 33 2026-04-04 15:09:50 +01:00
Peter Steinberger
6ce96c273f ci: retrigger workflow shell retry 32 2026-04-04 15:09:50 +01:00
Peter Steinberger
37a3b6b25f ci: retrigger workflow shell retry 31 2026-04-04 15:09:50 +01:00
Peter Steinberger
b7296fe5dd ci: retrigger workflow shell retry 30 2026-04-04 15:09:50 +01:00
Peter Steinberger
e191bf36a5 ci: retrigger workflow shell retry 29 2026-04-04 15:09:50 +01:00
Peter Steinberger
9766da7f00 ci: retrigger workflow shell retry 28 2026-04-04 15:09:50 +01:00
Peter Steinberger
e509c5c3ea fix(ci): avoid readonly embedded session mutation 2026-04-04 15:09:50 +01:00
Peter Steinberger
93fe3c5442 ci: retrigger workflow shell retry 27 2026-04-04 15:09:50 +01:00
Peter Steinberger
e949bd7d04 ci: retrigger workflow shell retry 26 2026-04-04 15:09:50 +01:00
Peter Steinberger
a29abebee0 ci: retrigger workflow shell retry 25 2026-04-04 15:09:50 +01:00
Peter Steinberger
dbeab5e60f ci: retrigger workflow shell retry 24 2026-04-04 15:09:50 +01:00
Peter Steinberger
99ebb7a248 ci: retrigger workflow shell retry 23 2026-04-04 15:09:50 +01:00
Peter Steinberger
788cff6759 ci: retrigger workflow shell retry 22 2026-04-04 15:09:50 +01:00
Peter Steinberger
0a69b3558a fix(build): stabilize lazy runtime entrypoints 2026-04-04 15:09:50 +01:00
Peter Steinberger
e5b9e32979 ci: retrigger workflow shell retry 21 2026-04-04 15:09:49 +01:00
Peter Steinberger
fe0b209850 ci: retrigger workflow shell retry 20 2026-04-04 15:09:49 +01:00
Peter Steinberger
8dc049abc5 ci: retrigger workflow shell retry 19 2026-04-04 15:09:49 +01:00
Peter Steinberger
6b265ce415 ci: retrigger workflow shell retry 18 2026-04-04 15:09:49 +01:00
Peter Steinberger
470b4452ce fix(ci): drop stale browser runtime imports 2026-04-04 15:09:49 +01:00
Peter Steinberger
5ef3bdb5f4 ci: retrigger workflow shell retry 17 2026-04-04 15:09:49 +01:00
Peter Steinberger
fb59b5c461 fix(ci): sync openrouter stream hook seams 2026-04-04 15:09:49 +01:00
Peter Steinberger
b575dc704c ci: retrigger workflow shell retry 16 2026-04-04 15:09:49 +01:00
Peter Steinberger
a0dbdbd8d4 ci: retrigger workflow shell retry 15 2026-04-04 15:09:49 +01:00
Peter Steinberger
571cd92b22 ci: retrigger workflow shell retry 14 2026-04-04 15:09:49 +01:00
Peter Steinberger
5a6a2bb861 ci: retrigger workflow shell retry 13 2026-04-04 15:09:49 +01:00
Peter Steinberger
5a3062ffb9 ci: retrigger workflow shell retry 12 2026-04-04 15:09:49 +01:00
Peter Steinberger
e0e6eaa03c ci: retrigger workflow shell retry 11 2026-04-04 15:09:49 +01:00
Peter Steinberger
867402449f ci: retrigger workflow shell retry 10 2026-04-04 15:09:49 +01:00
Peter Steinberger
e1ea02e556 ci: retrigger workflow shell retry 9 2026-04-04 15:09:49 +01:00
Peter Steinberger
d2ff8e28dd ci: retrigger workflow shell retry 8 2026-04-04 15:09:49 +01:00
Peter Steinberger
671c724626 ci: retrigger workflow shell retry 7 2026-04-04 15:09:49 +01:00
Peter Steinberger
cad662196f ci: retrigger workflow shell retry 6 2026-04-04 15:09:49 +01:00
Peter Steinberger
35260d3443 ci: retrigger workflow shell retry 5 2026-04-04 15:09:49 +01:00
Peter Steinberger
a1b794a12c fix(ci): repair node test regressions 2026-04-04 15:09:49 +01:00
Peter Steinberger
41513eaf2b ci: retrigger workflow shell retry 4 2026-04-04 15:09:49 +01:00
Peter Steinberger
7b8c4335b3 ci: retrigger workflow shell retry 3 2026-04-04 15:09:49 +01:00
Peter Steinberger
95480863f3 ci: retrigger workflow shell retry 2 2026-04-04 15:09:49 +01:00
Peter Steinberger
d0e041ad5c ci: retrigger workflow shell retry 2026-04-04 15:09:49 +01:00
Peter Steinberger
2ea583496d ci: retrigger workflow shell another time 2026-04-04 15:09:49 +01:00
Peter Steinberger
9e596e383d ci: retrigger workflow shell again again 2026-04-04 15:09:49 +01:00
Peter Steinberger
f81e31b23e ci: retrigger workflow shell once more 2026-04-04 15:09:49 +01:00
Peter Steinberger
5f8ae068dc ci: retrigger workflow shell again 2026-04-04 15:09:49 +01:00
Peter Steinberger
cad18b5ec2 ci: retrigger workflow shell 2026-04-04 15:09:48 +01:00
Peter Steinberger
dd771f1dc6 fix(ci): repair plugin boundary and bootstrap regressions 2026-04-04 15:09:48 +01:00
Peter Steinberger
a5836343df fix(ci): guard anthropic cli backend registration 2026-04-04 15:09:48 +01:00
Peter Steinberger
73f0b11a88 ci: retrigger workflow shell again 2026-04-04 15:09:48 +01:00
Peter Steinberger
daf4eea943 ci: retrigger stuck workflow shell 2026-04-04 15:09:48 +01:00
Peter Steinberger
2c6c2d4907 ci: retrigger stuck workflow 2026-04-04 15:09:48 +01:00
Peter Steinberger
2a0d5f9094 fix(ci): remove duplicated heartbeat prompt setup 2026-04-04 15:09:48 +01:00
Peter Steinberger
c5c5c77ebb fix(ci): restore contract-safe core imports 2026-04-04 15:09:48 +01:00
Chinar Amrutkar
8cf20a0c59 fix(heartbeat): address review comments 3035416659, 3035425446, 3035425447
- sessionId: derive valid ID from sessionKey (replace : with _)
- Move prompt null check before isolated session setup to avoid churn
- Improve tasks block stripping regex to handle blank lines

Fixes: #3035416659, #3035425446, #3035425447
2026-04-04 15:09:48 +01:00
Peter Steinberger
5c32dddb1c fix(ci): restore heartbeat task batching checks 2026-04-04 15:09:48 +01:00
Chinar Amrutkar
e0634aab66 fix(heartbeat): update task timestamps on alerts-disabled exit
Fixes: #3034825973
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
dbfb0b5618 fix(heartbeat): prevent outer loop from exiting on task field lines
The YAML parser's outer loop was exiting the tasks block when it
encountered 'interval:' or 'prompt:' lines, causing only the first
task to be parsed. Added isTaskField check to skip those lines.

Fixes: #3034790131
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
05c948e4de fix(heartbeat): preserve HEARTBEAT.md directives in task-mode prompt
Pass heartbeatFileContent to resolveHeartbeatRunPrompt and append
non-task directives from HEARTBEAT.md to the task-mode prompt.

Fixes: #3033850983
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
cebea1bf95 fix(heartbeat): remove dead helpers, persist timestamps on all exits
- Remove unused getTaskLastRunMs/updateTaskLastRunMs functions
- Add timestamp updates to all successful exit paths

Fixes: #3030557564, #3034645588
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
5fffdc478e fix(heartbeat): add startedAt param, null prompt handling, timestamp updates
- Fix: Pass startedAt into resolveHeartbeatRunPrompt
- Fix: Return proper object instead of null for no-tasks-due
- Fix: Add early return when prompt is null
- Fix: Persist timestamps on successful exits
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
ba09426707 fix(heartbeat): address review comments - parsing, timing, state, skips
- Fix YAML parsing to capture interval:/prompt: before breaking
- Record task timestamps AFTER successful execution (not before)
- Initialize task state on first run (handle undefined session)
- Skip API call when no tasks due (return null)
- Use startedAt consistently for due-task filtering

Fixes: #3030568439, #3033833124, #3030570872, #3030568408, #3030570872, #3035434022, #3035434368
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
728d14e918 fix: add heartbeatTaskState to SessionEntry type
The heartbeat task batching feature uses heartbeatTaskState to track
last run times for periodic tasks, but this property was missing
from the SessionEntry type, causing TypeScript compilation errors.
2026-04-04 15:09:47 +01:00
Chinar Amrutkar
103bebd651 feat(heartbeat): add task batching support via HEARTBEAT.md
- Add parseHeartbeatTasks() to parse YAML-like task definitions
- Add isTaskDue() to check if task interval has elapsed
- Add heartbeatTaskState to session store for tracking last run times
- Modify resolveHeartbeatRunPrompt to build batched prompts for due tasks
- Update task last run times after successful heartbeat execution

Implements openclaw#29570
2026-04-04 15:09:47 +01:00
Peter Steinberger
890de57036 docs: refresh failover billing refs 2026-04-04 15:09:05 +01:00
Peter Steinberger
5fa60e6535 docs: refresh channel overview mirrors 2026-04-04 15:07:32 +01:00
Peter Steinberger
fde6e07f2a docs: refresh bundled channel setup refs 2026-04-04 15:06:39 +01:00
Peter Steinberger
1a431a532b docs: refresh bundled channel mirrors 2026-04-04 15:05:02 +01:00
Rockcent
b2f972e364 fix(failover): OpenRouter 403 Key limit exceeded triggers billing fallback (#59892)
Merged via squash.

Prepared head SHA: 7f8265231c
Co-authored-by: rockcent <128210877+rockcent@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-04 17:03:21 +03:00
Peter Steinberger
11542e9310 docs: refresh bundled channel plugin refs 2026-04-04 15:02:08 +01:00
Peter Steinberger
f02af9bb41 docs: refresh onboarding channel setup refs 2026-04-04 15:00:41 +01:00
Peter Steinberger
9dea255ee2 docs: refresh bundled channel overview refs 2026-04-04 14:58:17 +01:00
Peter Steinberger
756cb22f15 docs: refresh model selection fallback refs 2026-04-04 14:55:44 +01:00
Peter Steinberger
3e5bcc8cb2 docs: refresh isolated cron model switch refs 2026-04-04 14:53:45 +01:00
Vincent Koc
9cc300be78 fix(ci): restore main follow-up checks 2026-04-04 22:51:31 +09:00
Peter Steinberger
aa32f74fe6 docs: refresh cron delivery ownership refs 2026-04-04 14:51:08 +01:00
Peter Steinberger
981737035d docs: refresh isolated cron delivery refs 2026-04-04 14:48:51 +01:00
Peter Steinberger
3bc2e47966 docs: clarify failover 402 handling 2026-04-04 14:46:32 +01:00
Peter Steinberger
73584b1d33 docs: refresh failover and compaction refs 2026-04-04 14:44:51 +01:00
Peter Steinberger
bbb73d3171 refactor: split isolated cron runner phases 2026-04-04 14:42:35 +01:00
Peter Steinberger
9698ba7215 test: split isolated cron harness resets 2026-04-04 14:42:35 +01:00
Peter Steinberger
91d20781ed refactor: extract isolated cron execution seams 2026-04-04 14:42:35 +01:00
Peter Steinberger
083b882052 style(plugin-sdk): format provider stream helpers 2026-04-04 22:40:08 +09:00
Peter Steinberger
f9717f2eae fix(agents): align runtime with updated deps 2026-04-04 22:40:08 +09:00
Peter Steinberger
76d1f26782 chore(deps): update workspace dependencies 2026-04-04 22:40:08 +09:00
Peter Steinberger
70b39f4893 docs: refresh mattermost group config refs 2026-04-04 14:39:38 +01:00
Peter Steinberger
60206817b3 docs: refresh telegram command sdk refs 2026-04-04 14:38:33 +01:00
ToToKr
3b80f42152 fix(mattermost): add groups property to config schema (#57618) (#58271)
Merged via squash.

Prepared head SHA: 8d478fc092
Co-authored-by: MoerAI <26067127+MoerAI@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-04 16:37:53 +03:00
Peter Steinberger
8ca5a9174a docs: refresh gateway auth precedence refs 2026-04-04 14:36:52 +01:00
Peter Steinberger
882654d9ae docs: refresh talk config and doctor refs 2026-04-04 14:35:03 +01:00
Peter Steinberger
13f9475f6c docs: refresh bootstrap handoff token refs 2026-04-04 14:32:40 +01:00
Peter Steinberger
93ab8dd531 test: add CLI handshake regression coverage (#50240) (thanks @xiwuqi) 2026-04-04 22:32:15 +09:00
Peter Steinberger
114496871d docs: refresh tailscale auth rate limit refs 2026-04-04 14:30:13 +01:00
Peter Steinberger
7d22a16adb fix: bound bootstrap handoff token scopes 2026-04-04 22:29:52 +09:00
Peter Steinberger
7c0752f834 docs: refresh cron model override refs 2026-04-04 14:26:46 +01:00
Peter Steinberger
f502b023d9 docs: refresh device token scope mirrors 2026-04-04 14:25:47 +01:00
Peter Steinberger
ebe0a27b4d docs: refresh device token scope refs 2026-04-04 14:23:41 +01:00
Peter Steinberger
3758a0ce5b refactor(gateway): simplify connect auth parsing 2026-04-04 22:23:09 +09:00
Peter Steinberger
68ec7c9bbf docs: refresh plugin config schema refs 2026-04-04 14:21:00 +01:00
AARON AGENT
16e7e2551b fix(cron): prevent agent default model from overriding cron payload model (#58294)
* fix(cron): prevent agent default model from overriding cron payload model (#58065)

When a cron job specifies a model override via the Advanced settings,
runWithModelFallback could silently fall back to the agent's configured
primary model. This happened because fallbacksOverride was undefined
when neither payload.fallbacks nor per-agent fallbacks were configured,
causing resolveFallbackCandidates to append the agent primary as a
last-resort candidate. A transient failure on the cron-selected model
(rate limit, model-not-found, etc.) would then succeed on the agent
default, making it appear as if the override was ignored entirely.

Fix: when the cron payload carries an explicit model override, ensure
fallbacksOverride is always a defined array (empty when no fallbacks
are configured) so the agent primary is never silently appended.

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

* test: use stricter toEqual([]) assertion for fallbacksOverride

Replace toBeDefined() + toBeInstanceOf(Array) with toEqual([])
to catch regressions where the array unexpectedly gains entries.
Addresses review feedback.

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

* fix: preserve cron override fallback semantics (#58294)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-04 22:18:38 +09:00
Peter Steinberger
79be1e126a fix: harden parallels smoke harness 2026-04-04 14:18:18 +01:00
Peter Steinberger
99e45eb3ba docs: refresh remote bootstrap refs 2026-04-04 14:17:59 +01:00
Peter Steinberger
3f1b2703b7 fix: preserve cached device token scopes safely (#46032) (thanks @caicongyang) 2026-04-04 22:17:38 +09:00
Assistant
056c0870a9 fix(gateway): preserve stored scopes when reconnecting with device token
When the gateway client reconnects using a stored device token, it was
defaulting to ["operator.admin"] scopes instead of preserving the
previously authorized scopes from the stored token. This caused the
operator device token to be regenerated without operator.read scope,
breaking status/probe/health commands.

This fix:
1. Loads the stored scopes along with the stored token in selectConnectAuth
2. Uses the stored scopes when reconnecting with a valid device token
3. Falls back to explicitly requested scopes or default admin-only scope
   when no stored scopes exist

Fixes #46000
2026-04-04 22:17:38 +09:00
Peter Steinberger
2ecb8ca352 docs: refresh control ui auth ux refs 2026-04-04 14:14:54 +01:00
Peter Steinberger
07c7c4b9ec docs: refresh tailscale http auth refs 2026-04-04 14:13:36 +01:00
Peter Steinberger
11b8a025a4 docs: refresh gateway auth overview refs 2026-04-04 14:12:38 +01:00
Sebastian B Otaegui
33e6a7a28e feat(plugin-sdk): export OpenClawSchema via plugin-sdk/config-schema (#60557)
Merged via squash.

Prepared head SHA: 637ff7d3c8
Co-authored-by: feniix <91633+feniix@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-04 16:10:43 +03:00
Evan Newman
a26b844b88 fix(doctor): avoid repeat talk normalization changes from key order (#59911)
Merged via squash.

Prepared head SHA: a67bcaa11b
Co-authored-by: ejames-dev <180847219+ejames-dev@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
2026-04-04 21:07:10 +08:00
Peter Steinberger
022618e887 docs: refresh browser auth refs 2026-04-04 14:04:24 +01:00
Peter Steinberger
0afd30d325 docs: refresh shared-secret auth mirrors 2026-04-04 14:02:29 +01:00
Peter Steinberger
f8dcd3ed83 docs: refresh tailscale auth mirrors 2026-04-04 14:00:36 +01:00
Peter Steinberger
b0025b1921 docs: refresh hook ingress security refs 2026-04-04 13:59:09 +01:00
Vincent Koc
0d47106b98 fix(tests): restore stream wrapper type coverage 2026-04-04 21:56:48 +09:00
Vincent Koc
71ea82a4f4 fix(build): restore portable provider runtime types 2026-04-04 21:56:48 +09:00
Vincent Koc
2a03326925 fix(plugin-sdk): keep telegram command config available 2026-04-04 21:56:48 +09:00
Vincent Koc
b3faf20d91 perf(agents): avoid repeated subagent registry rescans 2026-04-04 21:56:48 +09:00
Peter Steinberger
6cff644dc9 docs: refresh http endpoint auth refs 2026-04-04 13:56:08 +01:00
Peter Steinberger
032dbf0ec6 fix: serialize async auth rate-limit attempts 2026-04-04 21:55:09 +09:00
Peter Steinberger
c63a32661a docs: refresh gateway auth overview mirrors 2026-04-04 13:54:15 +01:00
Peter Steinberger
11d17b3c38 docs: refresh control ui device identity refs 2026-04-04 13:52:23 +01:00
Vincent Koc
a6707c2e1f refactor(providers): flatten shared stream hooks 2026-04-04 21:51:58 +09:00
Peter Steinberger
8f473023e4 docs: refresh web surface auth mirrors 2026-04-04 13:50:47 +01:00
Hsiao A
ae16452a69 fix(slack): pre-set shuttingDown before app.stop() to prevent orphaned ping intervals (#56646)
Merged via squash with admin override.

Prepared head SHA: f1c91d50b0
Note: required red lanes are currently inherited from latest origin/main, not introduced by this PR.
Co-authored-by: hsiaoa <70124331+hsiaoa@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-04-04 20:49:23 +08:00
Peter Steinberger
16346d6784 docs: clarify trusted proxy mirror refs 2026-04-04 13:49:05 +01:00
Peter Steinberger
4991cd66ef docs: refresh reverse proxy hardening refs 2026-04-04 13:47:59 +01:00
Peter Steinberger
7985cf5531 docs: refresh trusted proxy auth guidance 2026-04-04 13:44:34 +01:00
Peter Steinberger
62babffc40 docs: refresh security audit reference docs 2026-04-04 13:42:47 +01:00
jason
6e28bd2eb6 feishu: fix schema 2.0 card config in interactive card UX functions (#53395)
Merged via squash.

Prepared head SHA: 31f2396404
Co-authored-by: drvoss <3031622+drvoss@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-04 15:38:37 +03:00
Peter Steinberger
375bd73ce1 docs: refresh security fix refs 2026-04-04 13:35:42 +01:00
Peter Steinberger
f2b3b3d912 docs: clarify setup node-manager refs 2026-04-04 13:34:02 +01:00
Peter Steinberger
6ee905c7bd docs: refresh gateway skills rpc refs 2026-04-04 13:32:32 +01:00
Peter Steinberger
b05761aae0 docs: refresh skills cli and install refs 2026-04-04 13:31:13 +01:00
Peter Steinberger
db2cc5c28a docs: refresh clawhub mirror refs 2026-04-04 13:29:07 +01:00
Peter Steinberger
f16566d30e docs: refresh cron failure delivery refs 2026-04-04 13:25:44 +01:00
XING
587f19967c fix(cron): notify user via primary delivery channel on job failure (#60622)
Merged via squash.

Prepared head SHA: bee4dfca06
Co-authored-by: artwalker <44759507+artwalker@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-04-04 20:24:16 +08:00
Peter Steinberger
c89d4857e4 docs: clarify bundled plugin recovery refs 2026-04-04 13:24:04 +01:00
Peter Steinberger
56960e33e6 docs: refresh plugin install and marketplace refs 2026-04-04 13:22:46 +01:00
Peter Steinberger
3607962a44 docs: refresh plugin channel metadata refs 2026-04-04 13:18:34 +01:00
Peter Steinberger
86c799f4e1 docs: refresh plugin cli and inspect refs 2026-04-04 13:16:39 +01:00
Peter Steinberger
20f9f99db6 docs: refresh plugin manifest and bundle refs 2026-04-04 13:15:25 +01:00
Vincent Koc
9b82692425 refactor(providers): drop trivial stream lambdas 2026-04-04 21:14:00 +09:00
Vincent Koc
b742909dca fix(agents): prefer cron for deferred follow-ups (#60811)
* fix(agents): prefer cron for deferred follow-ups

* fix(agents): gate cron scheduling guidance

* fix(changelog): add scheduling guidance note

* fix(agents): restore exec approval agent hint
2026-04-04 21:11:27 +09:00
Peter Steinberger
d46eabb010 docs: complete sdk export coverage docs 2026-04-04 13:10:46 +01:00
Peter Steinberger
6b991b2afa docs: clarify reserved bundled sdk families 2026-04-04 13:09:17 +01:00
Peter Steinberger
b424a7a3a4 docs: refresh sdk memory import refs 2026-04-04 13:07:52 +01:00
Peter Steinberger
e91b52f396 docs: refresh sdk helper import refs 2026-04-04 13:06:57 +01:00
Peter Steinberger
363c666201 docs: refresh sdk capability import refs 2026-04-04 13:05:49 +01:00
Vincent Koc
486505a54e refactor(providers): share kilocode stream family 2026-04-04 21:05:42 +09:00
Peter Steinberger
dd030fb761 docs: refresh sdk core runtime refs 2026-04-04 13:04:01 +01:00
Peter Steinberger
f9f9462c79 docs: refresh channel helper import refs 2026-04-04 13:02:43 +01:00
Peter Steinberger
8cf6e4b5df fix(plugin-sdk): unblock gateway test surfaces 2026-04-04 21:02:04 +09:00
Peter Steinberger
27972489d3 docs: refresh sdk runtime import refs 2026-04-04 13:01:15 +01:00
Peter Steinberger
cec15e08d1 docs: clarify bundled helper sdk seams 2026-04-04 12:59:26 +01:00
Vincent Koc
8059942216 refactor(providers): share xai stream helper 2026-04-04 20:56:34 +09:00
Peter Steinberger
72f54059c4 docs: refresh setup helper import refs 2026-04-04 12:56:02 +01:00
Peter Steinberger
1c5c15b1d4 docs: refresh sdk entrypoint wording 2026-04-04 12:55:05 +01:00
Peter Steinberger
940bf899f0 docs: refresh provider entry import refs 2026-04-04 12:54:15 +01:00
Peter Steinberger
502b024523 docs: refresh bundled provider package examples 2026-04-04 12:52:55 +01:00
Peter Steinberger
120b1d2ed2 docs: refresh provider package barrel refs 2026-04-04 12:51:31 +01:00
Peter Steinberger
e5b48ea2b4 docs: refresh anthropic stream helper refs 2026-04-04 12:49:53 +01:00
Peter Steinberger
0166fd426e docs: refresh minimax auth path refs 2026-04-04 12:47:07 +01:00
Peter Steinberger
9da0feeecf docs: fix minimax usage docs merge markers 2026-04-04 12:43:44 +01:00
Peter Steinberger
a375635a9a docs: refresh status token fallback refs 2026-04-04 12:42:50 +01:00
Peter Steinberger
fb0d60d7f3 fix: resolve MiniMax portal usage auth 2026-04-04 12:42:30 +01:00
Peter Steinberger
9d684e1040 docs: refresh provider usage auth refs 2026-04-04 12:40:55 +01:00
Peter Steinberger
c0d509e794 docs: refresh status cache fallback refs 2026-04-04 12:39:02 +01:00
Peter Steinberger
ac254f50e8 docs: refresh minimax usage refs 2026-04-04 12:36:18 +01:00
Vincent Koc
83c10350c6 refactor(providers): share anthropic stream helper 2026-04-04 20:35:30 +09:00
Stuart Sy
3f457cabf7 fix(status): hydrate cache usage in transcript fallback (#59247)
* fix(status): hydrate cache usage in transcript fallback

* docs(changelog): note status cache fallback fix

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-04 20:34:41 +09:00
Peter Steinberger
3100984a33 docs: refresh browser origin auth refs 2026-04-04 12:34:11 +01:00
Peter Steinberger
72847db28b test: cover android canvas a2ui trust gate 2026-04-04 20:33:24 +09:00
Peter Steinberger
1efce6f23c docs: refresh provider stream family docs 2026-04-04 12:32:43 +01:00
Peter Steinberger
9eb8184f36 fix: improve MiniMax coding-plan parsing (#52349) (thanks @IVY-AI-gif) 2026-04-04 20:32:15 +09:00
IVY
dd9c9dac53 style: format with oxfmt 2026-04-04 20:32:15 +09:00
IVY
30de4337bf fix: address review feedback and formatting
- Remove redundant name === 'MiniMax-M*' condition (already matched by startsWith)
- Use !== undefined guard instead of falsy check in deriveWindowLabelFromTimestamps
- Pass chatRemains directly to deriveWindowLabel when available
- Remove JSDoc comment style to match codebase conventions
2026-04-04 20:32:15 +09:00
IVY
efd5d5eb20 fix(usage): improve MiniMax coding-plan usage parsing for model_remains array
- Pick the chat model entry (MiniMax-M*) from model_remains instead of using the first BFS candidate, which could be a speech/video/image model with total_count=0.
- Derive window label from start_time/end_time timestamps when window_hours/window_minutes fields are absent; fixes the hardcoded 5h default for 4h windows.
- Include model name in plan label so users can distinguish free-tier coding-plan quota from paid API balance.

Closes #52335
2026-04-04 20:32:15 +09:00
Peter Steinberger
90af255a91 docs: refresh gemini cli usage refs 2026-04-04 12:30:55 +01:00
Peter Steinberger
65fcf7e104 fix(gateway): scope browser-origin auth throttling 2026-04-04 20:30:39 +09:00
Vincent Koc
8f7b02e567 refactor(providers): share openai stream families 2026-04-04 20:29:11 +09:00
Peter Steinberger
035a754f0f fix: harden android a2ui trust matching 2026-04-04 20:28:08 +09:00
Peter Steinberger
1cfc10e836 docs: refresh minimax multimodal refs 2026-04-04 12:27:47 +01:00
Vincent Koc
c75f82448f fix(google-cli): parse gemini json response and stats (#60801)
* fix(google-cli): restore gemini json reporting

* fix(google-cli): fall back to stats when usage is empty

* fix(changelog): note gemini cli cache reporting
2026-04-04 20:27:22 +09:00
Peter Steinberger
46cb493ac8 fix(sandbox): cover home credential bind audit 2026-04-04 20:27:10 +09:00
Peter Steinberger
3ec0463da9 docs: refresh minimax thinking refs 2026-04-04 12:23:33 +01:00
Peter Steinberger
3dda75894b refactor(agents): centralize run wait helpers 2026-04-04 20:22:16 +09:00
Peter Steinberger
42778ccd46 docs: refresh provider stream family refs 2026-04-04 12:21:37 +01:00
Peter Steinberger
9615488855 fix: disable MiniMax reasoning leak (#55809) (thanks @moktamd) 2026-04-04 20:21:37 +09:00
moktamd
2701e75f40 fix: disable thinking for MiniMax anthropic-messages streaming
MiniMax M2.7 returns reasoning_content in OpenAI-style delta chunks
({delta: {content: "", reasoning_content: "..."}}) when thinking is
active, rather than native Anthropic thinking block SSE events. Pi-ai's
Anthropic provider does not handle this format, causing the model's
internal reasoning to appear as visible chat output.

Add createMinimaxThinkingDisabledWrapper that injects
thinking: {type: "disabled"} into the outgoing payload for any MiniMax
anthropic-messages request where thinking is not already explicitly
configured, preventing the provider from generating reasoning_content
deltas during streaming.

Fixes #55739
2026-04-04 20:21:37 +09:00
Peter Steinberger
561bacd06a fix: harden synology chat TLS helper defaults 2026-04-04 20:21:13 +09:00
Peter Steinberger
b473816afb docs: refresh native streaming compat refs 2026-04-04 12:20:31 +01:00
Vincent Koc
bc648ac8e6 refactor(providers): add stream family hooks 2026-04-04 20:19:53 +09:00
Peter Steinberger
1037af01ad style(agents): normalize runtime prompt formatting 2026-04-04 12:19:08 +01:00
Peter Steinberger
c70b10460c style(auth): normalize auth choice formatting 2026-04-04 12:19:08 +01:00
Peter Steinberger
f3aad63f4e style(providers): normalize import and wrap formatting 2026-04-04 12:19:08 +01:00
Peter Steinberger
3207c5326a refactor: share native streaming compat helpers 2026-04-04 12:18:45 +01:00
Peter Steinberger
aaa173a4a7 docs: clarify node exec approval binding 2026-04-04 12:18:32 +01:00
Peter Steinberger
9ddfaff45f docs: clarify node exec approval plan forwarding 2026-04-04 12:18:04 +01:00
Peter Steinberger
605f48556b refactor(browser): share lifecycle cleanup helpers 2026-04-04 12:17:46 +01:00
Peter Steinberger
c3f415ad6e fix: preserve node system.run approval plans 2026-04-04 20:16:53 +09:00
Peter Steinberger
f832699fd7 docs: refresh provider hook overview refs 2026-04-04 12:16:29 +01:00
Peter Steinberger
53c33f8207 fix: forward node exec approval plans 2026-04-04 20:16:19 +09:00
Peter Steinberger
62c54fdc16 docs: refresh provider replay family refs 2026-04-04 12:15:31 +01:00
Jasmine Zhang
b838ecf885 fix: add 60s timeout to MiniMax VLM fetch call
The VLM image analysis fetch had no timeout, causing sessions to hang
indefinitely when the MiniMax API is slow or unresponsive. Other
vision/model API calls in the codebase already use timeouts. Adds
AbortSignal.timeout(60_000) consistent with image upload workloads.

Fixes #54139
2026-04-04 20:15:13 +09:00
Peter Steinberger
39bcf695dc fix(cron): reject unsafe custom session targets earlier 2026-04-04 20:13:39 +09:00
Peter Steinberger
00337cdde1 docs: refresh codex auth and ws refs 2026-04-04 12:11:45 +01:00
Vincent Koc
c29d4bbb86 test(providers): add family capability matrix coverage 2026-04-04 20:11:25 +09:00
Peter Steinberger
91bac7cb83 fix(usage): restore provider auth fallback 2026-04-04 12:10:45 +01:00
Peter Steinberger
6bbccb087a docs: refresh google cached content refs 2026-04-04 12:10:29 +01:00
Peter Steinberger
49bf527fd4 docs: clarify reserved gateway method namespaces 2026-04-04 12:08:41 +01:00
Peter Steinberger
9b352ab5b0 test: isolate session status from provider runtime leak 2026-04-04 12:08:05 +01:00
Peter Steinberger
b7411ad594 refactor(cron): share descendant run quiescence wait 2026-04-04 20:07:33 +09:00
Peter Steinberger
7b6334b0f4 refactor(agents): share run wait reply helpers 2026-04-04 20:07:33 +09:00
Peter Steinberger
bbb0b574c4 refactor: centralize gateway method policy helpers 2026-04-04 20:07:18 +09:00
Vincent Koc
d766465e38 fix(google): add direct cachedContent support (#60757)
* fix(google): restore gemini cache reporting

* fix(google): split cli parsing into separate PR

* fix(google): drop remaining cli overlap

* fix(google): honor cachedContent alias precedence
2026-04-04 20:07:13 +09:00
Peter Steinberger
b9e3c1a02e docs: refresh cron and subagent browser cleanup refs 2026-04-04 12:06:39 +01:00
Peter Steinberger
7ffbbd8586 fix: reserve admin gateway method prefixes 2026-04-04 20:04:48 +09:00
Peter Steinberger
86ee50b968 docs: refresh web search overview mirrors 2026-04-04 12:04:28 +01:00
Peter Steinberger
3b09b58c5d test: cover browser cleanup for cron and subagents (#60146) (thanks @BrianWang1990) 2026-04-04 20:03:57 +09:00
BrianWang1990
e697838899 style: fix import order in server-cron.ts
Move plugin-sdk import after cron/* imports per alphabetical convention.
2026-04-04 20:03:57 +09:00
BrianWang1990
72b2e413d6 fix(browser): clean up browser tabs/processes when cron tasks and subagents complete
When cron tasks or subagents use browser automation, the browser
processes were not cleaned up after the task completed. This caused
orphaned Chrome processes (PPID=1) to accumulate over time.

Root cause: closeTrackedBrowserTabsForSessions was only called during
session-reset/session-delete (via ensureSessionRuntimeCleanup), but
isolated cron runs and subagent completions never triggered these paths.

Fix: Add browser tab cleanup in two places:
1. server-cron.ts: wrap runCronIsolatedAgentTurn in try/finally to
   ensure browser tabs are cleaned up after every cron run.
2. subagent-registry-lifecycle.ts: call closeTrackedBrowserTabsForSessions
   when a subagent run completes, before the announce cleanup flow.

Both cleanup calls are best-effort (caught errors) so they never mask
the actual task result or break the completion flow.

Fixes #60104
2026-04-04 20:03:57 +09:00
Peter Steinberger
0b1c9c7057 fix: stabilize codex auth ownership and ws fallback cache 2026-04-04 20:03:15 +09:00
Peter Steinberger
fca889eea3 docs: refresh browser troubleshooting mirrors 2026-04-04 12:02:48 +01:00
Peter Steinberger
29f062770d docs: refresh browser stop cleanup refs 2026-04-04 12:02:10 +01:00
Peter Steinberger
c524d6c76c docs: refresh shared minimax web search refs 2026-04-04 12:00:58 +01:00
Peter Steinberger
bec891b2e2 test: cover attach-only browser stop cleanup (#60097) (thanks @pedh) 2026-04-04 19:59:59 +09:00
pedh
2c9723afd5 fix(browser): disconnect Playwright CDP session on stop for attachOnly/remote profiles
When `browser stop` is called for an `attachOnly` or remote CDP
profile, `profileState.running` is null (no process was launched), so
`stopRunningBrowser()` returned early without closing the Playwright
CDP connection. This left emulation overrides (prefers-color-scheme,
viewport, etc.) permanently applied until a full gateway restart.

Now call `closePlaywrightBrowserConnectionForProfile()` before
returning for attachOnly and remote CDP profiles, matching the cleanup
behavior already present in `resetProfile()`. Regular profiles that
were never started still return `{ stopped: false }`.

Fixes #60095
2026-04-04 19:59:59 +09:00
Peter Steinberger
0bc9f0b5ba docs: refresh browser screenshot route refs 2026-04-04 11:58:46 +01:00
Jithendra
d204be80af feat(tools): add MiniMax as bundled web search provider
Add native MiniMax Search integration via their Coding Plan search API
(POST /v1/coding_plan/search). This brings MiniMax in line with Brave,
Kimi, Grok, Gemini, and other providers that already have bundled web
search support.

- Implement WebSearchProviderPlugin with caching, credential resolution,
  and trusted endpoint wrapping
- Support both global (api.minimax.io) and CN (api.minimaxi.com)
  endpoints, inferred from explicit region config, model provider base
  URL, or minimax-portal OAuth base URL
- Prefer MINIMAX_CODE_PLAN_KEY over MINIMAX_API_KEY in credential
  fallback, matching existing repo precedence
- Accept SecretRef objects for webSearch.apiKey (type: [string, object])
- Register in bundled registry, provider-id compat map, and fast-path
  plugin id list with full alignment test coverage
- Add unit tests for endpoint/region resolution and edge cases

Closes #47927
Related #11399

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 19:56:04 +09:00
Peter Steinberger
a722719720 docs: refresh synology webhook auth refs 2026-04-04 11:55:57 +01:00
Peter Steinberger
7d16359aae docs: note Chrome 146 screenshot compat fix (#60682) (thanks @mvanhorn) 2026-04-04 19:55:37 +09:00
Matt Van Horn
b22f6257f0 fix(browser): remove fromSurface: false for Chrome 146+ screenshot compat 2026-04-04 19:55:37 +09:00
Peter Steinberger
05da802e1c refactor: split device-pair command helpers 2026-04-04 19:55:04 +09:00
Peter Steinberger
fdb1be0079 docs: refresh mattermost slash auth refs 2026-04-04 11:54:52 +01:00
Peter Steinberger
8a532dead2 docs: refresh browser cdp validation refs 2026-04-04 11:53:36 +01:00
Peter Steinberger
2a65bfee96 fix(mattermost): harden slash command token validation 2026-04-04 19:51:41 +09:00
Peter Steinberger
53d3fbcef6 docs: refresh browser existing session docs 2026-04-04 11:51:07 +01:00
Peter Steinberger
5583bda61d docs: note browser profile CDP validation fix (#60477) (thanks @eleqtrizit) 2026-04-04 19:51:02 +09:00
Agustin Rivera
5da360cada fix(browser): trim validation error prefix 2026-04-04 19:51:02 +09:00
Agustin Rivera
aefc6fc161 fix(browser): validate profile cdp urls 2026-04-04 19:51:02 +09:00
Peter Steinberger
36cc397548 fix: reuse shared Synology Chat secret compare 2026-04-04 19:49:35 +09:00
Peter Steinberger
c5b2b69f94 docs: refresh live model switch docs 2026-04-04 11:49:23 +01:00
Peter Steinberger
bc356cc8c2 fix: harden direct CDP websocket validation (#60469) (thanks @eleqtrizit) 2026-04-04 19:48:01 +09:00
Agustin Rivera
c3f8427973 fix(browser): validate initial cdp endpoints 2026-04-04 19:48:01 +09:00
Agustin Rivera
80720b4994 fix(browser): validate cdp websocket pivots 2026-04-04 19:48:01 +09:00
Peter Steinberger
e4ea3c03cf fix: scope live model switch pending state (#60266) (thanks @kiranvk-2011) 2026-04-04 19:45:53 +09:00
kiranvk2011
b36a3a3295 fix: add .catch() to fire-and-forget stale-flag clear to prevent unhandled rejection 2026-04-04 19:45:53 +09:00
kiranvk2011
e8f6ceedd4 fix: clear stale liveModelSwitchPending flag when model already matches
When the liveModelSwitchPending flag is set but the current model already
matches the persisted selection (e.g. the switch was applied as an override
and the current attempt is already using the new model), the flag is now
consumed eagerly via a fire-and-forget clearLiveModelSwitchPending() call.

Without this, the stale flag could persist across fallback iterations and
later cause a spurious LiveSessionModelSwitchError when the model rotates
to a fallback candidate that differs from the persisted selection.

Also expands JSDoc on shouldSwitchToLiveModel to document the stale-flag
clearing and deferral semantics.
2026-04-04 19:45:53 +09:00
kiranvk2011
251e086eac fix: use explicit flag for live model switch detection in fallback chain
Replace the ambiguous comparison-based approach (hasDifferentLiveSessionModelSelection
+ in-memory map EMBEDDED_RUN_MODEL_SWITCH_REQUESTS) with a persisted
`liveModelSwitchPending` flag on SessionEntry.

The root cause: the in-memory map was never populated in production because
requestLiveSessionModelSwitch() was removed in commit 622b91d04e and replaced
with refreshQueuedFollowupSession(). This left the comparison-based detection
as the only path, which could not distinguish user-initiated model switches
(via /model command) from system-initiated fallback rotations.

The fix:
- Add `liveModelSwitchPending?: boolean` to SessionEntry (persisted)
- Set the flag to true ONLY when /model command applies a model override
- New `shouldSwitchToLiveModel()` checks the flag + model mismatch together
- New `clearLiveModelSwitchPending()` resets the flag after consumption
- Replace throw-site logic in run.ts to use the new flag-based functions
- Remove orphaned resolveCurrentLiveSelection helper

Only the /model command sets this flag, so system-initiated fallback rotations
are never mistaken for user-initiated model switches. This restores the
live-switch-during-active-run feature that was accidentally broken.

Fixes #57857, #57760, #58137
2026-04-04 19:45:53 +09:00
Peter Steinberger
678e9e6078 docs: refresh gemini cli oauth references 2026-04-04 11:45:37 +01:00
Peter Steinberger
20a7b1a9dc fix: finalize device-pair scope hardening (#55996) (thanks @coygeek) 2026-04-04 19:44:43 +09:00
Coy Geek
9dcef6df02 fix: scope pairing guard to internal gateway callers 2026-04-04 19:44:43 +09:00
Coy Geek
05ca581ed0 fix: fail closed when pairing scopes are missing 2026-04-04 19:44:43 +09:00
Coy Geek
353d93613c fix: enforce pairing approval scopes 2026-04-04 19:44:43 +09:00
Peter Steinberger
5d0562badf docs: clarify cli backend mcp overlays 2026-04-04 11:43:29 +01:00
Peter Steinberger
cc602fe9d4 docs: refresh anthropic cli backend docs 2026-04-04 11:40:58 +01:00
Peter Steinberger
3f042ed002 fix: stabilize async provider test types 2026-04-04 19:39:22 +09:00
Peter Steinberger
87d840e9ee fix: tighten Teams and device typing 2026-04-04 19:39:22 +09:00
Peter Steinberger
75fb29ffe6 docs: refresh provider sdk hook docs 2026-04-04 11:38:25 +01:00
Peter Steinberger
d1bf2c6de1 docs: clarify device token role bounds 2026-04-04 11:36:02 +01:00
Peter Steinberger
e675634eb3 fix: preserve streamed Kimi tool args on repair fallback 2026-04-04 11:35:49 +01:00
Peter Steinberger
5bef64bc31 test: harden media provider auto-registration (#56279) (thanks @Ezio0) 2026-04-04 19:35:28 +09:00
Peter Steinberger
277df463d6 docs: clarify openrouter cache markers 2026-04-04 11:34:17 +01:00
Vincent Koc
39d2a719c9 refactor(providers): add family replay and tool hooks 2026-04-04 19:33:31 +09:00
Peter Steinberger
4e099689c0 feat: stream Claude CLI JSONL output 2026-04-04 19:33:08 +09:00
Peter Steinberger
2ab1f1c054 docs: clarify openai usage normalization 2026-04-04 11:32:58 +01:00
Peter Steinberger
10e0592ed0 refactor: extract device token rotate target guard 2026-04-04 19:32:25 +09:00
Vincent Koc
0a3211df2d fix(openrouter): gate prompt cache markers by endpoint (#60761)
* fix(openrouter): gate prompt cache markers by endpoint

* test(openrouter): use claude sonnet 4.6 cache model
2026-04-04 19:32:13 +09:00
Peter Steinberger
ee742cec40 fix: fallback ws usage totals (#54940) (thanks @lyfuci) 2026-04-04 19:32:05 +09:00
Peter Steinberger
4ee648c508 docs: refresh model picker provider filtering 2026-04-04 11:30:18 +01:00
复试资料
e955cffd32 Agents: widen WS usage aliases 2026-04-04 19:28:54 +09:00
复试资料
d166f2648e Agents: normalize WS usage aliases 2026-04-04 19:28:54 +09:00
Peter Steinberger
9367379771 docs: clarify prompt cache stability 2026-04-04 11:28:19 +01:00
Peter Steinberger
f0d3e231ef fix: cover bundled provider picker aliases (#58819) (thanks @Luckymingxuan) 2026-04-04 19:27:26 +09:00
Mingxuan
c4a903319e fix(model-picker): fallback to unfiltered list when provider filter yields empty results 2026-04-04 19:27:26 +09:00
Mingxuan
360fdaa4f2 fix(model-picker): use matchesPreferredProvider for plan variant matching 2026-04-04 19:27:26 +09:00
Mingxuan
fd3b7b5ae7 fix: add augmentModelCatalog hooks to bundled providers for proper filtering 2026-04-04 19:27:26 +09:00
Mingxuan
792558de01 fix(model-picker): use preferredProvider presence for filtering instead of catalog check
When auth choice explicitly sets a preferred provider (e.g., volcengine-api-key or byteplus-api-key), the model picker should always filter by that provider. Previously, it relied on providerIds.includes(preferredProvider), which could be false if the catalog hadn't loaded that provider's models yet due to a race condition between auth choice setup and catalog loading.

This ensures that selecting a provider via auth choice consistently filters the model list to only that provider's models, rather than showing all providers.
2026-04-04 19:27:26 +09:00
Peter Steinberger
6b82140336 fix: land device token role guard follow-up (#60462) (thanks @eleqtrizit) 2026-04-04 19:27:10 +09:00
Agustin Rivera
7cda9df4cb fix(device): reject unapproved token roles 2026-04-04 19:27:10 +09:00
Peter Steinberger
d58b4d7425 fix: respect MINIMAX_API_HOST in bundled minimax catalogs (#34524) (thanks @caiqinghua) 2026-04-04 19:26:12 +09:00
Peter Steinberger
2c36ca562d docs: clarify minimax usage window semantics 2026-04-04 11:25:51 +01:00
Peter Steinberger
01a24c20bf refactor: expose node pairing approval scopes 2026-04-04 19:23:33 +09:00
Peter Steinberger
848e7abb57 docs: refresh node pairing scope references 2026-04-04 11:22:02 +01:00
0912078
28021a0325 fix(minimax): invert usage_percent when deriving usedPercent from remaining-only fields
MiniMax's usage_percent / usagePercent fields report the *remaining* quota
as a percentage, not the consumed quota. When count fields (prompt_limit /
prompt_remain) are also present, fromCounts already computed the correct
usedPercent and the inverted value was silently ignored. But when only
usage_percent is returned (no count fields), the code treated it as a
used-percent and passed it through unchanged, causing the menu bar to show
"2% left" instead of "98% left".

Move usage_percent and usagePercent from PERCENT_KEYS to a new
REMAINING_PERCENT_KEYS array. deriveUsedPercent now inverts remaining-percent
values to obtain usedPercent, matching the behaviour already validated by the
existing "prefers count-based usage when percent looks inverted" test. Count-
based fromCounts still takes priority over both key groups.

Fixes #60193

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 19:20:50 +09:00
Peter Steinberger
1222961a77 docs: clarify macos cli install fallbacks 2026-04-04 11:20:23 +01:00
Peter Steinberger
7807e1ef05 docs: refresh bun install and onboarding references 2026-04-04 11:19:13 +01:00
Vincent Koc
5779831723 fix(agents): stabilize prompt cache followups 2026-04-04 19:17:59 +09:00
Peter Steinberger
a631270f01 docs: refresh package-manager update references 2026-04-04 11:17:14 +01:00
Peter Steinberger
c441db7e13 docs: refresh update channel references 2026-04-04 11:14:51 +01:00
Peter Steinberger
ca2fdcc45f fix: enforce node pairing approval scopes end-to-end (#60461) (thanks @eleqtrizit) 2026-04-04 19:13:48 +09:00
Agustin Rivera
0089d0e2e6 fix(pairing): require pairing scope for node approvals 2026-04-04 19:13:48 +09:00
Peter Steinberger
a90f3ffdac docs: clarify installer service refresh behavior 2026-04-04 10:52:02 +01:00
Peter Steinberger
93d8a8602b docs: refresh local installer references 2026-04-04 10:51:22 +01:00
Peter Steinberger
790a24002e docs: refresh daemon overview references 2026-04-04 10:49:13 +01:00
Peter Steinberger
f39b5e86e5 docs: refresh persistence guidance 2026-04-04 10:44:55 +01:00
Peter Steinberger
a2fa6e8b90 docs: refresh cloud persistence wording 2026-04-04 10:44:08 +01:00
Peter Steinberger
508ca72fc7 docs: refresh hosted backup guidance 2026-04-04 10:42:02 +01:00
Peter Steinberger
559e42b60c docs: fix hosted auth profile paths 2026-04-04 10:40:40 +01:00
Peter Steinberger
d7e288bee9 docs: refresh backup and migration storage refs 2026-04-04 10:39:42 +01:00
Peter Steinberger
f7c5988334 docs: refresh docker hosting auth storage refs 2026-04-04 10:36:35 +01:00
Peter Steinberger
0ed7662365 docs: refresh container auth and runtime refs 2026-04-04 10:35:35 +01:00
Brad Groux
fce81fccd8 msteams: add typingIndicator config and prevent duplicate DM typing indicator (#60771)
* msteams: add typingIndicator config and avoid duplicate DM typing

* fix(msteams): validate typingIndicator config

* fix(msteams): stop streaming before Teams timeout

* fix(msteams): classify expired streams correctly

* fix(msteams): handle link text from html attachments

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-04 04:34:24 -05:00
Peter Steinberger
af4e9d19cf docs: refresh linux gateway service guidance 2026-04-04 10:32:33 +01:00
Peter Steinberger
2d0ca75282 docs: refresh systemd service refs 2026-04-04 10:29:00 +01:00
Peter Steinberger
0182dd1694 docs: refresh linux service docs 2026-04-04 10:27:09 +01:00
Peter Steinberger
eb932d59e0 docs: refresh ci pipeline docs 2026-04-04 10:24:24 +01:00
Peter Steinberger
36fe4800d2 docs: refresh pi development docs 2026-04-04 10:21:30 +01:00
Peter Steinberger
cfcdf002c8 docs: refresh legacy tts and logging docs 2026-04-04 10:19:38 +01:00
Peter Steinberger
de63a646d6 docs: refresh shared web search references 2026-04-04 10:16:02 +01:00
Peter Steinberger
6b7d0deaf6 docs: refresh image generation shared references 2026-04-04 10:13:04 +01:00
Peter Steinberger
d24b9088fd docs: refresh image generation fallback refs 2026-04-04 10:10:32 +01:00
Peter Steinberger
c06248aee7 docs: refresh pdf tool model fallback refs 2026-04-04 10:07:16 +01:00
Peter Steinberger
2a5da613f4 docs: refresh media auto-detect refs 2026-04-04 10:05:30 +01:00
Peter Steinberger
459ede5a7e docs: refresh groq audio docs 2026-04-04 10:01:12 +01:00
Peter Steinberger
ac8d91edff docs: refresh bedrock discovery docs 2026-04-04 09:57:13 +01:00
Peter Steinberger
29033400eb docs: refresh zai glm refs 2026-04-04 09:54:52 +01:00
Peter Steinberger
74d39e9efe fix(ci): type zai dynamic model test callbacks 2026-04-04 09:52:34 +01:00
Peter Steinberger
c26ab4649d docs: refresh xai model ids 2026-04-04 09:52:02 +01:00
Peter Steinberger
7c43dfe28f fix(ci): isolate discord think autocomplete runtime 2026-04-04 09:49:35 +01:00
Peter Steinberger
05baeb2ada docs: refresh moonshot catalog refs 2026-04-04 09:49:20 +01:00
Peter Steinberger
7f5cf1a837 style: format explicit session-id resume helpers 2026-04-04 17:48:43 +09:00
Peter Steinberger
cd36ff7483 fix: resume explicit session-id agent runs 2026-04-04 17:48:43 +09:00
Peter Steinberger
87f512f80d docs: refresh minimax auth choice refs 2026-04-04 09:47:01 +01:00
Peter Steinberger
b5608397d0 docs: refresh minimax and kilocode refs 2026-04-04 09:45:18 +01:00
Peter Steinberger
323415204e fix: preserve registered glm-5 variants (#48185) (thanks @haoyu-haoyu) 2026-04-04 17:42:20 +09:00
Peter Steinberger
6b100e4dcf docs: expand static provider catalogs 2026-04-04 09:42:02 +01:00
ximi
9e0cf17d0c fix(minimax): correct model pricing per official docs 2026-04-04 17:40:57 +09:00
Peter Steinberger
7207a36d40 docs: refresh bundled provider overview refs 2026-04-04 09:39:56 +01:00
Peter Steinberger
1d5c57bad9 fix(ci): align browser and signal test expectations 2026-04-04 09:38:53 +01:00
Peter Steinberger
238fac6636 fix: cover status transcript fallback (#55041) (thanks @jjjojoj) 2026-04-04 17:38:44 +09:00
jjjojoj
97a8ba89fd fix: use transcript usage as fallback for /status token display
When using custom providers like LM Studio, Ollama, or DashScope,
token counts in /status show as 0 because the agent meta store
does not always have usage data populated for these providers.

Fix: set includeTranscriptUsage: true in both /status command and
the session_status tool. This enables the existing fallback path
that reads usage from the session transcript JSONL file when the
meta store has zero/missing token counts.

The merge logic already guards against overwriting valid data:
- totalTokens: only updated when zero or transcript value is larger
- inputTokens/outputTokens: only filled when zero/missing
- model/contextTokens: only filled when missing

Fixes #54995
2026-04-04 17:38:44 +09:00
Peter Steinberger
b601c7cb8f docs: refresh modelstudio catalog refs 2026-04-04 09:37:58 +01:00
Peter Steinberger
6a1ed07b33 docs: refresh router provider catalogs 2026-04-04 09:37:20 +01:00
Peter Steinberger
b1e3e59429 fix(ci): align stale provider and channel tests 2026-04-04 09:35:14 +01:00
Peter Steinberger
44762c0c80 docs: refresh bundled provider defaults 2026-04-04 09:32:58 +01:00
潘晓波0668000512
cca35404ea 修复:MiniMax coding_plan 将 interval/weekly usage_count 按剩余配额解析 2026-04-04 17:32:00 +09:00
Peter Steinberger
edc470f6b0 docs: refresh openai compatible proxy guides 2026-04-04 09:30:57 +01:00
Peter Steinberger
69980e8bf4 fix: resolve bare model ids via allowlist (#51580) (thanks @honwee) 2026-04-04 17:30:54 +09:00
陈大虾🦞
1ffbe09a6a fix(model): infer provider from allowlist for bare model IDs to prevent prefix drift (#48369) 2026-04-04 17:30:54 +09:00
Peter Steinberger
2906cfd6d7 fix: auto-register image-capable config providers (#51418) (thanks @xydt-610) 2026-04-04 17:29:54 +09:00
xydt-610
1d8bba7e39 fix(media-understanding): auto-register image capability for config providers with image input (#51392) 2026-04-04 17:29:54 +09:00
Peter Steinberger
3da187156f docs: clarify native and proxy request shaping 2026-04-04 09:29:09 +01:00
Peter Steinberger
f4855baf35 fix(ci): await async provider test registration 2026-04-04 09:28:43 +01:00
Peter Steinberger
4812b9d2e2 fix: preserve qualified chat model refs (#49874) (thanks @ShionEria) 2026-04-04 17:28:28 +09:00
ShionElia
683c028553 fix: preserve qualified provider prefix in Control UI model selector
When sessions report an already-qualified model id (e.g. ollama/qwen3:30b),
resolveServerChatModelValue was re-qualifying it using modelProvider,
producing incorrect values like openai-codex/qwen3:30b.

Preserve already-qualified model refs as-is before applying provider prefix.
Adds test coverage for qualified model preservation.

Fixes #49839
2026-04-04 17:28:28 +09:00
Peter Steinberger
1fcb2cfeb5 docs: clarify provider attribution behavior 2026-04-04 09:27:31 +01:00
Peter Steinberger
73572e04c1 fix: preserve generic DashScope streaming usage (#52395) (thanks @IVY-AI-gif) 2026-04-04 17:25:33 +09:00
Peter Steinberger
a192f345d4 docs: refresh key-free web search ordering 2026-04-04 09:25:20 +01:00
Peter Steinberger
54cfd746de docs: polish moonshot kimi docs (#57883) (thanks @chenxin-yan) 2026-04-04 17:23:29 +09:00
Chenxin Yan
8347022b50 remove redundency 2026-04-04 17:23:29 +09:00
Chenxin Yan
6615c5788b docs: fix incorrect Kimi Coding provider ID and model refs
The Kimi Coding plugin registers with provider ID `kimi` and default
model ID `kimi-code`, making the correct model ref `kimi/kimi-code`.

The docs incorrectly showed `kimi-coding/k2p5` as the provider/model
ref. This is confusing because `kimi-coding` is only a plugin alias,
not the actual provider ID used in config.

Updated all references in:
- docs/concepts/model-providers.md
- docs/providers/moonshot.md
- docs/zh-CN/concepts/model-providers.md
- docs/zh-CN/providers/moonshot.md
2026-04-04 17:23:29 +09:00
Peter Steinberger
df4b5d2137 docs: refresh self-hosted web search references 2026-04-04 09:22:30 +01:00
Vincent Koc
cdccbf2c1c fix(github-copilot): send IDE auth headers on runtime requests (#60755)
* Fix Copilot IDE auth headers

* fix(github-copilot): align tests and changelog

* fix(changelog): scope copilot replacement entry

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
2026-04-04 17:22:19 +09:00
Peter Steinberger
38ed8c355a docs: refresh perplexity web search references 2026-04-04 09:21:06 +01:00
Vincent Koc
e4c3df2fb6 docs(changelog): note cache boundary fix 2026-04-04 17:20:24 +09:00
Vincent Koc
a50b838dc2 test(agents): annotate cache trace wrapper params 2026-04-04 17:20:23 +09:00
Vincent Koc
1a13c34f5b fix(agents): close cache boundary transport gaps 2026-04-04 17:20:23 +09:00
Peter Steinberger
58a56d9a82 feat: add MiniMax TTS provider (#55921) (thanks @duncanita) 2026-04-04 09:19:45 +01:00
Peter Steinberger
a746f0e8c3 style: normalize telegram fetch test formatting 2026-04-04 09:19:45 +01:00
Vincent Koc
7ad43f21d3 style(msteams): format split graph message import 2026-04-04 09:19:45 +01:00
gnuduncan
e934211170 fix(minimax): use global TTS endpoint default and add missing Talk Mode overrides
Switch DEFAULT_MINIMAX_TTS_BASE_URL from api.minimaxi.com (CN) to
api.minimax.io (global) so international API keys work out of the box.
Add vol and pitch to resolveTalkOverrides for parity with resolveTalkConfig.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:19:45 +01:00
gnuduncan
7d7f5d85b4 feat(minimax): add native TTS speech provider (T2A v2)
Add MiniMax as a fourth TTS provider alongside OpenAI, ElevenLabs, and
Microsoft. Registers a SpeechProviderPlugin in the existing minimax
extension with config resolution, directive parsing, and Talk Mode
support. Hex-encoded audio response from the T2A v2 API is decoded to
MP3.

Closes #52720

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:19:45 +01:00
Peter Steinberger
49d962a82f docs: refresh brave web search references 2026-04-04 09:19:11 +01:00
Peter Steinberger
d1a4363783 fix(runtime): restore gateway watch on legacy state 2026-04-04 09:18:28 +01:00
Peter Steinberger
0051a86b8f docs: clarify synthesized web search count behavior 2026-04-04 09:17:49 +01:00
Gaston Rodriguez
b6b1d5dd6c Moonshot: reuse native base URL for Kimi web search 2026-04-04 17:16:29 +09:00
Peter Steinberger
e5d03f734a docs: refresh kimi web search setup 2026-04-04 09:15:01 +01:00
Peter Steinberger
21ca006eca fix(infra): restore approval account binding compatibility 2026-04-04 09:13:11 +01:00
Peter Steinberger
af7c6f4c68 fix: harden kimi web search setup (#59356) (thanks @Innocent-children) 2026-04-04 17:11:47 +09:00
innocent-children
216294765b fix(kimi): unify runtime model fallback to kimi-k2.5
Remove DEFAULT_KIMI_MODEL (moonshot-v1-128k) and align resolveKimiModel
fallback to DEFAULT_KIMI_SEARCH_MODEL (kimi-k2.5). The legacy model
does not support the $web_search builtin_function tool, so env-var-only
users without a configured model would hit the original bug.
2026-04-04 17:11:47 +09:00
innocent-children
111495d3ca 修复kimi web_search错误 2026-04-04 17:11:47 +09:00
Peter Steinberger
11a87b4b7a docs: clarify plugin facade runtime snapshots 2026-04-04 09:11:25 +01:00
Peter Steinberger
85ade25003 docs: refresh minimax multimodal references 2026-04-04 09:09:06 +01:00
Peter Steinberger
daac149744 fix(ci): honor runtime config snapshots for facades 2026-04-04 09:08:25 +01:00
Peter Steinberger
42f6de16b2 fix: advertise MiniMax M2.7 image input (#54843) (thanks @MerlinMiao88888888) 2026-04-04 17:07:35 +09:00
王淼0668000666
51d998d828 minimax: add image capability to MiniMax-M2.7 model 2026-04-04 17:07:35 +09:00
王淼0668000666
87b41ca693 minimax: add image capability to MiniMax-M2.7 model 2026-04-04 17:07:35 +09:00
Peter Steinberger
ed866020df docs: refresh task reconciliation references 2026-04-04 09:07:08 +01:00
Peter Steinberger
7d1575b5df fix: reconcile stale cron and chat-backed tasks (#60310) (thanks @lml2468) 2026-04-04 17:05:57 +09:00
Peter Steinberger
7036e5afbf fix: honor exec approval security from approvals (#60310) (thanks @lml2468) 2026-04-04 17:05:57 +09:00
Peter Steinberger
8cec7c68b9 fix(ci): restore typecheck on main 2026-04-04 09:05:17 +01:00
Peter Steinberger
da50b492c8 docs: refresh gateway status diagnostics refs 2026-04-04 09:05:08 +01:00
Peter Steinberger
dc2575f6c4 docs: clarify local agent plugin preload 2026-04-04 09:04:11 +01:00
Peter Steinberger
7671f4f1e3 docs: clarify gateway and plugin http auth scopes 2026-04-04 09:01:05 +01:00
Peter Steinberger
bc75968074 perf(cli): trim gateway status startup imports 2026-04-04 08:59:56 +01:00
Peter Steinberger
be15805a84 refactor(runtime): lazy-load control-ui and channel-config surfaces 2026-04-04 08:59:56 +01:00
Peter Steinberger
f9e9d4e357 fix(cli): preload plugins for local agent runs 2026-04-04 08:59:37 +01:00
Vincent Koc
12be79ac48 docs(agents): clarify mobile pairing ws scope 2026-04-04 16:57:58 +09:00
Peter Steinberger
7286a10679 fix: resolve rebase gate drift (#59815) 2026-04-04 16:57:44 +09:00
Peter Steinberger
36987831ce fix: restore current-main gate (#59815) 2026-04-04 16:57:44 +09:00
Peter Steinberger
926c107fe5 fix: narrow plugin route runtime scope fallback (#59815) (thanks @pgondhi987) 2026-04-04 16:57:44 +09:00
Pavan Kumar Gondhi
0e04ca36b9 fix: finalize issue changes 2026-04-04 16:57:44 +09:00
Pavan Kumar Gondhi
74270762ff fix: address review feedback 2026-04-04 16:57:44 +09:00
Pavan Kumar Gondhi
b02b2c3a0b fix: address issue 2026-04-04 16:57:44 +09:00
Peter Steinberger
4f3ad7c6fc docs(changelog): dedupe prompt cache entry 2026-04-04 16:57:30 +09:00
Peter Steinberger
8f85c7386b docs: close remaining cli index coverage gaps 2026-04-04 08:57:20 +01:00
Peter Steinberger
5a13756ca3 docs: expand cli index coverage refs 2026-04-04 08:56:23 +01:00
Peter Steinberger
a81cf1da1f refactor: share sdk lazy config and cli test helpers 2026-04-04 16:55:04 +09:00
Peter Steinberger
6a55556b83 docs: expand sandbox and daemon index refs 2026-04-04 08:54:21 +01:00
Peter Steinberger
edfaa01d1d refactor(plugin-sdk): split runtime helper seams 2026-04-04 08:53:19 +01:00
Peter Steinberger
470898b5e1 docs: refresh gateway update and memory refs 2026-04-04 08:52:43 +01:00
Peter Steinberger
4c450ede65 fix(feishu): narrow channel sdk seams 2026-04-04 08:50:28 +01:00
Peter Steinberger
19036ef394 fix: unblock current main checks 2026-04-04 16:50:25 +09:00
Peter Steinberger
cbc6a1ddb8 fix: restore main type surfaces 2026-04-04 16:50:25 +09:00
Peter Steinberger
04b539e98c fix: restore channel sdk schema typing 2026-04-04 16:50:25 +09:00
Peter Steinberger
f6df3ed70c fix: clean up stale cron and chat-backed tasks (#60310) 2026-04-04 16:50:25 +09:00
Peter Steinberger
6afdf10266 fix: honor exec approval security from approvals (#60310) 2026-04-04 16:50:25 +09:00
Peter Steinberger
b5265a07d7 refactor: replace 156k-line generated baselines with SHA-256 hash files
Config and Plugin SDK drift detection now compares SHA-256 hashes instead
of full JSON content. The .sha256 files (6 lines total) are tracked in git;
the full JSON baselines are gitignored and generated locally for inspection.

Same CI guarantee, zero repo churn on schema changes.
2026-04-04 16:49:21 +09:00
Peter Steinberger
b4e9802ef3 test: tidy gateway scope forwarding coverage 2026-04-04 16:48:26 +09:00
Peter Steinberger
22dad753a5 docs: refresh setup and config refs 2026-04-04 08:48:15 +01:00
Peter Steinberger
1d1c52e6e6 docs: refresh mcp approvals and hooks refs 2026-04-04 08:46:37 +01:00
sudie-codes
928a5128f4 msteams: add channel-list and channel-info actions (#57529)
* msteams: add channel-list and channel-info actions via Graph API

* msteams: use action helpers, add channel-list pagination

* msteams: address PR #57529 review feedback
2026-04-04 02:43:08 -05:00
Peter Steinberger
3967ffec22 docs: refresh agent and agents refs 2026-04-04 08:42:55 +01:00
Peter Steinberger
9bbedf3caa test: replace hanging pair approve poc coverage 2026-04-04 16:42:46 +09:00
Peter Steinberger
2c0f096688 docs: refresh channel support messaging 2026-04-04 16:41:56 +09:00
Peter Steinberger
138ef136ee docs: refresh message and channels refs 2026-04-04 08:39:04 +01:00
Brad Groux
c88d6d67c8 feat(msteams): add OpenClaw User-Agent header to Microsoft HTTP calls (#51568) (#60433)
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-04 02:38:57 -05:00
Brad Groux
dd2faa3764 fix(msteams): persist conversation reference during DM pairing (#60432)
* fix(msteams): persist conversation reference during DM pairing (#43323)

* ci: retrigger checks

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-04 02:38:54 -05:00
Brad Groux
06c6ff6670 fix(msteams): handle Adaptive Card Action.Submit invoke activities (#60431)
* fix(msteams): handle Adaptive Card Action.Submit invoke activities (#55384)

* ci: retrigger checks

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-04 02:38:51 -05:00
Brad Groux
1b2fb6b98b feat: add bundled StepFun provider plugin (#60032) (#60430)
Co-authored-by: hengm3467 <100685635+hengm3467@users.noreply.github.com>
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
2026-04-04 02:38:49 -05:00
Peter Steinberger
7a03027e7f docs: refresh pairing devices and dns refs 2026-04-04 08:36:27 +01:00
Peter Steinberger
545ecc63bd docs: refresh docs search and tui refs 2026-04-04 08:34:43 +01:00
Peter Steinberger
4b490d90ec docs: expand cli security and webhook refs 2026-04-04 08:33:50 +01:00
Vincent Koc
74f60dfd0b test(agents): extend live cache runner scenarios 2026-04-04 16:33:14 +09:00
Peter Steinberger
f79c00b972 docs: expand cli maintenance summaries 2026-04-04 08:31:36 +01:00
Peter Steinberger
5d7979c5c7 docs: refresh reset and uninstall refs 2026-04-04 08:30:25 +01:00
Vincent Koc
c76646adb1 feat(agents): add prompt cache break diagnostics (#60707)
* feat(agents): add prompt cache break diagnostics

* test(agents): wire cache trace into live cache suite

* fix(agents): always record cache trace result stage

* feat(status): show cache reuse in verbose output

* fix(agents): ignore missing prompt cache usage

* chore(changelog): note prompt cache diagnostics

* fix(agents): harden prompt cache diagnostics
2026-04-04 16:29:32 +09:00
Peter Steinberger
df09fe9adf docs: refresh system health and sessions refs 2026-04-04 08:28:41 +01:00
Peter Steinberger
d584ccfc77 docs: expand logs cli reference 2026-04-04 08:27:14 +01:00
Vincent Koc
9ea37202a8 fix(config): strip legacy googlechat streamMode on load 2026-04-04 16:26:35 +09:00
Peter Steinberger
09997f032f docs: refresh tasks and status references 2026-04-04 08:24:24 +01:00
Peter Steinberger
0a5bce21a6 fix: tighten pairing guard and unblock landing gate (#60491) (thanks @eleqtrizit) 2026-04-04 16:24:10 +09:00
Agustin Rivera
cb0b15a195 fix(pair): guard setup fallback subcommands 2026-04-04 16:24:10 +09:00
Agustin Rivera
9bb97b54fe fix(pair): fail fast before qr setup lookup 2026-04-04 16:24:10 +09:00
Agustin Rivera
83e5fe5e8b fix(pair): enforce pairing scope for setup commands 2026-04-04 16:24:10 +09:00
Peter Steinberger
c3a2701c45 fix(android): delay operator bootstrap reconnect until stored auth 2026-04-04 16:23:37 +09:00
Peter Steinberger
4f95822aa8 docs: refresh cron cli references 2026-04-04 08:22:24 +01:00
Vincent Koc
d75a8933e7 fix(agents): stabilize prompt cache fingerprints (#60731)
* fix(agents): stabilize prompt cache fingerprints

* chore(changelog): note prompt cache fingerprint stability

* refactor(agents): simplify capability normalization

* refactor(agents): simplify prompt capability normalization helper
2026-04-04 16:20:36 +09:00
Peter Steinberger
0660bef81e docs: refresh cli acp and approvals summaries 2026-04-04 08:20:20 +01:00
Peter Steinberger
3e5c571e57 docs: sync browser cli summary 2026-04-04 08:18:14 +01:00
Peter Steinberger
53e2554281 docs: expand browser cli reference 2026-04-04 08:17:19 +01:00
Vincent Koc
5c685eee9c fix(config): remove lingering channel streamMode leaks (#60733) 2026-04-04 16:14:38 +09:00
Peter Steinberger
644ed24ed8 docs(changelog): clarify breaking config aliases 2026-04-04 16:14:28 +09:00
Vincent Koc
65842aabad refactor(providers): share google and xai provider helpers (#60722)
* refactor(google): share oauth token helpers

* refactor(xai): share tool auth fallback helpers

* refactor(xai): share tool auth resolution

* refactor(xai): share tool config helpers

* refactor(xai): share fallback auth helpers

* refactor(xai): share responses tool helpers

* refactor(google): share http request config helper

* fix(xai): re-export shared web search extractor

* fix(xai): import plugin config type

* fix(providers): preserve default google network guard
2026-04-04 16:14:15 +09:00
Peter Steinberger
c87903a4c6 fix(ci): restore build and typecheck on main 2026-04-04 08:13:16 +01:00
Peter Steinberger
d2bace59d1 docs: refresh live testing auth storage 2026-04-04 08:12:52 +01:00
Peter Steinberger
66b1520d92 docs: refresh auth command references 2026-04-04 08:10:34 +01:00
Peter Steinberger
32d2654340 build: bump version to 2026.4.4 2026-04-04 16:09:42 +09:00
Peter Steinberger
95a6d386c0 docs: expand provider overview coverage 2026-04-04 08:07:36 +01:00
Peter Steinberger
14cfcdba1a docs(test): refresh stale model refs 2026-04-04 08:05:49 +01:00
Peter Steinberger
f38a3ae996 docs(changelog): reorder unreleased notes 2026-04-04 16:04:40 +09:00
Peter Steinberger
9195cf839b docs: refresh provider overview references 2026-04-04 08:03:56 +01:00
Peter Steinberger
1738900a9a docs: refresh moonshot kimi coding refs 2026-04-04 08:01:41 +01:00
Peter Steinberger
0013568500 docs: refresh google and openrouter onboarding docs 2026-04-04 07:59:52 +01:00
Peter Steinberger
406a47284a fix(ci): restore channel typing and root-help metadata build 2026-04-04 07:59:32 +01:00
Peter Steinberger
7b4e20fc8c docs: sync cloudflare and synthetic provider docs 2026-04-04 07:57:43 +01:00
Peter Steinberger
20266ff7dd fix: preserve mobile bootstrap auth fallback (#60238) (thanks @ngutman) 2026-04-04 15:57:38 +09:00
Nimrod Gutman
226ca1f324 fix(auth): address qr bootstrap review feedback 2026-04-04 15:57:38 +09:00
Nimrod Gutman
a9140abea6 fix(auth): hand off qr bootstrap to bounded device tokens 2026-04-04 15:57:38 +09:00
Vincent Koc
c4597992ca fix(config): remove remaining legacy surface leaks (#60726) 2026-04-04 15:55:31 +09:00
Peter Steinberger
1c42f0e866 docs: refresh auth storage reference examples 2026-04-04 07:52:22 +01:00
Peter Steinberger
ad7461b639 docs: align auth storage and token auth guidance 2026-04-04 07:50:26 +01:00
Peter Steinberger
da3f5e9bca docs(providers): refresh model examples and env defaults 2026-04-04 07:49:22 +01:00
Vincent Koc
0609bf8581 feat(memory): harden dreaming and multilingual memory promotion (#60697)
* feat(memory): add recall audit and doctor repair flow

* refactor(memory): rename symbolic scoring and harden dreaming

* feat(memory): add multilingual concept vocabulary

* docs(changelog): note dreaming memory follow-up

* docs(changelog): shorten dreaming follow-up entry

* fix(memory): address review follow-ups

* chore(skills): tighten security triage trust model

* Update CHANGELOG.md
2026-04-04 15:48:13 +09:00
Peter Steinberger
0ab160cda9 docs(anthropic): remove setup-token setup docs 2026-04-04 15:46:25 +09:00
Peter Steinberger
1b4bb5be19 fix(anthropic): remove setup-token onboarding path 2026-04-04 15:46:25 +09:00
Peter Steinberger
15bee338e9 docs: refresh provider hook docs 2026-04-04 07:46:15 +01:00
Peter Steinberger
359c6dedbe docs: prefer channel-core in channel sdk docs 2026-04-04 07:46:15 +01:00
Peter Steinberger
6e6b4f6004 ci: gate releases on live cache floors 2026-04-04 15:44:34 +09:00
Peter Steinberger
be4eb269fc refactor: tighten ACP spawn failure typing 2026-04-04 15:43:23 +09:00
Peter Steinberger
b167ad052c refactor(providers): move defaults and error policy into plugins 2026-04-04 07:43:14 +01:00
Peter Steinberger
e34f42559f docs: refresh plugin sdk import reference 2026-04-04 07:41:44 +01:00
Peter Steinberger
27aa659498 docs: clarify plugin entry export contract 2026-04-04 07:40:24 +01:00
Peter Steinberger
d5cb8cebcd refactor(extensions): split channel runtime helper seams 2026-04-04 07:39:53 +01:00
Peter Steinberger
667a54a4b7 refactor(plugins): narrow bundled channel core seams 2026-04-04 07:39:53 +01:00
Peter Steinberger
381ee4d218 docs: align bundled plugin defaults in docs 2026-04-04 07:38:55 +01:00
Peter Steinberger
50a1fac1c5 docs: remove stale plugins status command 2026-04-04 07:37:25 +01:00
Peter Steinberger
c8be1ca6ae docs: note sdk config schema memoization 2026-04-04 07:35:04 +01:00
Peter Steinberger
25b069a6f3 refactor(gateway): split MCP loopback transport helpers 2026-04-04 15:34:13 +09:00
Peter Steinberger
f856aaea40 fix(ci): pick the real root-help bundle 2026-04-04 07:33:48 +01:00
Vincent Koc
26f0c7ee90 refactor(plugin-sdk): lazily resolve plugin config schemas 2026-04-04 15:32:33 +09:00
Peter Steinberger
85c5d90c11 docs: sync acp spawn workspace behavior 2026-04-04 07:32:09 +01:00
Peter Steinberger
71c0c2cc06 fix: harden ACP spawn workspace resolution 2026-04-04 15:29:56 +09:00
zssggle-rgb
d718d17b5b fix(acp): fall back when inherited target workspace is missing 2026-04-04 15:29:56 +09:00
Peter Steinberger
6507f54965 docs: refresh generic model examples 2026-04-04 07:27:32 +01:00
Vincent Koc
bcd11176ef refactor(amazon-bedrock): lazy-load provider registration 2026-04-04 15:26:37 +09:00
Peter Steinberger
195e380e05 docs: remove legacy cache retention notes 2026-04-04 15:26:19 +09:00
Peter Steinberger
cb6d0576be docs: refresh media understanding examples 2026-04-04 07:25:52 +01:00
Peter Steinberger
332caa4cb1 style: normalize embedded runner imports 2026-04-04 15:24:50 +09:00
Peter Steinberger
3d55b28853 style: wrap long runtime and test lines 2026-04-04 15:24:50 +09:00
Peter Steinberger
b379dac798 chore: ignore dist-runtime artifacts 2026-04-04 15:24:50 +09:00
Peter Steinberger
1809da659e docs: refresh cli and node pairing references 2026-04-04 07:23:11 +01:00
Vincent Koc
6fc69f5d33 fix(secrets): drop legacy talk apiKey target surface (#60717) 2026-04-04 15:22:41 +09:00
Peter Steinberger
e7e1707277 fix(ci): restore build and typecheck on main 2026-04-04 07:22:16 +01:00
Vincent Koc
7e7460c2f9 refactor(anthropic): lazy-load provider registration 2026-04-04 15:20:28 +09:00
Peter Steinberger
666f1f4db0 refactor(providers): remove core default and usage bias 2026-04-04 07:19:29 +01:00
Peter Steinberger
9e4cf3996e test: add gateway durable allow-always coverage (#59880) (thanks @luoyanglang) 2026-04-04 15:18:24 +09:00
Peter Steinberger
cdb572d703 test: tune live cache assertions 2026-04-04 15:18:09 +09:00
Vincent Koc
c4d2c4899d refactor(browser): lazy-load plugin registration 2026-04-04 15:17:44 +09:00
Peter Steinberger
3de09fbe74 fix: restore claude cli loopback mcp bridge (#35676) (thanks @mylukin) 2026-04-04 15:16:20 +09:00
Vincent Koc
c2435306a7 refactor(acpx): lazy-load runtime service entry 2026-04-04 15:14:51 +09:00
Peter Steinberger
e2454d4b8a docs: align provider and onboarding references 2026-04-04 07:14:28 +01:00
Peter Steinberger
dd16080af7 refactor(exec): dedupe durable approval checks 2026-04-04 07:12:26 +01:00
Peter Steinberger
b32a2cadc2 docs(acp): clarify default startup and runtime paths 2026-04-04 15:10:26 +09:00
Vincent Koc
e56ffd48df refactor(github-copilot): lazy-load provider registration 2026-04-04 15:06:02 +09:00
Ayaan Zaidi
1c1f32e756 fix: trust local bot api media roots (#60705) 2026-04-04 11:35:36 +05:30
Ayaan Zaidi
cfc52fcf2b fix(telegram): trust local bot api media roots 2026-04-04 11:35:36 +05:30
Peter Steinberger
c91b6bf322 fix(ci): unblock agent typing and cache startup metadata 2026-04-04 07:04:17 +01:00
Peter Steinberger
3a3f88a80a refactor(media): move provider defaults into media metadata 2026-04-04 07:00:47 +01:00
Peter Steinberger
fca80d2ee2 refactor(thinking): move provider thinking fallback out of core 2026-04-04 07:00:47 +01:00
Peter Steinberger
b59ce0903c docs: add SOUL personality guide 2026-04-04 14:59:35 +09:00
Vincent Koc
3437818b91 refactor(vllm): lazy-load provider registration 2026-04-04 14:56:04 +09:00
Peter Steinberger
0587fb3fc8 fix: note gateway allow-always reuse (#59880) (thanks @luoyanglang) 2026-04-04 14:55:26 +09:00
luoyanglang
b54acd97b3 fix(exec): reuse gateway allow-always approvals 2026-04-04 14:55:26 +09:00
Vincent Koc
ede6d03850 refactor(openrouter): lazy-load provider registration 2026-04-04 14:54:59 +09:00
Vincent Koc
0099c309c9 refactor(openai): lazy-load provider registration 2026-04-04 14:53:02 +09:00
Vincent Koc
fcf1aee2b4 refactor(microsoft): lazy-load speech provider 2026-04-04 14:51:55 +09:00
Vincent Koc
73115b5480 fix(zalouser): migrate legacy group allow aliases (#60702)
* fix(channels): prefer source contract surfaces in source checkouts

* fix(zalouser): migrate legacy group allow aliases
2026-04-04 14:50:15 +09:00
Peter Steinberger
ae7942bf5e fix: prefer Claude CLI in Anthropic onboarding 2026-04-04 14:49:55 +09:00
Peter Steinberger
1ab37d7a12 refactor(gateway): classify pairing locality 2026-04-04 06:47:14 +01:00
Vincent Koc
b3186aeef9 test(agents): expand live cache runner scenarios 2026-04-04 14:46:56 +09:00
Vincent Koc
32dd0aa7e7 fix(plugin-sdk): lazy acp runtime testing merge 2026-04-04 14:43:53 +09:00
Vincent Koc
fd01561327 fix(agents): close remaining prompt cache boundary gaps (#60691)
* fix(agents): route default stream fallbacks through boundary shapers

* fix(agents): close remaining cache boundary gaps

* chore(changelog): note cache prefix follow-up rollout

* fix(agents): preserve cache-safe fallback stream bases
2026-04-04 14:41:47 +09:00
Peter Steinberger
30ba837a7b test: isolate MCP live cache probe 2026-04-04 14:39:51 +09:00
Peter Steinberger
0ebc7b6077 docs: clarify anthropic claude cli migration 2026-04-04 14:38:42 +09:00
Peter Steinberger
40da986b21 fix: preserve docker cli pairing locality (#55113) (thanks @sar618) 2026-04-04 14:36:30 +09:00
sar618
224fceee1a fix(gateway): skip device pairing for authenticated CLI connections in Docker
CLI connections with valid shared auth (token/password) now bypass device
pairing, fixing the chicken-and-egg problem where Docker CLI commands fail
with 'pairing required' (1008) despite sharing the gateway's network
namespace and auth token.

The existing shouldSkipBackendSelfPairing only matched gateway-client/backend
mode. CLI connections use cli/cli mode and were excluded. Additionally,
isLocalDirectRequest produces false negatives in Docker (host networking,
network_mode sharing) even when remoteAddress is 127.0.0.1, so CLI connections
with valid shared auth skip the locality check entirely — the token is the
trust anchor.

Closes #55067
Related: #12210, #23471, #30740
2026-04-04 14:36:30 +09:00
Peter Steinberger
2b538464e1 fix(docs): format dreaming memory tables 2026-04-04 06:31:40 +01:00
Vincent Koc
71562cc570 docs(changelog): add config surface breaking note 2026-04-04 14:30:46 +09:00
Vincent Koc
b390591779 fix(matrix): migrate room allow aliases to enabled (#60690)
* fix(matrix): migrate room allow aliases to enabled

* test(matrix): keep migration coverage on the channel seam

* chore(config): refresh baselines after matrix alias cleanup
2026-04-04 14:27:50 +09:00
Vincent Koc
6e0fe1b91e docs: expand dreaming memory documentation 2026-04-04 14:25:29 +09:00
Vignesh Natarajan
10d5b8813d Agents/logging: reduce orphaned-user warning noise for background runs 2026-04-03 22:24:02 -07:00
Peter Steinberger
e4dc03f108 refactor(acpx): split Windows command parsing 2026-04-04 14:19:20 +09:00
Peter Steinberger
41243529fb refactor(providers): centralize provider model policy 2026-04-04 06:16:48 +01:00
Vincent Koc
e07d8fd20b docs(agents): tighten provider boundary guidance 2026-04-04 14:13:46 +09:00
Peter Steinberger
026ca40be9 fix(ci): repair voice-call provider resolution typing 2026-04-04 06:11:30 +01:00
Vignesh Natarajan
18016e7546 Docs/memory: add Dreaming concept page and overview links 2026-04-03 22:10:32 -07:00
Peter Steinberger
db177ab2ac docs: add changelog for #60689 2026-04-04 14:10:20 +09:00
Peter Steinberger
e985324d87 fix(acpx): preserve Windows Claude CLI paths 2026-04-04 14:10:20 +09:00
Vignesh Natarajan
9802c060bf Dreaming UI: explain modes on hover in header controls 2026-04-03 22:08:49 -07:00
Peter Steinberger
b392c78bab fix(ci): align settings host test fixtures 2026-04-04 06:08:26 +01:00
Peter Steinberger
cff8b5bebd fix(agents): preserve acp and openai wrapper defaults 2026-04-04 14:07:19 +09:00
Peter Steinberger
bc8048250e fix(agents): harden claude cli parsing and queueing 2026-04-04 14:07:19 +09:00
Peter Steinberger
4ed17fd987 refactor(voice-call): migrate legacy config via doctor 2026-04-04 14:06:52 +09:00
Vincent Koc
561db47566 docs(boundaries): add import-topology guardrails 2026-04-04 14:06:18 +09:00
Peter Steinberger
0777ddace8 perf: split more targeted test lanes 2026-04-04 06:05:24 +01:00
Peter Steinberger
5ddc57aa22 style(ui): format chat view templates 2026-04-04 06:02:57 +01:00
Peter Steinberger
64d9b65b56 style(core): format reply and infra helpers 2026-04-04 06:02:47 +01:00
Peter Steinberger
fd75d214f2 style(extensions): format channel integration updates 2026-04-04 06:02:37 +01:00
Peter Steinberger
8b5672bda4 test: align ui vitest configs with thread policy 2026-04-04 06:00:15 +01:00
Vignesh Natarajan
f8c4777515 Dreaming: move setup controls to header and tighten status plumbing 2026-04-03 21:58:46 -07:00
Vignesh Natarajan
a5f66b5c48 fix(plugins): constrain workspace discovery to .openclaw/extensions 2026-04-03 21:57:58 -07:00
Peter Steinberger
02cc09dafe test: refresh vitest config assertions 2026-04-04 05:57:27 +01:00
Peter Steinberger
ca9d2f3b41 ci: align vitest entrypoints with root config 2026-04-04 05:57:27 +01:00
Peter Steinberger
757a20b656 test: enforce thread-first vitest configs 2026-04-04 05:57:26 +01:00
Peter Steinberger
33e10c4772 fix(ci): repair bundled test selection and compat typing 2026-04-04 05:56:55 +01:00
Vincent Koc
230a39797a fix(infra): break exec safe-bin import cycle 2026-04-04 13:53:32 +09:00
Peter Steinberger
8a3d946f4a test: cover vitest contention scheduling 2026-04-04 05:51:27 +01:00
Peter Steinberger
55812eaf14 fix: throttle vitest under local contention 2026-04-04 05:50:46 +01:00
Vincent Koc
9afaec1b0c docs(changelog): add cache-prefix attribution 2026-04-04 13:47:43 +09:00
Peter Steinberger
53fd262173 ci: align pnpm pins and vitest config 2026-04-04 05:44:29 +01:00
Peter Steinberger
22e6225dd0 perf: split hooks, tui, and extension lanes 2026-04-04 05:38:47 +01:00
Peter Steinberger
af102907c5 docs: add GitHub sponsor to README 2026-04-04 13:36:58 +09:00
Peter Steinberger
39135ca3a4 refactor(voice-call): isolate config compatibility 2026-04-04 13:34:05 +09:00
Vincent Koc
64f28906de fix(agents): split system prompt cache prefix by transport (#59054)
* fix(agents): restore Anthropic prompt cache seam

* fix(agents): strip cache boundary for completions

* fix(agents): strip cache boundary for cli backends

* chore(changelog): note cross-transport cache boundary rollout

* fix(agents): route default stream fallbacks through boundary shapers

* fix(agents): strip cache boundary for provider streams
2026-04-04 13:32:32 +09:00
Peter Steinberger
b0e1551eb8 refactor(extensions): add channel-owned config schema seams 2026-04-04 05:31:11 +01:00
Peter Steinberger
c17985aa9f test: align hook install unsafe flag assertion 2026-04-04 05:27:57 +01:00
Peter Steinberger
e95b723b82 fix: load telegram command config from contract surfaces 2026-04-04 05:26:54 +01:00
Peter Steinberger
c7cb43cac9 perf: split more scoped vitest lanes 2026-04-04 05:26:32 +01:00
Peter Steinberger
64b971b2b0 fix: resolve config write test drift 2026-04-04 05:25:57 +01:00
Peter Steinberger
3a62b0e75b fix(ci): remove invalid live cache reasoning flag 2026-04-04 05:24:29 +01:00
Peter Steinberger
b16e70e37f refactor(plugins): route bundled channel config runtime through metadata 2026-04-04 05:20:43 +01:00
Peter Steinberger
5b294b7fbd test: keep vitest thread workers conservative 2026-04-04 05:20:19 +01:00
Peter Steinberger
943da1864a test: add tool-turn cache coverage 2026-04-04 13:19:00 +09:00
Peter Steinberger
53b5b1b32d fix(ci): repair redundant channel union types 2026-04-04 05:08:02 +01:00
Peter Steinberger
1246e2b03a refactor(extensions): move channel-specific config surfaces out of core 2026-04-04 05:06:32 +01:00
Peter Steinberger
0f544fa1ca fix(ci): repair bluebubbles status test import 2026-04-04 05:03:19 +01:00
Peter Steinberger
39d3cad479 fix(ci): repair check lane type drift 2026-04-04 04:59:18 +01:00
Peter Steinberger
e277ac0838 fix: defer command secret target registry loading 2026-04-04 04:58:09 +01:00
Peter Steinberger
f84486157e refactor(channels): remove bluebubbles core status collector 2026-04-04 04:53:38 +01:00
Peter Steinberger
bc457fd1b8 refactor(channels): move bootstrap channel logic behind extension seams 2026-04-04 04:53:38 +01:00
Peter Steinberger
fff7e610df feat(plugins): auto-load provider plugins from model support 2026-04-04 04:52:25 +01:00
Peter Steinberger
5b144655f2 test(ci): align channel defaults and clean stale hook tests 2026-04-04 04:51:33 +01:00
Peter Steinberger
f4fa53de3f fix(ci): repair zalouser sdk path and exec timeout kill 2026-04-04 04:51:33 +01:00
Peter Steinberger
ca99ad0af8 test: add live cache provider probes 2026-04-04 12:46:10 +09:00
Peter Steinberger
efefa5560d perf: optimize vitest jsdom and isolated lanes 2026-04-04 04:45:01 +01:00
Marcus Castro
9d1a58f551 fix(auto-reply): preserve reasoning markers during block coalescing (#60655)
* fix: preserve reasoning markers during block coalescing

* docs(changelog): add auto-reply reasoning fix entry
2026-04-04 00:44:11 -03:00
Peter Steinberger
ed0cbcba2f refactor(voice-call): use config for realtime tuning 2026-04-04 12:43:23 +09:00
@zimeg
e636ba6ab0 docs(slack): move slash command settings to matching section 2026-04-03 20:42:23 -07:00
Peter Steinberger
32ba917079 perf: split infra, tooling, and provider test lanes 2026-04-04 04:39:47 +01:00
Vignesh Natarajan
f62db7950a fix(control-ui): keep session key helpers browser-safe 2026-04-03 20:39:36 -07:00
Vincent Koc
b7ec90258b fix(plugins): preserve bundled origin when workspace matches bundled root 2026-04-04 12:38:43 +09:00
Peter Steinberger
0ad75cffe3 test: restore native root vitest entrypoint 2026-04-04 04:37:08 +01:00
Peter Steinberger
bb1cc84d50 test: default vitest root projects to threads 2026-04-04 04:37:08 +01:00
Vincent Koc
fb5066dfb1 refactor(zalouser): lazy-load account runtimes 2026-04-04 12:36:39 +09:00
Peter Steinberger
6b003a7f2b refactor(cli): reuse install safety overrides 2026-04-04 12:35:58 +09:00
Peter Steinberger
406f06dcc5 fix: preserve linked install unsafe flag and baseline regressions 2026-04-04 12:34:55 +09:00
JD Davis
8a8ea94228 CLI: forward unsafe flag to linked hook-pack probes 2026-04-04 12:34:55 +09:00
JD Davis
bac15a7313 CLI: pass unsafe flag through linked plugin probes 2026-04-04 12:34:55 +09:00
Peter Steinberger
7cd40ad565 refactor(voice-call): clean provider boundaries 2026-04-04 12:33:47 +09:00
Vincent Koc
6964e4acf7 refactor(discord): lazy-load action and audit runtimes 2026-04-04 12:32:21 +09:00
Peter Steinberger
a82bc7d887 fix(ci): align contract expectations 2026-04-04 12:29:11 +09:00
Peter Steinberger
df48a7bfc0 fix: resolve stale plugin-sdk and test type regressions 2026-04-04 04:28:59 +01:00
Peter Steinberger
eb9051cc7c refactor(openai): move native transport policy into extension 2026-04-04 04:27:14 +01:00
Peter Steinberger
585b1c9413 fix(ci): repair openai codex provider test syntax 2026-04-04 04:27:02 +01:00
Vignesh
4c1022c73b feat(memory-core): add dreaming promotion with weighted recall thresholds (#60569)
* memory-core: add dreaming promotion flow with weighted thresholds

* docs(memory): mark dreaming as experimental

* memory-core: address dreaming promotion review feedback

* memory-core: harden short-term promotion concurrency

* acpx: make abort-process test timer-independent

* memory-core: simplify dreaming config with mode presets

* memory-core: add /dreaming command and tighten recall tracking

* ui: add Dreams tab with sleeping lobster animation

Adds a new Dreams tab to the gateway UI under the Agent group.
The tab is gated behind the memory-core dreaming config — it only
appears in the sidebar when dreaming.mode is not 'off'.

Features:
- Sleeping vector lobster with breathing animation
- Floating Z's, twinkling starfield, moon glow
- Rotating dream phrase bubble (17 whimsical phrases)
- Memory stats bar (short-term, long-term, promoted)
- Active/idle visual states
- 14 unit tests

* plugins: fix --json stdout pollution from hook runner log

The hook runner initialization message was using log.info() which
writes to stdout via console.log, breaking JSON.parse() in the
Docker smoke test for 'openclaw plugins list --json'. Downgrade to
log.debug() so it only appears when debugging is enabled.

* ui: keep Dreams tab visible when dreaming is off

* tests: fix contracts and stabilize extension shards

* memory-core: harden dreaming recall persistence and locking

* fix: stabilize dreaming PR gates (#60569) (thanks @vignesh07)

* test: fix rebase drift in telegram and plugin guards
2026-04-03 20:26:53 -07:00
Vincent Koc
2687a49575 refactor(line): lazy-load channel runtime seams 2026-04-04 12:26:20 +09:00
Peter Steinberger
eeb2888f6e fix(ci): sync openai provider lockfile 2026-04-04 04:24:31 +01:00
Ayaan Zaidi
d7b8faa7bf fix: keep Kimi anthropic tool payloads native (#60391) (thanks @Eric-Guo) 2026-04-04 08:53:57 +05:30
Peter Steinberger
41e16a883b fix(cli): honor unsafe override for linked installs 2026-04-04 12:22:49 +09:00
Peter Steinberger
2416e2d51d fix(ci): repair seam drift and matrix test timing 2026-04-04 04:22:17 +01:00
Peter Steinberger
d7ba6d3e68 test: move vitest config regression under active unit surface 2026-04-04 04:19:08 +01:00
Peter Steinberger
33453838da perf: route test commands through scoped lanes 2026-04-04 04:18:10 +01:00
tmimmanuel
0fef95b17d fix: preserve Windows scheduled task restart/install behavior (#59335) (thanks @tmimmanuel)
* fix(daemon): preserve Windows Task Scheduler settings on reinstall and exit early on failed restart

* fix(daemon): add test coverage for Create/Change paths, fix early exit grace period

* fix(daemon): fix startup-fallback tests for new isRegisteredScheduledTask call

* fix(daemon): report early restart failure accurately

* fix: preserve Windows scheduled task restart/install behavior (#59335) (thanks @tmimmanuel)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 08:46:00 +05:30
Peter Steinberger
ff0c1b57a7 fix(auth): respect externally managed codex refresh tokens 2026-04-04 04:12:05 +01:00
Vincent Koc
26c9a4ce63 fix(contracts): align runtime seams and codex expectations 2026-04-04 12:11:07 +09:00
Vincent Koc
fc79ebe098 refactor(zalouser): narrow channel runtime imports 2026-04-04 12:09:58 +09:00
Vincent Koc
20937422ca refactor(mattermost): narrow channel runtime imports 2026-04-04 12:09:54 +09:00
Vincent Koc
e750c10577 refactor(nostr): narrow channel runtime imports 2026-04-04 12:08:38 +09:00
Vincent Koc
ba20e6cd98 refactor(nextcloud-talk): narrow channel runtime imports 2026-04-04 12:08:38 +09:00
Vincent Koc
6349e6aa3e refactor(irc): narrow channel runtime imports 2026-04-04 12:08:38 +09:00
Vincent Koc
c4bae0f7bf refactor(msteams): narrow channel runtime imports 2026-04-04 12:08:38 +09:00
Peter Steinberger
a23ab9b906 refactor: move voice-call realtime providers into extensions 2026-04-04 12:07:23 +09:00
Vincent Koc
61f93540b2 refactor(discord): narrow channel runtime imports 2026-04-04 12:06:00 +09:00
Vincent Koc
9bfaf7b681 refactor(slack): narrow channel runtime imports 2026-04-04 12:06:00 +09:00
Peter Steinberger
7e69c2f6a7 test: trim remaining mock drift 2026-04-04 04:04:12 +01:00
Vincent Koc
2f5509e36d refactor(slack): lazy-load directory config seam 2026-04-04 12:03:14 +09:00
Vincent Koc
f9cf868553 refactor(slack): lazy-load target resolution seams 2026-04-04 12:02:58 +09:00
Vincent Koc
bc6b20e542 refactor(discord): lazy-load directory and resolver seams 2026-04-04 12:02:58 +09:00
Peter Steinberger
af94a3a89b test: use native vitest root projects 2026-04-04 04:01:32 +01:00
Vincent Koc
2050ef2740 refactor(whatsapp): lazy-load channel directory and action seams 2026-04-04 12:01:30 +09:00
Peter Steinberger
df86f4dc00 docs(changelog): reorder unreleased highlights 2026-04-04 12:00:10 +09:00
Vincent Koc
6c31b2fbc5 refactor(imessage): narrow channel runtime imports 2026-04-04 11:59:38 +09:00
Vincent Koc
0737816010 refactor(line): narrow channel runtime imports 2026-04-04 11:59:38 +09:00
Vincent Koc
e9d802c32b fix(openai): align gpt-5.4 codex context test 2026-04-04 11:59:05 +09:00
Peter Steinberger
94b0062e90 fix: keep local marketplace paths stable (#60556) (thanks @eleqtrizit) 2026-04-04 11:58:52 +09:00
Agustin Rivera
e8ebd6ab8c fix(marketplace): narrow canonical path checks 2026-04-04 11:58:52 +09:00
Agustin Rivera
750d963cb9 fix(marketplace): preserve local symlink installs 2026-04-04 11:58:52 +09:00
Agustin Rivera
b1dd3ded35 fix(marketplace): canonicalize remote plugin paths 2026-04-04 11:58:52 +09:00
Peter Steinberger
f25f147fc3 refactor(whatsapp): move legacy group session detection into contract surface 2026-04-04 03:57:56 +01:00
Vincent Koc
098abd484d fix(channels): keep feishu override parent fallbacks 2026-04-04 11:57:27 +09:00
Vincent Koc
5eb32f24ea refactor(discord): normalize lazy loader formatting 2026-04-04 11:53:21 +09:00
Vincent Koc
bf1b1d63bd refactor(discord): lazy-load channel send seams 2026-04-04 11:53:21 +09:00
Vincent Koc
e249a852ae refactor(slack): lazy-load async channel seams 2026-04-04 11:53:21 +09:00
Peter Steinberger
a3a06524f2 fix(ci): restore session and setup fallbacks 2026-04-04 03:52:37 +01:00
Peter Steinberger
3c23126980 fix(ci): tolerate missing contract surface roots 2026-04-04 03:52:37 +01:00
Peter Steinberger
6b3ff0dd4f feat(openai): add codex gpt-5.4-mini support 2026-04-04 11:51:57 +09:00
Vincent Koc
7df763b04d refactor(providers): share xai compat helper 2026-04-04 11:45:13 +09:00
Karl Yang
6d33c67c01 fix: enable groq and deepgram bundled media providers by default (#59982) (thanks @yxjsxy)
* fix: add enabledByDefault to groq and deepgram media plugin manifests

The groq and deepgram plugin manifests were missing the
enabledByDefault: true flag. Without this flag, both plugins are
treated as bundled-but-disabled-by-default, so resolveRuntimePluginRegistry
loads without them. When buildProviderRegistry later needs to resolve
audio providers, the active registry is used first (short-circuits
the compat path in resolvePluginCapabilityProviders), leaving groq
and deepgram absent from the registry.

This caused 'Media provider not available: groq' errors when users
configured tools.media.audio.models with groq or deepgram, even
with GROQ_API_KEY / DEEPGRAM_API_KEY set correctly.

The fix mirrors the pattern used by other audio/media-only providers
such as mistral, which already has enabledByDefault: true.

Fixes #59875

* fix: enable groq and deepgram bundled media providers by default (#59982) (thanks @yxjsxy)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 08:11:34 +05:30
Monty Taylor
d605cb08c5 matrix: force SSSS recreation on backup reset when SSSS key is broken (bad MAC) (#60599)
Merged via squash.

Prepared head SHA: 3b0a623407
Co-authored-by: emonty <95156+emonty@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 22:34:23 -04:00
Vincent Koc
fb1cb99c88 fix(xai): narrow stream wrapper params 2026-04-04 11:31:27 +09:00
Vincent Koc
b5a849801c chore(plugin-sdk): refresh api baseline 2026-04-04 11:30:30 +09:00
Vincent Koc
e273753d45 refactor(providers): share anthropic tool payload helper 2026-04-04 11:30:30 +09:00
George Zhang
87885b948a fix: handle sensitive, number-clear, and array-clear edge cases in plugin config TUI (#60640) (#60640)
- Skip sensitive fields with a note directing users to openclaw config set
  or the Web UI (WizardPrompter has no masked input)
- Clear number fields to undefined when input is empty instead of storing 0
- Allow clearing array fields to undefined via empty input
2026-04-03 19:27:26 -07:00
Vincent Koc
761bd3bbd0 refactor(providers): share passthrough replay helpers 2026-04-04 11:22:41 +09:00
Ayaan Zaidi
6a3a0c405f fix: replay interrupted recurring jobs on first restart (#60583) (thanks @joelnishanth) 2026-04-04 07:51:04 +05:30
joelnishanth
7a16e14301 fix(cron): resume interrupted recurring jobs on first restart (#60495) 2026-04-04 07:51:04 +05:30
Vincent Koc
9e389cff3d fix(config): migrate legacy group allow aliases (#60597)
* fix(config): migrate legacy group allow aliases

* fix(config): inline legacy streaming migration helpers

* refactor(config): rename legacy account matcher helper

* chore(agents): codify config contract boundaries

* fix(config): keep legacy allow aliases writable

* Update AGENTS.md
2026-04-04 11:15:32 +09:00
Ayaan Zaidi
945b198c76 fix(android): allow cleartext LAN gateways 2026-04-04 07:36:18 +05:30
Vincent Koc
94adc24393 chore(plugin-sdk): refresh api baseline 2026-04-04 11:03:28 +09:00
Vincent Koc
30479b4ee0 refactor(providers): compose provider stream wrappers 2026-04-04 11:03:28 +09:00
Michael Faath
85c76e83b7 fix: restore android talk mode reply tts (#60306) (thanks @MKV21)
* Android: keep talk-mode session key synced for TTS replies

* Android: harden talk-mode reply playback state

* Android: harden talk-mode playback cancellation

* Android: avoid stale talk-mode playback preemption

* Android: tighten talk-mode playback claiming

* fix: distill android talk-mode playback ownership

* fix: restore android talk mode reply tts (#60306) (thanks @MKV21)

---------

Co-authored-by: Michael Faath <michaelfaath@macbookpro.speedport.ip>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 07:28:56 +05:30
Vincent Koc
858bf405f4 refactor(providers): share replay and tool compat helpers (#60637)
* refactor(providers): share replay and tool compat helpers

* chore(plugin-sdk): refresh api baseline
2026-04-04 10:55:36 +09:00
Vincent Koc
dd31ee1139 fix(cli): log pending control ui build 2026-04-04 10:47:38 +09:00
Peter Steinberger
b76ed0fadf fix: harden OpenAI websocket transport 2026-04-04 02:38:36 +01:00
Peter Steinberger
1e6e685347 fix: unblock cli startup metadata 2026-04-04 02:35:36 +01:00
Peter Steinberger
143d377c5a fix(cli): keep status json startup lean 2026-04-04 02:16:56 +01:00
Gustavo Madeira Santana
3713b0e506 vertex: read ADC files without exists preflight (#60592)
Merged via squash.

Prepared head SHA: 72f7372e97
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 21:13:34 -04:00
Peter Steinberger
34cd49faa6 perf: route browser and line extension tests 2026-04-04 02:08:45 +01:00
Peter Steinberger
1e90b3afcd perf: split extension channel vitest lane 2026-04-04 02:08:45 +01:00
Peter Steinberger
e941d425ac perf: split acp and ui vitest lanes 2026-04-04 02:08:45 +01:00
Peter Steinberger
fb0ff6896a perf: route contract test targets 2026-04-04 02:08:45 +01:00
Peter Steinberger
b04c4e599c perf: route bundled and extension helper tests 2026-04-04 02:08:44 +01:00
Peter Steinberger
ac11e02518 perf: route bundled and extension helper tests 2026-04-04 02:08:44 +01:00
Peter Steinberger
269771a4b6 perf: route targeted tests to scoped vitest configs 2026-04-04 02:08:44 +01:00
Peter Steinberger
37ee19521f fix(status): keep empty status path lightweight 2026-04-04 10:02:42 +09:00
Peter Steinberger
f8a3840a42 fix(ci): restore contextTokens runtime typing 2026-04-04 02:00:19 +01:00
Gustavo Madeira Santana
931ddd96f0 fix(cache): preserve full 3-turn history image cache window (#60603)
Merged via squash.

Prepared head SHA: 58d06ea372
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 20:48:56 -04:00
Peter Steinberger
b8021d6709 docs: add prompt cache stability rules 2026-04-04 01:47:00 +01:00
Peter Steinberger
58d2b9dd46 fix: add runtime model contextTokens caps 2026-04-04 09:36:53 +09:00
Peter Steinberger
45675c1698 docs: update Anthropic subscription billing guidance 2026-04-04 09:32:13 +09:00
Peter Steinberger
b2fb1210e1 fix: normalize openai websocket errors 2026-04-04 01:31:49 +01:00
Peter Steinberger
a38cb20177 feat(openai): add default prompt overlay 2026-04-04 09:27:07 +09:00
Gustavo Madeira Santana
f6f7609b66 matrix: retry credentials after legacy migration race (#60591)
Merged via squash.

Prepared head SHA: e050b39de0
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 20:25:49 -04:00
Boris Cherny
af81c437fa fix(cache): delay history image pruning to preserve prompt cache prefix (#58038)
pruneProcessedHistoryImages was stripping image blocks from every
already-answered user turn on each run. Turn N sends image bytes → provider
caches the prefix. Turn N+1 replaces image with text marker → bytes diverge
at that message → cache miss from there onward.

Now only prune images older than 3 assistant turns. Recent history stays
byte-identical so the cached prefix survives, while legacy sessions with
persisted image payloads still get cleaned up.
2026-04-03 17:22:58 -07:00
Gustavo Madeira Santana
300fb36879 infra: atomically replace sync JSON writes (#60589)
Merged via squash.

Prepared head SHA: cb8ed77049
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 20:21:44 -04:00
Peter Steinberger
628c71103e fix: align native openai transport defaults 2026-04-04 01:20:34 +01:00
Boris Cherny
bc16b9dccf fix(cache): sort MCP tools deterministically to stabilize prompt cache (#58037)
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
2026-04-03 17:19:53 -07:00
George Zhang
881f7dc82f Plugin SDK: add plugin config TUI prompts to onboard and configure wizards (#60590) (#60590)
Wire uiHints from plugin manifests into the TUI wizard so sandbox/tool
plugins get interactive config prompts during openclaw onboard (manual
flow) and openclaw configure --section plugins.

- Add setup.plugin-config.ts: discovers plugins with non-advanced uiHints,
  generates type-aware prompts (enum→select, boolean→confirm, array→csv,
  string/number→text) from jsonSchema + uiHints metadata.
- Onboard: new step after Skills, before Hooks (skipped in QuickStart).
  Only shows plugins with unconfigured fields.
- Configure: new 'plugins' section in the section menu. Shows all
  configurable plugins with configured/total field counts.

Closes #60030
2026-04-03 17:19:19 -07:00
Boris Cherny
f6380ae4b7 fix(cache): compact newest tool results first to preserve prompt cache prefix (#58036)
* fix(cache): compact newest tool results first to preserve prompt cache prefix

compactExistingToolResultsInPlace iterated front-to-back, replacing the
oldest tool results with placeholders when context exceeded 75%. This
rewrote messages[k] for small k, invalidating the provider prompt cache
from that point onward on every subsequent turn.

Reverse the loop to compact newest-first. The cached prefix stays intact;
the tradeoff is the model loses recent tool output instead of old, which
is acceptable since this guard only fires as an emergency measure past
the 75% threshold.

* fix(cache): compact newest tool results first to preserve prompt cache prefix (#58036) Thanks @bcherny

---------

Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
2026-04-03 17:19:15 -07:00
5163 changed files with 291036 additions and 311048 deletions

View File

@@ -17,6 +17,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Per-phase logs land under `/tmp/openclaw-parallels-*`.
- Do not run local and gateway agent turns in parallel on the same fresh workspace or session.
- If `main` is moving under active multi-agent work, prefer a detached worktree pinned to one commit for long Parallels suites. The smoke scripts now verify the packed tgz commit instead of live `git rev-parse HEAD`, but a pinned worktree still avoids noisy rebuild/version drift during reruns.
- For `openclaw update --channel dev` lanes, remember the guest clones GitHub `main`, not your local worktree. If a local fix exists but the rerun still fails inside the cloned dev checkout, do not treat that as disproof of the fix until the branch has been pushed.
- For `prlctl exec`, pass the VM name before `--current-user` (`prlctl exec "$VM" --current-user ...`), not the other way around.
- If the workflow installs OpenClaw from a repo checkout instead of the site installer/npm release, finish by installing a real guest CLI shim and verifying it in a fresh guest shell. `pnpm openclaw ...` inside the repo is not enough for handoff parity.
- On macOS guests, prefer a user-global install plus a stable PATH-visible shim:
@@ -30,6 +31,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Flow: fresh snapshot -> install npm package baseline -> smoke -> install current main tgz on the same guest -> smoke again.
- Same-guest update verification should set the default model explicitly to `openai/gpt-5.4` before the agent turn and use a fresh explicit `--session-id` so old session model state does not leak into the check.
- The aggregate npm-update wrapper must resolve the Linux VM with the same Ubuntu fallback policy as `parallels-linux-smoke.sh` before both fresh and update lanes. Treat any Ubuntu guest with major version `>= 24` as acceptable when the exact default VM is missing, preferring the closest version match. On Peter's current host today, missing `Ubuntu 24.04.3 ARM64` should fall back to `Ubuntu 25.10`.
- On macOS same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; launchd can otherwise report a loaded service while the old process has exited and the fresh process is not RPC-ready yet.
- On Windows same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; in-place global npm updates can otherwise leave stale hashed `dist/*` module imports alive in the running service.
- For Windows same-guest update checks, prefer the done-file/log-drain PowerShell runner pattern over one long-lived `prlctl exec ... powershell -EncodedCommand ...` transport. The guest can finish successfully while the outer `prlctl exec` still hangs.
- The Windows same-guest update helper should write stage markers to its log before long steps like tgz download and `npm install -g` so the outer progress monitor does not sit on `waiting for first log line` during healthy but quiet installs.
@@ -44,8 +46,15 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
## macOS flow
- Preferred entrypoint: `pnpm test:parallels:macos`
- Default upgrade coverage on macOS should now include: fresh snapshot -> site installer pinned to the latest stable tag -> `openclaw update --channel dev` on the guest. Treat this as part of the default Tahoe regression plan, not an optional side quest.
- `parallels-macos-smoke.sh --mode upgrade` should run that release-to-dev lane by default. Keep the older host-tgz upgrade path only when the caller explicitly passes `--target-package-spec`.
- Because the default upgrade lane no longer needs a host tgz, skip `npm pack` + host HTTP server startup for `--mode upgrade` unless `--target-package-spec` is set. Keep the pack/server path for `fresh` and `both`.
- If that release-to-dev lane fails with `reason=preflight-no-good-commit` and repeated `sh: pnpm: command not found` tails from `preflight build`, treat it as an updater regression first. The fix belongs in the git/dev updater bootstrap path, not in Parallels retry logic.
- Until the public stable train includes that updater bootstrap fix, the macOS release-to-dev lane may seed a temporary guest-local `pnpm` shim immediately before `openclaw update --channel dev`. Keep that workaround scoped to the smoke harness and remove it once the latest stable no longer needs it.
- In Tahoe `prlctl exec --current-user` runs, prefer explicit `node .../openclaw.mjs ...` invocations for the release->dev handoff itself and for post-update verification. The shebanged global `openclaw` wrapper can fail with `env: node: No such file or directory`, and self-updating through the wrapper is a weaker lane than invoking the entrypoint under a fixed `node`.
- Default to the snapshot closest to `macOS 26.3.1 latest`.
- On Peter's Tahoe VM, `fresh-latest-march-2026` can hang in `prlctl snapshot-switch`; if restore times out there, rerun with `--snapshot-hint 'macOS 26.3.1 latest'` before blaming auth or the harness.
- `parallels-macos-smoke.sh` now retries `snapshot-switch` once after force-stopping a stuck running/suspended guest. If Tahoe still times out after that recovery path, then treat it as a real Parallels/host issue and rerun manually.
- The macOS smoke should include a dashboard load phase after gateway health: resolve the tokenized URL with `openclaw dashboard --no-open`, verify the served HTML contains the Control UI title/root shell, then open Safari and require an established localhost TCP connection from Safari to the gateway port.
- If a packaged install regresses with `500` on `/`, `/healthz`, or `__openclaw/control-ui-config.json` after `fresh.install-main` or `upgrade.install-main`, suspect bundled plugin runtime deps resolving from the package root `node_modules` rather than `dist/extensions/*/node_modules`. Repro quickly with a real `npm pack`/global install lane before blaming dashboard auth or Safari.
- `prlctl exec` is fine for deterministic repo commands, but use the guest Terminal or `prlctl enter` when installer parity or shell-sensitive behavior matters.
@@ -59,13 +68,25 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Preferred entrypoint: `pnpm test:parallels:windows`
- Use the snapshot closest to `pre-openclaw-native-e2e-2026-03-12`.
- Default upgrade coverage on Windows should now include: fresh snapshot -> site installer pinned to the requested stable tag -> `openclaw update --channel dev` on the guest. Keep the older host-tgz upgrade path only when the caller explicitly passes `--target-package-spec`.
- Optional exact npm-tag baseline on Windows: `bash scripts/e2e/parallels-windows-smoke.sh --mode upgrade --target-package-spec openclaw@<tag> --json`. That lane installs the published npm tarball as baseline, then runs `openclaw update --channel dev`.
- Optional forward-fix Windows validation: `bash scripts/e2e/parallels-windows-smoke.sh --mode upgrade --upgrade-from-packed-main --json`. That lane installs the packed current-main npm tgz as baseline, then runs `openclaw update --channel dev`.
- Always use `prlctl exec --current-user`; plain `prlctl exec` lands in `NT AUTHORITY\\SYSTEM`.
- Prefer explicit `npm.cmd` and `openclaw.cmd`.
- Use PowerShell only as the transport with `-ExecutionPolicy Bypass`, then call the `.cmd` shims from inside it.
- Current Windows Node installs expose `corepack` as a `.cmd` shim. If a release-to-dev lane sees `corepack` on PATH but `openclaw update --channel dev` still behaves as if corepack is missing, treat that as an exec-shim regression first.
- If an exact published-tag Windows lane fails during preflight with `npm run build` and `'pnpm' is not recognized`, remember that the guest is still executing the old published updater. Validate the fix with `--upgrade-from-packed-main`, then wait for the next tagged npm release before expecting the historical tag lane to pass.
- Multi-word `openclaw agent --message ...` checks should call `& $openclaw ...` inside PowerShell, not `Start-Process ... -ArgumentList` against `openclaw.cmd`, or Commander can see split argv and throw `too many arguments for 'agent'`.
- Windows installer/tgz phases now retry once after guest-ready recheck; keep new Windows smoke steps idempotent so a transport-flake retry is safe.
- If a Windows retry sees the VM become `suspended` or `stopped`, resume/start it before the next `prlctl exec`; otherwise the second attempt just repeats the same `rc=255`.
- Windows global `npm install -g` phases can stay quiet for a minute or more even when healthy; inspect the phase log before calling it hung, and only treat it as a regression once the retry wrapper or timeout trips.
- When those Windows global installs stay quiet, the useful progress often lives in the guest npm debug log, not the helper phase log. The smoke script now streams incremental `npm-cache/_logs/*-debug-0.log` deltas into the phase log during long baseline/package installs; read those lines before assuming the lane is stalled.
- The Windows baseline-package helpers now auto-dump the latest guest `npm-cache/_logs/*-debug-0.log` tail on timeout or nonzero completion. Read that tail in the phase log before opening a second guest shell.
- The same incremental npm-debug streaming also applies to `--upgrade-from-packed-main` / packaged-install baseline phases. A phase log that still says only `install.start`, `install.download-tgz`, `install.install-tgz` can still be healthy if the streamed npm-debug section shows registry fetches or bundled-plugin postinstall work.
- Fresh Windows tgz install phases should also use the background PowerShell runner plus done-file/log-drain pattern; do not rely on one long-lived `prlctl exec ... powershell ... npm install -g` transport for package installs.
- Windows release-to-dev helpers should log `where pnpm` before and after the update and require `where pnpm` to succeed post-update. That proves the updater installed or enabled `pnpm` itself instead of depending on a smoke-only bootstrap.
- Fresh Windows ref-mode onboard should use the same background PowerShell runner plus done-file/log-drain pattern as the npm-update helper, including startup materialization checks, host-side timeouts on short poll `prlctl exec` calls, and retry-on-poll-failure behavior for transient transport flakes.
- Fresh Windows daemon-health reachability should use a hello-only gateway probe and a longer per-probe timeout than the default local attach path; full health RPCs are too eager during initial startup on current main.
- Fresh Windows ref-mode agent verification should set `OPENAI_API_KEY` in the PowerShell environment before invoking `openclaw.cmd agent`, for the same pairing-required fallback reason as macOS.
- The standalone Windows upgrade smoke lane should stop the managed gateway after `upgrade.install-main` and before `upgrade.onboard-ref`. Restarting before onboard can leave the old process alive on the pre-onboard token while onboard rewrites `~/.openclaw/openclaw.json`, which then fails `gateway-health` with `unauthorized: gateway token mismatch`.
- If standalone Windows upgrade fails with a gateway token mismatch but `pnpm test:parallels:npm-update` passes, trust the mismatch as a standalone ref-onboard ordering bug first; the npm-update helper does not re-run ref-mode onboard on the same guest.
@@ -82,6 +103,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Fresh `main` tgz smoke still needs the latest-release installer first because the snapshot has no Node or npm before bootstrap.
- This snapshot does not have a usable `systemd --user` session; managed daemon install is unsupported.
- The Linux smoke now falls back to a manual `setsid openclaw gateway run --bind loopback --port 18789 --force` launch with `HOME=/root` and the provider secret exported, then verifies `gateway status --deep --require-rpc` when available.
- The Linux manual gateway launch should wait for `gateway status --deep --require-rpc` inside the `gateway-start` phase; otherwise the first status probe can race the background bind and fail a healthy lane.
- If Linux gateway bring-up fails, inspect `/tmp/openclaw-parallels-linux-gateway.log` in the guest phase logs first; the common failure mode is a missing provider secret in the launched gateway environment.
## Discord roundtrip

View File

@@ -0,0 +1,86 @@
---
name: openclaw-qa-testing
description: Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
---
# OpenClaw QA Testing
Use this skill for `qa-lab` / `qa-channel` work. Repo-local QA only.
## Read first
- `docs/concepts/qa-e2e-automation.md`
- `docs/help/testing.md`
- `docs/channels/qa-channel.md`
- `qa/QA_KICKOFF_TASK.md`
- `qa/seed-scenarios.json`
- `extensions/qa-lab/src/suite.ts`
## Model policy
- Live OpenAI lane: `openai/gpt-5.4`
- Fast mode: on
- Do not use:
- `openai/gpt-5.4-pro`
- `openai/gpt-5.4-mini`
- Only change model policy if the user explicitly asks.
## Default workflow
1. Read the seed plan and current suite implementation.
2. Decide lane:
- mock/dev: `mock-openai`
- real validation: `live-openai`
3. For live OpenAI, use:
```bash
OPENCLAW_LIVE_OPENAI_KEY="${OPENAI_API_KEY}" \
pnpm openclaw qa suite \
--provider-mode live-openai \
--model openai/gpt-5.4 \
--alt-model openai/gpt-5.4 \
--fast \
--output-dir .artifacts/qa-e2e/run-all-live-openai-<tag>
```
4. Watch outputs:
- summary: `.artifacts/qa-e2e/run-all-live-openai-<tag>/qa-suite-summary.json`
- report: `.artifacts/qa-e2e/run-all-live-openai-<tag>/qa-suite-report.md`
5. If the user wants to watch the live UI, find the current `openclaw-qa` listen port and report `http://127.0.0.1:<port>`.
6. If a scenario fails, fix the product or harness root cause, then rerun the full lane.
## Repo facts
- Seed scenarios live in `qa/`.
- Main live runner: `extensions/qa-lab/src/suite.ts`
- QA lab server: `extensions/qa-lab/src/lab-server.ts`
- Child gateway harness: `extensions/qa-lab/src/gateway-child.ts`
- Synthetic channel: `extensions/qa-channel/`
## What “done” looks like
- Full suite green for the requested lane.
- User gets:
- watch URL if applicable
- pass/fail counts
- artifact paths
- concise note on what was fixed
## Common failure patterns
- Live timeout too short:
- widen live waits in `extensions/qa-lab/src/suite.ts`
- Discovery cannot find repo files:
- point prompts at `repo/...` inside seeded workspace
- Subagent proof too brittle:
- prefer stable final reply evidence over transient child-session listing
- Harness “rebuild” delay:
- dirty tree can trigger a pre-run build; expect that before ports appear
## When adding scenarios
- Add scenario metadata to `qa/seed-scenarios.json`
- Keep kickoff expectations in `qa/QA_KICKOFF_TASK.md` aligned
- Add executable coverage in `extensions/qa-lab/src/suite.ts`
- Prefer end-to-end assertions over mock-only checks
- Save outputs under `.artifacts/qa-e2e/`

View File

@@ -0,0 +1,4 @@
interface:
display_name: "QA Test OpenClaw"
short_description: "Run and debug qa-lab and qa-channel scenarios"
default_prompt: "Use $openclaw-qa-testing to run or extend the OpenClaw QA suite with qa-lab and qa-channel, using regular openai/gpt-5.4 in fast mode for live OpenAI runs."

View File

@@ -55,6 +55,8 @@ Check in this order:
- Was it fixed before release?
3. Exploit path
- Does the report show a real boundary bypass, not just prompt injection, local same-user control, or helper-level semantics?
- If data only moves between trusted workspace-memory files called out in `SECURITY.md`, do not treat "injection markers" alone as a security bug.
- In that case, frame sanitization as optional hardening only if it preserves expected memory workflows.
4. Functional tradeoff
- If a hardening change would reduce intended user functionality, call that out before proposing it.
- Prefer fixes that preserve user workflows over deny-by-default regressions unless the boundary demands it.
@@ -104,5 +106,6 @@ gh search prs --repo openclaw/openclaw --match title,body,comments -- "<terms>"
- “fixed on main, unreleased” is usually not a close.
- “needs attacker-controlled trusted local state first” is usually out of scope.
- “same-host same-user process can already read/write local state” is usually out of scope.
- “trusted workspace memory promotes/reindexes trusted workspace memory” is usually out of scope unless it crosses a documented boundary.
- “helper function behaves differently than documented config semantics” is usually invalid.
- If only the severity is wrong but the bug is real, keep it open and narrow the impact in the reply.

View File

@@ -14,7 +14,7 @@ inputs:
pnpm-version:
description: pnpm version for corepack.
required: false
default: "10.23.0"
default: "10.32.1"
install-bun:
description: Whether to install Bun alongside Node.
required: false

View File

@@ -4,7 +4,7 @@ inputs:
pnpm-version:
description: pnpm version to activate via corepack.
required: false
default: "10.23.0"
default: "10.32.1"
cache-key-suffix:
description: Suffix appended to the cache key.
required: false

19
.github/labeler.yml vendored
View File

@@ -64,6 +64,17 @@
- any-glob-to-any-file:
- "extensions/qqbot/**"
- "docs/channels/qqbot.md"
"channel: qa-channel":
- changed-files:
- any-glob-to-any-file:
- "extensions/qa-channel/**"
- "docs/channels/qa-channel.md"
"extensions: qa-lab":
- changed-files:
- any-glob-to-any-file:
- "extensions/qa-lab/**"
- "docs/concepts/qa-e2e-automation.md"
- "docs/channels/qa-channel.md"
"channel: signal":
- changed-files:
- any-glob-to-any-file:
@@ -222,10 +233,18 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/memory-lancedb/**"
"extensions: memory-wiki":
- changed-files:
- any-glob-to-any-file:
- "extensions/memory-wiki/**"
"extensions: open-prose":
- changed-files:
- any-glob-to-any-file:
- "extensions/open-prose/**"
"extensions: webhooks":
- changed-files:
- any-glob-to-any-file:
- "extensions/webhooks/**"
"extensions: device-pair":
- changed-files:
- any-glob-to-any-file:

View File

@@ -407,8 +407,12 @@ jobs:
"Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.";
if (pullRequest) {
// `bad-barnacle` exempts PRs that Barnacle incorrectly marked dirty.
if (labelSet.has(dirtyLabel) && !labelSet.has(badBarnacleLabel)) {
if (labelSet.has(badBarnacleLabel)) {
core.info(`Skipping PR auto-response checks for #${pullRequest.number} because ${badBarnacleLabel} is present.`);
return;
}
if (labelSet.has(dirtyLabel)) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,

View File

@@ -46,6 +46,7 @@ jobs:
run_check_additional: ${{ steps.manifest.outputs.run_check_additional }}
run_build_smoke: ${{ steps.manifest.outputs.run_build_smoke }}
run_check_docs: ${{ steps.manifest.outputs.run_check_docs }}
run_control_ui_i18n: ${{ steps.manifest.outputs.run_control_ui_i18n }}
run_checks_windows: ${{ steps.manifest.outputs.run_checks_windows }}
checks_windows_matrix: ${{ steps.manifest.outputs.checks_windows_matrix }}
run_macos_node: ${{ steps.manifest.outputs.run_macos_node }}
@@ -128,6 +129,7 @@ jobs:
OPENCLAW_CI_RUN_ANDROID: ${{ steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ steps.changed_scope.outputs.run_skills_python || 'false' }}
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
run: |
@@ -165,6 +167,8 @@ jobs:
const runAndroid = parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly;
const runWindows = parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) && !docsOnly;
const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly;
const runControlUiI18n =
parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly;
const hasChangedExtensions =
parseBoolean(process.env.OPENCLAW_CI_HAS_CHANGED_EXTENSIONS) && !docsOnly;
const changedExtensionsMatrix = hasChangedExtensions
@@ -241,6 +245,7 @@ jobs:
run_check_additional: runNode,
run_build_smoke: runNode,
run_check_docs: docsChanged,
run_control_ui_i18n: runControlUiI18n,
run_skills_python_job: runSkillsPython,
run_checks_windows: runWindows,
checks_windows_matrix: createMatrix(
@@ -545,7 +550,6 @@ jobs:
echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV"
if [ "$TASK" = "channels" ]; then
echo "OPENCLAW_VITEST_MAX_WORKERS=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_ISOLATE=1" >> "$GITHUB_ENV"
fi
- name: Download dist artifact
@@ -739,11 +743,27 @@ jobs:
continue-on-error: true
run: pnpm run lint:extensions:no-relative-outside-package
- name: Run extension channel lint
id: extension_channel_lint
continue-on-error: true
run: pnpm run lint:extensions:channels
- name: Run bundled extension lint
id: extension_bundled_lint
continue-on-error: true
run: pnpm run lint:extensions:bundled
- name: Enforce safe external URL opening policy
id: no_raw_window_open
continue-on-error: true
run: pnpm lint:ui:no-raw-window-open
- name: Check control UI locale sync
id: control_ui_i18n
if: needs.preflight.outputs.run_control_ui_i18n == 'true'
continue-on-error: true
run: pnpm ui:i18n:check
- name: Run gateway watch regression harness
id: gateway_watch_regression
continue-on-error: true
@@ -775,7 +795,10 @@ jobs:
EXTENSION_SRC_OUTSIDE_PLUGIN_SDK_BOUNDARY_OUTCOME: ${{ steps.extension_src_outside_plugin_sdk_boundary.outcome }}
EXTENSION_PLUGIN_SDK_INTERNAL_BOUNDARY_OUTCOME: ${{ steps.extension_plugin_sdk_internal_boundary.outcome }}
EXTENSION_RELATIVE_OUTSIDE_PACKAGE_BOUNDARY_OUTCOME: ${{ steps.extension_relative_outside_package_boundary.outcome }}
EXTENSION_CHANNEL_LINT_OUTCOME: ${{ steps.extension_channel_lint.outcome }}
EXTENSION_BUNDLED_LINT_OUTCOME: ${{ steps.extension_bundled_lint.outcome }}
NO_RAW_WINDOW_OPEN_OUTCOME: ${{ steps.no_raw_window_open.outcome }}
CONTROL_UI_I18N_OUTCOME: ${{ steps.control_ui_i18n.outcome == 'skipped' && 'success' || steps.control_ui_i18n.outcome }}
GATEWAY_WATCH_REGRESSION_OUTCOME: ${{ steps.gateway_watch_regression.outcome }}
run: |
failures=0
@@ -795,7 +818,10 @@ jobs:
"extension-src-outside-plugin-sdk-boundary|$EXTENSION_SRC_OUTSIDE_PLUGIN_SDK_BOUNDARY_OUTCOME" \
"extension-plugin-sdk-internal-boundary|$EXTENSION_PLUGIN_SDK_INTERNAL_BOUNDARY_OUTCOME" \
"extension-relative-outside-package-boundary|$EXTENSION_RELATIVE_OUTSIDE_PACKAGE_BOUNDARY_OUTCOME" \
"lint:extensions:channels|$EXTENSION_CHANNEL_LINT_OUTCOME" \
"lint:extensions:bundled|$EXTENSION_BUNDLED_LINT_OUTCOME" \
"lint:ui:no-raw-window-open|$NO_RAW_WINDOW_OPEN_OUTCOME" \
"ui:i18n:check|$CONTROL_UI_I18N_OUTCOME" \
"gateway-watch-regression|$GATEWAY_WATCH_REGRESSION_OUTCOME"; do
name="${result%%|*}"
outcome="${result#*|}"
@@ -950,7 +976,7 @@ jobs:
- name: Setup pnpm + cache store
uses: ./.github/actions/setup-pnpm-store-cache
with:
pnpm-version: "10.23.0"
pnpm-version: "10.32.1"
cache-key-suffix: "node24"
# Sticky disk mount currently retries/fails on every shard and adds ~50s
# before install while still yielding zero pnpm store reuse.

View File

@@ -0,0 +1,172 @@
name: Control UI Locale Refresh
on:
push:
branches:
- main
paths:
- ui/src/i18n/locales/en.ts
- ui/src/i18n/locales/*.ts
- ui/src/i18n/.i18n/*
- ui/src/i18n/lib/types.ts
- ui/src/i18n/lib/registry.ts
- scripts/control-ui-i18n.ts
- .github/workflows/control-ui-locale-refresh.yml
release:
types:
- published
schedule:
- cron: "23 4 * * *"
workflow_dispatch:
permissions:
contents: write
concurrency:
group: control-ui-locale-refresh
cancel-in-progress: false
jobs:
plan:
if: github.repository == 'openclaw/openclaw' && (github.event_name != 'push' || github.actor != 'github-actions[bot]')
runs-on: ubuntu-latest
outputs:
has_locales: ${{ steps.plan.outputs.has_locales }}
locales_json: ${{ steps.plan.outputs.locales_json }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
submodules: false
- name: Plan locale matrix
id: plan
env:
BEFORE_SHA: ${{ github.event.before }}
EVENT_NAME: ${{ github.event_name }}
run: |
set -euo pipefail
all_locales_json='["zh-CN","zh-TW","pt-BR","de","es","ja-JP","ko","fr","tr","uk","id","pl"]'
if [ "$EVENT_NAME" != "push" ]; then
echo "has_locales=true" >> "$GITHUB_OUTPUT"
echo "locales_json=$all_locales_json" >> "$GITHUB_OUTPUT"
exit 0
fi
before_ref="$BEFORE_SHA"
if [ -z "$before_ref" ] || [ "$before_ref" = "0000000000000000000000000000000000000000" ]; then
before_ref="$(git rev-parse HEAD^)"
fi
changed_files="$(git diff --name-only "$before_ref" HEAD)"
echo "changed files:"
printf '%s\n' "$changed_files"
if printf '%s\n' "$changed_files" | grep -Eq '^(ui/src/i18n/locales/en\.ts|ui/src/i18n/lib/types\.ts|ui/src/i18n/lib/registry\.ts|scripts/control-ui-i18n\.ts|\.github/workflows/control-ui-locale-refresh\.yml)$'; then
echo "has_locales=true" >> "$GITHUB_OUTPUT"
echo "locales_json=$all_locales_json" >> "$GITHUB_OUTPUT"
exit 0
fi
locales_json="$(printf '%s\n' "$changed_files" | node <<'EOF'
const fs = require("node:fs");
const changed = fs.readFileSync(0, "utf8").split(/\r?\n/).filter(Boolean);
const locales = new Set();
for (const file of changed) {
let match = file.match(/^ui\/src\/i18n\/locales\/(.+)\.ts$/);
if (match && match[1] !== "en") {
locales.add(match[1]);
continue;
}
match = file.match(/^ui\/src\/i18n\/\.i18n\/(.+)\.(?:meta\.json|tm\.jsonl)$/);
if (match) {
locales.add(match[1]);
}
}
process.stdout.write(JSON.stringify([...locales]));
EOF
)"
if [ "$locales_json" = "[]" ]; then
echo "has_locales=false" >> "$GITHUB_OUTPUT"
echo "locales_json=[]" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "has_locales=true" >> "$GITHUB_OUTPUT"
echo "locales_json=$locales_json" >> "$GITHUB_OUTPUT"
refresh:
needs: plan
if: github.repository == 'openclaw/openclaw' && needs.plan.outputs.has_locales == 'true'
strategy:
fail-fast: false
max-parallel: 4
matrix:
locale: ${{ fromJson(needs.plan.outputs.locales_json) }}
runs-on: ubuntu-latest
name: Refresh ${{ matrix.locale }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: true
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "false"
- name: Ensure translation provider secrets exist
env:
OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
set -euo pipefail
if [ -z "${OPENAI_API_KEY:-}" ] && [ -z "${ANTHROPIC_API_KEY:-}" ]; then
echo "Missing OPENCLAW_DOCS_I18N_OPENAI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY secret."
exit 1
fi
- name: Refresh control UI locale files
env:
OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENCLAW_CONTROL_UI_I18N_MODEL: gpt-5.4
OPENCLAW_CONTROL_UI_I18N_THINKING: low
run: node --import tsx scripts/control-ui-i18n.ts sync --locale "${{ matrix.locale }}" --write
- name: Commit and push locale updates
env:
LOCALE: ${{ matrix.locale }}
TARGET_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
if git diff --quiet -- ui/src/i18n; then
echo "No control UI locale changes for ${LOCALE}."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A ui/src/i18n
git commit --no-verify -m "chore(ui): refresh ${LOCALE} control ui locale"
for attempt in 1 2 3 4 5; do
git fetch origin "${TARGET_BRANCH}"
git rebase --autostash "origin/${TARGET_BRANCH}"
if git push origin HEAD:"${TARGET_BRANCH}"; then
exit 0
fi
echo "Push attempt ${attempt} for ${LOCALE} failed; retrying."
sleep $((attempt * 2))
done
echo "Failed to push ${LOCALE} locale update after retries."
exit 1

70
.github/workflows/docs-sync-publish.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
name: Docs Sync Publish Repo
on:
push:
branches:
- main
paths:
- docs/**
- scripts/docs-sync-publish.mjs
- .github/workflows/docs-sync-publish.yml
workflow_dispatch:
permissions:
contents: read
jobs:
sync-publish-repo:
runs-on: ubuntu-latest
steps:
- name: Checkout source repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Clone publish repo
env:
OPENCLAW_DOCS_SYNC_TOKEN: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN }}
run: |
set -euo pipefail
git clone \
"https://x-access-token:${OPENCLAW_DOCS_SYNC_TOKEN}@github.com/openclaw/docs.git" \
publish
- name: Sync docs into publish repo
run: |
node scripts/docs-sync-publish.mjs \
--target "$GITHUB_WORKSPACE/publish" \
--source-repo "$GITHUB_REPOSITORY" \
--source-sha "$GITHUB_SHA"
- name: Commit publish repo sync
working-directory: publish
run: |
set -euo pipefail
if git diff --quiet -- docs .openclaw-sync; then
echo "No publish-repo changes."
exit 0
fi
git config user.name "openclaw-docs-sync[bot]"
git config user.email "openclaw-docs-sync[bot]@users.noreply.github.com"
git add docs .openclaw-sync
git commit -m "chore(sync): mirror docs from $GITHUB_REPOSITORY@$GITHUB_SHA"
for attempt in 1 2 3 4 5; do
git fetch origin main
git rebase origin/main
if git push origin HEAD:main; then
exit 0
fi
echo "Push attempt ${attempt} failed; retrying."
sleep $((attempt * 2))
done
echo "Failed to push publish-repo sync after retries."
exit 1

View File

@@ -0,0 +1,42 @@
name: Docs Trigger Locale Translate On Release
on:
release:
types:
- published
permissions:
contents: read
jobs:
dispatch-translate:
runs-on: ubuntu-latest
steps:
- name: Trigger locale translates in publish repo
env:
GH_TOKEN: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
for event_type in \
translate-zh-cn-release \
translate-ja-jp-release \
translate-es-release \
translate-pt-br-release \
translate-ko-release \
translate-de-release \
translate-fr-release \
translate-ar-release \
translate-it-release \
translate-tr-release \
translate-uk-release \
translate-id-release \
translate-pl-release
do
gh api repos/openclaw/docs/dispatches \
--method POST \
-f event_type="${event_type}" \
-f client_payload[release_tag]="${RELEASE_TAG}" \
-f client_payload[source_repository]="${GITHUB_REPOSITORY}" \
-f client_payload[source_sha]="${GITHUB_SHA}"
done

View File

@@ -20,7 +20,7 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.23.0"
PNPM_VERSION: "10.32.1"
jobs:
validate_macos_release_request:

View File

@@ -37,7 +37,7 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.23.0"
PNPM_VERSION: "10.32.1"
jobs:
preflight_openclaw_npm:
@@ -78,7 +78,7 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
install-bun: "true"
use-sticky-disk: "false"
- name: Ensure version is not already published
@@ -129,6 +129,31 @@ jobs:
- name: Verify release contents
run: pnpm release:check
- name: Validate live cache credentials
if: ${{ github.ref == 'refs/heads/main' }}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY}" ]]; then
echo "Missing OPENAI_API_KEY secret for release live cache validation." >&2
exit 1
fi
if [[ -z "${ANTHROPIC_API_KEY}" ]]; then
echo "Missing ANTHROPIC_API_KEY secret for release live cache validation." >&2
exit 1
fi
- name: Verify live prompt cache floors
if: ${{ github.ref == 'refs/heads/main' }}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_LIVE_CACHE_TEST: "1"
OPENCLAW_LIVE_TEST: "1"
run: pnpm test:live:cache
- name: Pack prepared npm tarball
id: packed_tarball
env:

View File

@@ -23,7 +23,7 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.23.0"
PNPM_VERSION: "10.32.1"
CLAWHUB_REGISTRY: "https://clawhub.ai"
CLAWHUB_REPOSITORY: "openclaw/clawhub"
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.

View File

@@ -38,7 +38,7 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.23.0"
PNPM_VERSION: "10.32.1"
jobs:
preview_plugins_npm:

9
.gitignore vendored
View File

@@ -4,7 +4,7 @@ node_modules
docker-compose.override.yml
docker-compose.extra.yml
dist
dist-runtime
dist-runtime/
pnpm-lock.yaml
bun.lock
bun.lockb
@@ -136,10 +136,17 @@ ui/src/ui/views/__screenshots__
ui/.vitest-attachments
docs/superpowers
# Generated docs baseline artifacts (locally generated, only hashes tracked)
docs/.generated/*.json
docs/.generated/*.jsonl
# Deprecated changelog fragment workflow
changelog/fragments/
# Local scratch workspace
.tmp/
.artifacts/
test/fixtures/openclaw-vitest-unit-report.json
analysis/
.artifacts/qa-e2e/
extensions/qa-lab/web/dist/

View File

@@ -25,8 +25,8 @@
"ignorePatterns": [
"assets/",
"dist/",
"dist-runtime/",
"docs/_layouts/",
"extensions/",
"node_modules/",
"patches/",
"pnpm-lock.yaml",
@@ -34,6 +34,36 @@
"src/auto-reply/reply/export-html/template.js",
"src/canvas-host/a2ui/a2ui.bundle.js",
"Swabble/",
"vendor/"
"vendor/",
"**/.cache/**",
"**/build/**",
"**/coverage/**",
"**/dist/**",
"**/dist-runtime/**",
"**/node_modules/**"
],
"overrides": [
{
"files": [
"**/*.test.ts",
"**/*.test.tsx",
"**/*.e2e.test.ts",
"**/*.live.test.ts",
"**/*test-harness.ts",
"**/*test-helpers.ts",
"**/*test-support.ts"
],
"rules": {
"typescript/await-thenable": "off",
"typescript/no-base-to-string": "off",
"typescript/no-explicit-any": "off",
"typescript/no-floating-promises": "off",
"typescript/no-misused-spread": "off",
"typescript/no-redundant-type-constituents": "off",
"typescript/no-unnecessary-template-expression": "off",
"typescript/unbound-method": "off",
"eslint/no-unsafe-optional-chaining": "off"
}
}
]
}

View File

@@ -38,8 +38,14 @@
- Plugin and extension boundary:
- Public docs: `docs/plugins/building-plugins.md`, `docs/plugins/architecture.md`, `docs/plugins/sdk-overview.md`, `docs/plugins/sdk-entrypoints.md`, `docs/plugins/sdk-runtime.md`, `docs/plugins/manifest.md`, `docs/plugins/sdk-channel-plugins.md`, `docs/plugins/sdk-provider-plugins.md`
- Definition files: `src/plugin-sdk/plugin-entry.ts`, `src/plugin-sdk/core.ts`, `src/plugin-sdk/provider-entry.ts`, `src/plugin-sdk/channel-contract.ts`, `scripts/lib/plugin-sdk-entrypoints.json`, `package.json`
- Invariant: core must stay extension-agnostic. Adding a bundled or third-party extension should not require unrelated core edits just to teach core that the extension exists.
- Rule: extensions must cross into core only through `openclaw/plugin-sdk/*`, manifest metadata, and documented runtime helpers. Do not import `src/**` from extension production code.
- Rule: core code and tests must not deep-import bundled plugin internals such as a plugin's `src/**` files or `onboard.js`. If core needs a bundled plugin helper, expose it through that plugin's `api.ts` and, when it is a real cross-package contract, through `src/plugin-sdk/<id>.ts`.
- Rule: do not add hardcoded bundled extension/provider/channel/capability id lists, maps, or named special cases in core when a manifest, capability, registry, or plugin-owned contract can express the same behavior.
- Rule: extension-owned compatibility behavior belongs to the owning extension. Core may orchestrate generic doctor/config flows, but extension-specific legacy repairs, detection rules, onboarding, auth detection, and provider defaults should live in plugin-owned contracts.
- Rule: for legacy config specifically, prefer doctor-owned repair paths over startup/load-time core migrations. Do not add new plugin-specific legacy migration logic to shared core/runtime surfaces when `openclaw doctor --fix` can own it.
- Rule: when a test is asserting extension-specific behavior, keep that coverage in the owning extension when feasible. Core tests should assert generic contracts and registry/capability behavior, not extension internals.
- Refactor trigger: if you encounter core code or tests that name a specific extension/provider/channel for extension-owned behavior, refactor toward a generic registry/capability/plugin-owned seam instead of adding another special case.
- Compatibility: new plugin seams are allowed, but they must be added as documented, backwards-compatible, versioned contracts. We have third-party plugins in the wild and do not break them casually.
- Channel boundary:
- Public docs: `docs/plugins/sdk-channel-plugins.md`, `docs/plugins/architecture.md`
@@ -55,6 +61,11 @@
- Public docs: `docs/gateway/protocol.md`, `docs/gateway/bridge-protocol.md`, `docs/concepts/architecture.md`
- Definition files: `src/gateway/protocol/schema.ts`, `src/gateway/protocol/schema/*.ts`, `src/gateway/protocol/index.ts`
- Rule: protocol changes are contract changes. Prefer additive evolution; incompatible changes require explicit versioning, docs, and client/codegen follow-through.
- Config contract boundary:
- Canonical public config lives in exported config types, zod/schema surfaces, schema help/labels, generated config metadata, config baselines, and any user-facing gateway/config payloads. Keep those surfaces aligned.
- When a legacy config key is retired from the public contract, remove it from every public config surface above. Keep backward compatibility only through raw-config migration/doctor seams unless explicit product policy says otherwise.
- Do not reintroduce removed legacy aliases into public types/schema/help/baselines “for convenience”. If old configs still need to load, handle that in `legacy.migrations.*`, config ingest, or `openclaw doctor --fix`.
- `hooks.internal.entries` is the canonical public hook config model. `hooks.internal.handlers` is compatibility-only input and must not be re-exposed in public schema/help/baseline surfaces.
- Bundled plugin contract boundary:
- Public docs: `docs/plugins/architecture.md`, `docs/plugins/manifest.md`, `docs/plugins/sdk-overview.md`
- Definition files: `src/plugins/contracts/registry.ts`, `src/plugins/types.ts`, `src/plugins/public-artifacts.ts`
@@ -62,6 +73,7 @@
- Extension test boundary:
- Keep extension-owned onboarding/config/provider coverage under the owning bundled plugin package when feasible.
- If core tests need bundled plugin behavior, consume it through public `src/plugin-sdk/<id>.ts` facades or the plugin's `api.ts`, not private extension modules.
- If a core test is asserting extension-specific behavior instead of a generic contract, move it to the owning extension package.
## Docs Linking (Mintlify)
@@ -76,16 +88,24 @@
- README (GitHub): keep absolute docs URLs (`https://docs.openclaw.ai/...`) so links work on GitHub.
- Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and “gateway host”.
## Docs i18n (zh-CN)
## Docs i18n (generated publish locales)
- `docs/zh-CN/**` is generated; do not edit unless the user explicitly asks.
- Pipeline: update English docs → adjust glossary (`docs/.i18n/glossary.zh-CN.json`) → run `scripts/docs-i18n` → apply targeted fixes only if instructed.
- Foreign-language docs are not maintained in this repo. The generated publish output lives in the separate `openclaw/docs` repo (often cloned locally as the sibling `openclaw-docs` directory); do not add or edit localized docs under `docs/<locale>/**` here.
- Those localized docs are autogenerated. Treat this repo's English docs plus glossary files as the source of truth, and let the publish/translation pipeline update `openclaw/docs`.
- Pipeline: update English docs here → adjust the matching `docs/.i18n/glossary.<locale>.json` entries → let the publish-repo sync + `scripts/docs-i18n` run in `openclaw/docs` / local `openclaw-docs` clone → apply targeted fixes only if instructed.
- Before rerunning `scripts/docs-i18n`, add glossary entries for any new technical terms, page titles, or short nav labels that must stay in English or use a fixed translation (for example `Doctor` or `Polls`).
- `pnpm docs:check-i18n-glossary` enforces glossary coverage for changed English doc titles and short internal doc labels before translation reruns.
- Translation memory: `docs/.i18n/zh-CN.tm.jsonl` (generated).
- Translation memory lives in generated `docs/.i18n/*.tm.jsonl` files in the publish repo.
- See `docs/.i18n/README.md`.
- The pipeline can be slow/inefficient; if its dragging, ping @jospalmbier on Discord instead of hacking around it.
## Control UI i18n (generated in repo)
- Control UI foreign-language locale bundles are generated in this repo; do not hand-edit `ui/src/i18n/locales/*.ts` for non-English locales or `ui/src/i18n/.i18n/*` unless a targeted generated-output fix is explicitly requested.
- Source of truth is `ui/src/i18n/locales/en.ts` plus the generator/runtime wiring in `scripts/control-ui-i18n.ts`, `ui/src/i18n/lib/types.ts`, and `ui/src/i18n/lib/registry.ts`.
- Pipeline: update English control UI strings and locale wiring here → run `pnpm ui:i18n:sync` (or let `Control UI Locale Refresh` do it) → commit the regenerated locale bundles and `.i18n` metadata.
- If the control UI locale outputs drift, regenerate them; do not manually translate or hand-maintain the generated locale files by default.
## exe.dev VM ops (general)
- Access: stable path is `ssh exe.dev` then `ssh vm-name` (assume SSH key already set).
@@ -112,7 +132,7 @@
- Type-check/build: `pnpm build`
- TypeScript checks: `pnpm tsgo`
- Lint/format: `pnpm check`
- Local agent/dev shells default to lower-memory `OPENCLAW_LOCAL_CHECK=1` behavior for `pnpm tsgo` and `pnpm lint`; set `OPENCLAW_LOCAL_CHECK=0` in CI/shared runs.
- Local agent/dev shells default to host-aware `OPENCLAW_LOCAL_CHECK=1` behavior for `pnpm tsgo` and `pnpm lint`; set `OPENCLAW_LOCAL_CHECK_MODE=throttled` to force the lower-memory profile, `OPENCLAW_LOCAL_CHECK_MODE=full` to keep lock-only behavior, or `OPENCLAW_LOCAL_CHECK=0` in CI/shared runs.
- Format check: `pnpm format` (oxfmt --check)
- Format fix: `pnpm format:fix` (oxfmt --write)
- Terminology:
@@ -125,10 +145,10 @@
- Formatting gate: the pre-commit hook runs `pnpm format` before `pnpm check`. If you want a formatting-only preflight locally, run `pnpm format` explicitly.
- If you need a fast commit loop, `FAST_COMMIT=1 git commit ...` skips the hooks repo-wide `pnpm format` and `pnpm check`; use that only when you are deliberately covering the touched surface some other way.
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
- Generated baseline artifacts live together under `docs/.generated/`.
- Generated baseline drift detection uses SHA-256 hash files under `docs/.generated/` (`.sha256` files tracked in git; full JSON baselines are gitignored, generated locally for inspection).
- Config schema drift uses `pnpm config:docs:gen` / `pnpm config:docs:check`.
- Plugin SDK API drift uses `pnpm plugin-sdk:api:gen` / `pnpm plugin-sdk:api:check`.
- If you change config schema/help or the public Plugin SDK surface, update the matching baseline artifact and keep the two drift-check flows adjacent in scripts/workflows/docs guidance rather than inventing a third pattern.
- If you change config schema/help or the public Plugin SDK surface, run the matching gen command and commit the updated `.sha256` hash file. Keep the two drift-check flows adjacent in scripts/workflows/docs guidance rather than inventing a third pattern.
- For narrowly scoped changes, prefer narrowly scoped tests that directly validate the touched behavior. If no meaningful scoped test exists, say so explicitly and use the next most direct validation available.
- Verification modes for work on `main`:
- Default mode: `main` is relatively stable. Count pre-commit hook coverage when it already verified the current tree, avoid rerunning the exact same checks just for ceremony, and prefer keeping CI/main green before landing.
@@ -140,6 +160,14 @@
- For narrowly scoped changes, if unrelated failures already exist on latest `origin/main`, state that clearly, report the scoped tests you ran, and ask before broadening scope into unrelated fixes or landing despite those failures.
- Do not use scoped tests as permission to ignore plausibly related failures.
## Prompt Cache Stability
- Treat prompt-cache stability as correctness/perf-critical, not cosmetic.
- Any code that assembles model or tool payloads from maps, sets, registries, plugin lists, MCP catalogs, filesystem reads, or network results must make ordering deterministic before building the request.
- Do not rewrite older transcript/history bytes on every turn unless you intentionally want to invalidate the cached prefix. Legacy cleanup, pruning, normalization, and migration logic should preserve recent prompt bytes when possible.
- If truncation or compaction is required, prefer mutating newest or tail content first so the cached prefix stays byte-identical for as long as possible.
- For cache-sensitive changes, require a regression test that proves turn-to-turn prefix stability or deterministic request assembly; helper-local tests alone are not enough.
## Coding Style & Naming Conventions
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
@@ -191,10 +219,10 @@
- Test performance guardrail: prefer narrow public SDK subpaths such as `models-provider-runtime`, `skill-commands-runtime`, and `reply-dispatch-runtime` over older broad helper barrels when both expose the needed helper.
- Test performance guardrail: treat import-dominated test time as a boundary bug. Refactor the import surface before adding more cases to the slow file.
- Agents MUST NOT modify baseline, inventory, ignore, snapshot, or expected-failure files to silence failing checks without explicit approval in this chat.
- For targeted/local debugging, keep using the wrapper: `pnpm test -- <path-or-filter> [vitest args...]` (for example `pnpm test -- src/commands/onboard-search.test.ts -t "shows registered plugin providers"`); do not default to raw `pnpm vitest run ...` because it bypasses wrapper config/profile/pool routing.
- For targeted/local debugging, use the native root-project entrypoint: `pnpm test <path-or-filter> [vitest args...]` (for example `pnpm test src/commands/onboard-search.test.ts -t "shows registered plugin providers"`); do not default to raw `pnpm vitest run ...` because it bypasses the repo's default config/profile/pool routing.
- Do not set test workers above 16; tried already.
- Keep Vitest on `forks` only. Do not introduce or reintroduce any non-`forks` Vitest pool or alternate execution mode in configs, wrapper scripts, or default test commands without explicit approval in this chat. This includes `threads`, `vmThreads`, `vmForks`, and any future/nonstandard pool variant.
- If local Vitest runs cause memory pressure, the wrapper now derives budgets from host capabilities (CPU, memory band, current load). For a conservative explicit override during land/gate runs, use `OPENCLAW_TEST_PROFILE=serial OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test`.
- Vitest now defaults to native root-project `threads`, with hard `forks` exceptions for `gateway`, `agents`, and `commands`. Keep new pool changes explicit and justified; use `OPENCLAW_VITEST_POOL=forks` for full local fork debugging.
- If local Vitest runs cause memory pressure, the default worker budget now derives from host capabilities (CPU, memory band, current load). For a conservative explicit override during land/gate runs, use `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test`.
- Live tests (real keys): `OPENCLAW_LIVE_TEST=1 pnpm test:live` (OpenClaw-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
- `pnpm test:live` defaults quiet now. Keep `[live]` progress; suppress profile/gateway chatter. Full logs: `OPENCLAW_LIVE_TEST_QUIET=0 pnpm test:live`.
- Full kit + whats covered: `docs/help/testing.md`.
@@ -252,6 +280,8 @@
- "Bump version everywhere" means all version locations above **except** `appcast.xml` (only touch appcast when cutting a new macOS Sparkle release).
- **Restart apps:** “restart iOS/Android apps” means rebuild (recompile/install) and relaunch, not just kill/launch.
- **Device checks:** before testing, verify connected real devices (iOS/Android) before reaching for simulators/emulators.
- Mobile pairing: `ws://` (cleartext) is allowed for private LAN addresses (RFC 1918, link-local, mDNS `.local`) and loopback. Private LAN hosts typically lack PKI-backed identity, so requiring TLS there adds complexity without meaningful security gain. `wss://` is required for Tailscale and public endpoints.
- Security report scope: reports that treat cleartext `ws://` mobile pairing over private LAN as a vulnerability are out of scope unless they demonstrate a trust-boundary bypass beyond passive network observation on the same LAN.
- iOS Team ID lookup: `security find-identity -p codesigning -v` → use Apple Development (…) TEAMID. Fallback: `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`.
- A2UI bundle hash: `src/canvas-host/a2ui/.bundle.hash` is auto-generated; ignore unexpected changes, and only regenerate via `pnpm canvas:a2ui:bundle` (or `scripts/bundle-a2ui.sh`) when needed. Commit the hash as a separate commit.
- Release signing/notary credentials are managed outside the repo; maintainers keep that setup in the private [maintainer release docs](https://github.com/openclaw/maintainers/tree/main/release).
@@ -266,7 +296,7 @@
- When working on a GitHub Issue or PR, print the full URL at the end of the task.
- When answering questions, respond with high-confidence answers only: verify in code; do not guess.
- Never update the Carbon dependency.
- Carbon: prefer latest published beta over stable when possible; do not switch to stable casually.
- Any dependency with `pnpm.patchedDependencies` must use an exact version (no `^`/`~`).
- Patching dependencies (pnpm patches, overrides, or vendored changes) requires explicit approval; do not do this by default.
- **Multi-agent safety:** do **not** create/apply/drop `git stash` entries unless explicitly requested (this includes `git pull --rebase --autostash`). Assume other agents may be working; keep unrelated WIP untouched and avoid cross-cutting state changes.

View File

@@ -6,102 +6,384 @@ Docs: https://docs.openclaw.ai
### Changes
- Channels/context visibility: add configurable `contextVisibility` per channel (`all`, `allowlist`, `allowlist_quote`) so supplemental quote, thread, and fetched history context can be filtered by sender allowlists instead of always passing through as received.
- Matrix/exec approvals: add Matrix-native exec approval prompts with account-scoped approvers, channel-or-DM delivery, and room-thread aware resolution handling. (#58635) Thanks @gumadeiras.
- Providers/StepFun: add the bundled StepFun provider plugin with standard and Step Plan endpoints, China/global onboarding choices, `step-3.5-flash` on both catalogs, and `step-3.5-flash-2603` currently exposed on Step Plan. (#60032) Thanks @hengm3467.
- Providers/config: add full `models.providers.*.request` transport overrides for model-provider paths, including headers, auth, proxy, and TLS, and keep media provider HTTP request transport overrides aligned with the same request-policy surface. (#60200) Thanks @vincentkoc.
- Control UI/skills: add ClawHub search, detail, and install flows directly in the Skills panel. (#60134) Thanks @samzong.
- Outbound/runtime seams: split delivery, target-resolution, and session/transcript helper loading into narrower runtime seams so outbound hot paths and their owner tests avoid broader setup fan-out. (#60311) Thanks @shakkernerd.
- Plugins/browser seams: split browser and WhatsApp plugin-sdk seams into narrower browser, approval-auth, and target-helper facades so hot paths and owner tests avoid broader runtime fan-out. (#60376) Thanks @shakkernerd.
- Tests/runtime: trim local unit-test import/runtime fan-out across browser, WhatsApp, cron, task, and reply flows so owner suites start faster with lower shared-worker overhead while preserving the same focused behavior coverage. (#60249) Thanks @shakkernerd.
- Tests/secrets runtime: restore split secrets suite cache and env isolation cleanup so broader runs do not leak stale plugin or provider snapshot state. (#60395) Thanks @shakkernerd.
- Providers/Ollama: add bundled Ollama Web Search provider for key-free web_search via your configured Ollama host and `ollama signin`. (#59318) Thanks @BruceMacD.
- Plugins/install: add `openclaw plugins install --force` to overwrite existing plugin and hook-pack install targets without using the dangerous-code override flag. (#60544) Thanks @gumadeiras.
- Providers/transport: add shared proxy/TLS/auth-aware request transport support across model-provider paths, including Anthropic and Google native transport runtimes, so provider request overrides work beyond OpenAI-family traffic.
- Providers/Anthropic: restore Claude CLI as the preferred local Anthropic path in onboarding, model-auth guidance, and doctor flows again, and keep the Docker Claude CLI live lane aligned with the restored guidance.
- Plugins/webhooks: add a bundled webhook ingress plugin so external automation can create and drive bound TaskFlows through per-route shared-secret endpoints. (#61892) Thanks @mbelinky.
- Tools/media: document per-provider music and video generation capabilities, and add shared live video-to-video sweep coverage for providers that support local reference clips.
### Fixes
- Skills/uv install: block workspace `.env` from overriding `UV_PYTHON` and strip related interpreter override keys from uv skill-install subprocesses so repository-controlled env files cannot steer the selected Python runtime. (#59178) Thanks @pgondhi987.
- Telegram/reactions: preserve `reactionNotifications: "own"` across gateway restarts by persisting sent-message ownership state instead of treating cold cache as a permissive fallback. (#59207) Thanks @samzong.
- Gateway/startup: detect PID recycling in gateway lock files on Windows and macOS, and add startup progress so stale lock conflicts no longer block healthy restarts. (#59843) Thanks @TonyDerek-dot.
- MS Teams/DM media: download inline images in 1:1 chats via Graph API so Teams DM image attachments stop failing to load. (#52212) Thanks @Ted-developer.
- MS Teams/threading: preserve channel reply threading in proactive fallback so replies stay in the original thread instead of dropping into the channel root. (#55198) Thanks @hyojin.
- Telegram/media: preserve `<media:...>` placeholders and `file_id` in captioned messages when Bot API downloads fail, so agents still receive media context. (#59948) Thanks @v1p0r.
- Telegram/media: keep inbound image attachments readable on upgraded installs where legacy state roots still differ from the managed config-dir media cache. (#59971) Thanks @neeravmakwana.
- Telegram/local Bot API: thread `channels.telegram.apiRoot` through buffered reply-media and album downloads so self-hosted Bot API file paths stop falling back to `api.telegram.org` and 404ing. (#59544) Thanks @SARAMALI15792.
- Telegram/replies: preserve explicit topic targets when `replyTo` is present while still inheriting the current topic for same-chat replies without an explicit topic. (#59634) Thanks @dashhuang.
- Telegram/native commands: clean up metadata-driven progress placeholders when replies fall back, edits fail, or local exec approval prompts are suppressed. (#59300) Thanks @jalehman.
- Telegram/models: compare full provider/model refs in the Telegram picker so same-id models from other providers no longer show the wrong current-model checkmark. (#60384) Thanks @sfuminya.
- Media/request overrides: resolve shared and capability-filtered media request SecretRefs correctly and expose media transport override fields to schema-driven config consumers. (#59848) Thanks @vincentkoc.
- Providers/request overrides: stop advertising unsupported proxy and TLS transport settings on `models.providers.*.request`, and fail closed if unvalidated config tries to route LLM model-provider traffic through dead transport fields. (#59682) Thanks @vincentkoc.
- Discord/mentions: treat `@everyone` and `@here` as valid mention-gate triggers in guild preflight so mention-required bots still respond to those broadcasts. (#60343) Thanks @geekhuashan.
- Matrix: allow secret-storage recreation during automatic repair bootstrap so clients that lose their recovery key can recover and persist new cross-signing keys. (#59846) Thanks @al3mart.
- Matrix/crypto persistence: capture and write the IndexedDB snapshot while holding the snapshot file lock so concurrent gateway and CLI persists cannot overwrite newer crypto state. (#59851) Thanks @al3mart.
- Ollama/auth: prefer real cloud auth over local marker during model auth resolution so cloud-backed Ollama auth does not get shadowed by stale local-only markers.
- Plugins/Kimi Coding: parse tagged Kimi tool-call text into structured tool calls on the provider stream path so tools execute instead of echoing raw markup. (#60051) Thanks @obviyus.
- Channels/passive hooks: emit passive message hooks for mention-skipped Telegram and Signal group messages when `ingest` is enabled, including wildcard/default fallback and per-group override handling. (#60018) Thanks @obviyus.
- Providers/compat: stop forcing OpenAI-only payload defaults on proxy and custom OpenAI-compatible routes, and preserve native vendor-specific reasoning, tool, and streaming behavior for Anthropic-compatible, Moonshot, Mistral, ModelStudio, OpenRouter, xAI, Z.ai, and other routed provider paths.
- Plugins/manifest registry: stop warning when an explicit manifest `id` intentionally differs from the discovery hint. (#59185) Thanks @samzong.
- WhatsApp/streaming: honor `channels.whatsapp.blockStreaming` again for inbound auto-replies so progressive block replies can be enabled explicitly instead of being forced to final-only delivery. Thanks @mcaxtr.
- Auth/failover: shorten `auth_permanent` lockouts, add dedicated config knobs for permanent-auth backoff, and downgrade ambiguous auth-ish upstream incidents to retryable auth failures so providers recover automatically after transient outages. (#60404) Thanks @extrasmall0.
- Providers/GitHub Copilot: route Claude models through Anthropic Messages with Copilot-compatible headers and Anthropic prompt-cache markers instead of forcing the OpenAI Responses transport.
- Plugins/runtime: reuse compatible active registries for `web_search` and `web_fetch` provider snapshot resolution so repeated runtime reads do not re-import the same bundled plugin set on each agent message. Related #48380.
- Infra/tailscale: ignore `OPENCLAW_TEST_TAILSCALE_BINARY` outside explicit test environments and block it from workspace `.env`, so test-only binary overrides cannot be injected through trusted repository state. (#58468) Thanks @eleqtrizit.
- Plugins/OpenAI: enable reference-image edits for `gpt-image-1` by routing edit calls to `/images/edits` with multipart image uploads, and update image-generation capability/docs metadata accordingly. Thanks @steipete.
- Cache/context guard: compact newest tool results first so the cached prompt prefix stays byte-identical and avoids full re-tokenization every turn past the 75% context threshold. (#58036) Thanks @bcherny.
- Agents/tools: include value-shape hints in missing-parameter tool errors so dropped, empty-string, and wrong-type write payloads are easier to diagnose from logs. (#55317) Thanks @priyansh19.
- Android/assistant: keep queued App Actions prompts pending when auto-send enqueue is rejected, so transient chat-health drops do not silently lose the assistant request. Thanks @obviyus.
- Plugins/startup: migrate legacy `tools.web.search.<provider>` config before strict startup validation, and record plugin failure phase/timestamp so degraded plugin startup is easier to diagnose from logs and `plugins list`.
- Plugins/Google: separate OAuth CSRF state from PKCE code verifier during Gemini browser sign-in so state validation and token exchange use independent values. (#59116) Thanks @eleqtrizit.
- Agents/subagents: honor `agents.defaults.subagents.allowAgents` for `sessions_spawn` and `agents_list`, so default cross-agent allowlists work without duplicating per-agent config. (#59944) Thanks @hclsys.
- Agents/tools: normalize only truly empty MCP tool schemas to `{ type: "object", properties: {} }` so OpenAI accepts parameter-free tools without rewriting unrelated conditional schemas. (#60176) Thanks @Bartok9.
- Update/npm: prefer the npm binary that owns the installed global OpenClaw prefix during package self-update, so mixed Homebrew-plus-nvm setups update the right install. (#60153) Thanks @jayeshp19.
- Plugins/browser: block SSRF redirect bypass by installing a real-time Playwright route handler before `page.goto()` so navigation to private/internal IPs is intercepted and aborted mid-redirect instead of checked post-hoc. (#58771) Thanks @pgondhi987.
- Android/gateway: require TLS for non-loopback remote gateway endpoints while still allowing local loopback and emulator cleartext setup flows. (#58475) Thanks @eleqtrizit.
- Exec/Windows: hide transient console windows for `runExec` and `runCommandWithTimeout` child-process launches, matching other Windows exec paths and stopping visible shell flashes during tool runs. (#59466) Thanks @lawrence3699.
- Zalo/webhook: scope replay-dedupe cache key to path and account using `JSON.stringify` so multi-account deployments do not silently drop events due to cross-account cache poisoning. (#59387) Thanks @pgondhi987.
- Exec/Windows: reject malformed drive-less rooted executable paths like `:\Users\...` so approval and allowlist candidate resolution no longer treat them as cwd-relative commands. (#58040) Thanks @SnowSky1.
- Exec/preflight: fail closed on complex interpreter invocations that would otherwise skip script-content validation, and correctly inspect quoted script paths before host execution. Thanks @pgondhi987.
- Exec/Windows: include Windows-compatible env override keys like `ProgramFiles(x86)` in system-run approval binding so changed approved values are rejected instead of silently passing unbound. (#59182) Thanks @pgondhi987.
- ACP/Windows spawn: fail closed on unresolved `.cmd` and `.bat` OpenClaw wrappers unless a caller explicitly opts into shell fallback, so Windows ACP launches do not silently drop into shell-mediated execution when wrapper unwrapping fails. (#58436) Thanks @eleqtrizit.
- Exec/Windows: prefer strict-inline-eval denial over generic allowlist prompts for interpreter carriers, while keeping persisted Windows allow-always approvals argv-bound. (#59780) Thanks @luoyanglang.
- Gateway/connect: omit admin-scoped config and auth metadata from lower-privilege `hello-ok` snapshots while preserving those fields for admin reconnects. (#58469) Thanks @eleqtrizit.
- iOS/canvas: restrict A2UI bridge trust to the bundled scaffold and exact capability-backed remote canvas URLs, so generic `canvas.navigate` and `canvas.present` loads no longer gain action-dispatch authority. (#58471) Thanks @eleqtrizit.
- Agents/tool policy: preserve restrictive plugin-only allowlists instead of silently widening access to core tools, and keep allowlist warnings aligned with the enforced policy. (#58476) Thanks @eleqtrizit.
- Hooks/session_end: preserve deterministic reason metadata for custom reset aliases and overlapping idle-plus-daily rollovers so plugins can rely on lifecycle reason reporting. (#59715) Thanks @jalehman.
- Tools/image generation: stop inferring unsupported resolution overrides for OpenAI reference-image edits when no explicit `size` or `resolution` is provided, so default edit flows no longer fail before the provider request is sent.
- Agents/sessions: release embedded runner session locks even when teardown cleanup throws, so timed-out or failed cleanup paths no longer leave sessions wedged until the stale-lock watchdog recovers them. (#59194) Thanks @samzong.
- Slack/app manifest: add the missing `groups:read` scope to the onboarding and example Slack app manifest so apps copied from the OpenClaw templates can resolve private group conversations reliably.
- Mobile pairing/Android: stop generating Tailscale and public mobile setup codes that point at unusable cleartext remote gateways, keep private LAN pairing allowed, and make Android reject insecure remote endpoints with clearer guidance while mixed bootstrap approvals honor operator scopes correctly. (#60128) Thanks @obviyus.
- Telegram/media: add `channels.telegram.network.dangerouslyAllowPrivateNetwork` for trusted fake-IP or transparent-proxy environments where Telegram media downloads resolve `api.telegram.org` to private/internal/special-use addresses.
- Discord/proxy: keep Carbon REST, monitor startup, and webhook sends on the configured Discord proxy while falling back cleanly when the proxy URL is invalid, so Discord replies and deploys do not hard-fail on malformed proxy config. (#57465) Thanks @geekhuashan.
- Discord/components: keep modal-trigger and spoiler-file component messages on the component path when sending media, so classic-message fallback does not silently drop component-only behavior. (#60361) Thanks @geekhuashan.
- Mobile pairing/device approval: mint both node and operator device tokens when one approval grants merged roles, so mixed mobile bootstrap pairings stop reconnecting as operator-only and showing the node offline. (#60208) Thanks @obviyus.
- Agents/tool policy: stop `tools.profile` warnings from flagging runtime-gated baseline core tools as unknown when the coding profile is missing tools like `code_execution`, `x_search`, `image`, or `image_generate`, while still warning on explicit extra allowlist entries. Thanks @vincentkoc.
- Sessions/resolution: collapse alias-duplicate session-id matches before scoring, keep distinct structural ties ambiguous, and prefer current-store reuse when resolving equal cross-store duplicates so follow-up turns stop dropping or duplicating sessions on timestamp ties.
- Mobile pairing/bootstrap: keep setup bootstrap tokens alive through the initial node auto-pair so the same QR bootstrap token can finish operator approval, then revoke it after the full issued profile connects successfully. (#60221) Thanks @obviyus.
- Plugins/allowlists: let explicit bundled chat channel enablement bypass `plugins.allow`, while keeping auto-enabled channel activation and startup sidecars behind restrictive allowlists. (#60233) Thanks @dorukardahan.
- Allowlist/commands: require owner access for `/allowlist add` and `/allowlist remove` so command-authorized non-owners cannot mutate persisted allowlists. (#59836) Thanks @eleqtrizit.
- Control UI/skills: clear stale ClawHub results immediately when the search query changes, so debounced searches cannot keep outdated install targets visible. Related #60134.
- Fetch/redirects: normalize guarded redirect method rewriting and loop detection so SSRF-guarded requests match platform redirect behavior without missing loops back to the original URL. (#59121) Thanks @eleqtrizit.
- Discord/ack reactions: keep automatic ACK reaction auth on the active hydrated Discord account so SecretRef-backed and non-default-account reactions stop falling back to stale default config resolution. (#60081) Thanks @FunJim.
- Telegram/model switching: render non-default `/model` callback confirmations with HTML formatting so Telegram shows the selected model in bold instead of raw `**...**` markers. (#60042) Thanks @GitZhangChi.
- Plugins/update: allow `openclaw plugins update` to use `--dangerously-force-unsafe-install` for built-in dangerous-code false positives during plugin updates. (#60066) Thanks @huntharo.
- Gateway/auth: disconnect shared-auth websocket sessions only for effective auth rotations on restart-capable config writes, and keep `config.set` auth edits from dropping still-valid live sessions. (#60387) Thanks @mappel-nv.
- Control UI/chat: keep the Stop button visible during tool-only execution so abortable runs do not fall back to Send while tools are still running. (#54528) thanks @chziyue.
- Discord/voice: make READY auto-join fire-and-forget while keeping the shorter initial voice-connect timeout separate from the longer playback-start wait. (#60345) Thanks @geekhuashan.
- Agents/skills: add inherited `agents.defaults.skills` allowlists, make per-agent `agents.list[].skills` replace defaults instead of merging, and scope embedded, session, sandbox, and cron skill snapshots through the effective runtime agent. (#59992) Thanks @gumadeiras.
- Matrix/Telegram exec approvals: recover stored same-channel account bindings even when session reply state drifted to another channel, so foreign-channel approvals route to the bound account instead of fanning out or being rejected as ambiguous. (#60417) thanks @gumadeiras.
- Slack/app manifest: set `bot_user.always_online` to `true` in the onboarding and example Slack app manifest so the Slack app appears ready to respond.
- Gateway/websocket auth: refresh auth on new websocket connects after secrets reload so rotated gateway tokens take effect immediately without requiring a restart. (#60323) Thanks @mappel-nv.
- Onboarding/plugins: keep non-interactive auth-choice inference scoped to bundled and already-trusted plugins so untrusted workspace manifests cannot hijack built-in provider API-key flows. (#59120) Thanks @eleqtrizit.
- Agents/workspace: respect `agents.defaults.workspace` for non-default agents by resolving them under the configured base path instead of falling back to `workspace-<id>`. (#59858) Thanks @joelnishanth.
- Config/All Settings: keep the raw config view intact when sensitive fields are blank instead of corrupting or dropping the snapshot during redaction. (#28214) thanks @solodmd.
- Plugins/runtime: honor explicit capability allowlists during fallback speech, media-understanding, and image-generation provider loading so bundled capability plugins do not bypass restrictive `plugins.allow` config. (#52262) Thanks @PerfectPan.
- Hooks/tool policy: block tool calls when a `before_tool_call` hook crashes so hook failures fail closed instead of silently allowing execution. (#59822) Thanks @pgondhi987.
- Matrix/media: surface a dedicated `[matrix <kind> attachment too large]` marker for oversized inbound media instead of the generic unavailable marker, and classify size-limit failures with a typed Matrix error. (#60289) Thanks @efe-arv.
- WhatsApp/watchdog: reset watchdog timeout after reconnect so quiet channels no longer enter a tight reconnect loop from stale message timestamps carried across connection runs. (#60007) Thanks @MonkeyLeeT.
- Agents/fallback: persist selected fallback overrides before retry attempts start, prefer persisted overrides during live-session reconciliation, and keep provider-scoped auth-profile failover from snapping retries back to stale primary selections.
- Channels/secrets: keep bundled channel artifact and secret-contract loading stable under lazy loading so bundled channel secrets continue to appear in `openclaw secret`, status, and security-audit surfaces.
- Providers/xAI: recognize `api.grok.x.ai` as an xAI-native endpoint again so native xAI web-search attribution keeps working on Grok-hosted base URLs. (#61377) Thanks @jjjojoj.
- Providers/Anthropic/cache: preserve thinking blocks for Claude Opus 4.5+, Sonnet 4.5+, and newer Claude 4-family models so Anthropic prompt-cache prefixes keep matching after thinking turns. (#61793)
- Providers/Ollama: honor the selected provider's `baseUrl` during streaming so multi-Ollama setups stop routing every stream to the first configured Ollama endpoint. (#61678)
- Browser/remote CDP: retry the DevTools websocket once after remote browser restarts so healthy remote browser profiles do not fail availability checks during CDP warm-up. (#57397) Thanks @ThanhNguyxn07.
- Memory/vector recall: surface explicit warnings when `sqlite-vec` is unavailable or vector writes are degraded so memory indexing no longer reports false-success while semantic recall is impaired.
- MS Teams/security: validate file-consent upload URLs against HTTPS, Microsoft/SharePoint host allowlists, and private-IP DNS checks before uploading attachments, blocking SSRF-style consent-upload abuse. (#23596)
- Discord/gateway monitor: use `ws://` again for gateway monitor sockets so Discord monitor connections recover reliably after recent gateway socket changes.
- Control UI/auth URLs: detect mistaken `?token=` links, show the correct `#token=` fragment hint only on real auth failures, and stop masking the real problem behind a generic device-identity error. (#54842)
- Control UI/chat layout: keep Copy and Canvas actions plus mobile exec-approval overlays from covering chat text or command previews on narrow screens. (#61514)
- Matrix/formatting: preserve multi-paragraph and loose-list rendering in Element so numbered and bulleted Markdown keeps its content attached to the correct list item. (#60997) Thanks @gucasbrg.
- Sessions/model selection: resolve the explicitly selected session model separately from runtime fallback resolution so session status and live model switching stay aligned with the chosen model.
- Secrets/x_search: keep legacy `x_search` auth resolution working so older xAI web-search configs continue to load after the plugin-owned auth move.
- iOS/Watch exec approvals: keep Apple Watch review and approval recovery working while the iPhone is locked or backgrounded, including background-safe reconnects, persisted pending approvals, notification cleanup, and APNs-backed watch refresh recovery. (#61757) Thanks @ngutman.
- Discord/forwarding: recover forwarded referenced message text and attachments when Discord omits snapshot payloads, so forwarded-message relays keep the original content. (#61670) Thanks @artwalker.
- TUI/status: route `/status` through the shared session-status command and move the old gateway-wide diagnostic summary to `/gateway-status` (`/gwstatus`). Thanks @vincentkoc.
- TUI/history and heartbeat: keep assistant commentary hidden on both streamed and reloaded TUI history views, preserve the phase-sanitized REST history contract, and stop forced heartbeat runs from targeting subagent sessions. (#61463) Thanks @100yenadmin.
- TUI/command messages: strip inbound envelope metadata before rendering command/system messages so async completion notices stop leaking raw wrappers into the operator terminal. (#59985) Thanks @MoerAI.
- TUI/terminal: restore Kitty keyboard protocol and `modifyOtherKeys` state on TUI exit and fatal CLI crashes so parent shells stop inheriting broken keyboard input after `openclaw tui` exits. (#49130) Thanks @biefan.
- Plugins/Windows: load plugin entrypoints through `file://` import specifiers on Windows without breaking plugin SDK alias resolution, fixing `ERR_UNSUPPORTED_ESM_URL_SCHEME` for absolute plugin paths. (#61832) Thanks @Zeesejo.
- Plugins/Windows: disable native Jiti loading for setup and doctor contract registries on Windows so onboarding and config-doctor plugin probes stop crashing with `ERR_UNSUPPORTED_ESM_URL_SCHEME`. (#61836, #61853)
- Plugins/install: preserve plugin-schema defaults during fresh-install raw config validation so bundled plugin installs stop failing when required fields rely on schema defaults. (#61856) Thanks @SuperMarioYL.
- macOS/gateway version: strip trailing commit metadata from CLI version output before semver parsing so the Mac app recognizes installed gateway versions like `OpenClaw 2026.4.2 (d74a122)` again. (#61111) Thanks @oliviareid-svg.
- Gateway/containers: auto-bind to `0.0.0.0` during container startup for Docker and Podman compatibility, while keeping host-side status and doctor checks on the hardened loopback default when `gateway.bind` is unset. (#61818) Thanks @openperf.
- Gateway/status: probe local TLS gateways over `wss://`, forward the local cert fingerprint for self-signed loopback probes, and warn when the local TLS runtime cannot load the configured cert. (#61935) Thanks @ThanhNguyxn07.
- Slack/threading: keep legacy thread stickiness for real replies when older callers omit `isThreadReply`, while still honoring `replyToMode` for Slack's auto-created top-level `thread_ts`. (#61835) Thanks @kaonash.
- Providers/Google: recognize Gemma model ids in native Google forward-compat resolution, keep the requested provider when cloning fallback templates, and force Gemma reasoning off so Gemma 4 routes stop failing through the Google catalog fallback. (#61507) Thanks @eyjohn.
- Providers/Anthropic: skip `service_tier` injection for OAuth-authenticated stream wrapper requests so Claude OAuth requests stop failing with HTTP 401. (#60356) thanks @openperf.
- Providers/OpenAI: keep WebSocket text buffered until a real assistant phase arrives, even when text deltas land before a phaseless `output_item.added` announcement. (#61954) Thanks @100yenadmin.
- Providers/OpenAI: accept case-insensitive `plugins.entries.openai.config.personality` values, keep unknown overrides on the friendly overlay path, and add `on` as an alias for `friendly`. Thanks @vincentkoc.
- Discord/thread titles: stop forcing a hardcoded temperature for generated auto-thread names so Codex-backed thread title generation works on `openai-codex/*` models again. (#59525)
- Agents/message tool: add a `read` plus `threadId` discoverability hint when the configured channel actions support threaded message reads.
- Agents/context overflow: combine oversized and aggregate tool-result recovery in one repair pass, and restore a total-context overflow backstop during tool loops so recoverable sessions retry instead of failing early. (#61651) Thanks @Takhoffman.
- Agents/exec: preserve explicit `host=node` routing under elevated defaults when `tools.exec.host=auto`, and fail loud on invalid elevated cross-host overrides. (#61739) Thanks @obviyus.
- Agents/heartbeat: stop truncating live session transcripts after no-op heartbeat acks, move heartbeat cleanup to prompt assembly and compaction, and keep post-filter context-engine ingestion aligned with the real session baseline. (#60998) Thanks @nxmxbbd.
- Gateway/TUI: defer terminal chat finalization for per-attempt lifecycle errors so fallback retries keep streaming before the run is marked failed. (#60043) Thanks @jwchmodx.
- Gateway/history: seed SSE startup history and raw transcript sequence tracking from one initial transcript snapshot so first history events cannot diverge from subsequent message sequence numbering. (#61855) Thanks @100yenadmin.
- Agents/history: keep history-based reply reads and subagent completion summaries on `final_answer` text only so internal commentary stops leaking into user-visible follow-up replies. (#61747) Thanks @afurm.
- Agents/history: suppress commentary-only visible-text leaks in streaming and chat history views, and keep sanitized SSE history sequence numbers monotonic after transcript-only refreshes. (#61829) Thanks @100yenadmin.
- Agents/history: use one shared assistant-visible sanitizer across embedded delivery and chat-history extraction so leaked `<tool_call>` and `<tool_result>` XML blocks stay hidden from user-facing replies. (#61729) Thanks @openperf.
- Agents/history: keep truly legacy unsigned replay text unphased when mixed with phased OpenAI WS assistant blocks, while still inheriting message phase for id-only replay signatures. (#61529) Thanks @100yenadmin.
- Memory/dreaming: strip managed Light Sleep and REM blocks before daily-note ingestion so dreaming summaries stop re-ingesting their own staged output into new candidates. (#61720) Thanks @MonkeyLeeT.
- Docs/i18n: relocalize final localized-page links after translation so generated locale pages stop keeping stale English-root links when targets appear later in the same run. (#61796) thanks @hxy91819.
- Docs/i18n: remove the zh-CN homepage redirect override so Mintlify can resolve the localized Chinese homepage without self-redirecting `/zh-CN/index`.
- Tools/web_fetch and web_search: fix `TypeError: fetch failed` caused by undici 8.0 enabling HTTP/2 by default; pinned SSRF-guard dispatchers now explicitly set `allowH2: false` to restore HTTP/1.1 behavior and keep the custom DNS-pinning lookup compatible. (#61738, #61777) Thanks @zozo123.
## 2026.4.5
### Breaking
- Config: remove legacy public config aliases such as `talk.voiceId` / `talk.apiKey`, `agents.*.sandbox.perSession`, `browser.ssrfPolicy.allowPrivateNetwork`, `hooks.internal.handlers`, and channel/group/room `allow` toggles in favor of the canonical public paths and `enabled`, while keeping load-time compatibility and `openclaw doctor --fix` migration support for existing configs. (#60726) Thanks @vincentkoc.
### Changes
- Agents/video generation: add the built-in `video_generate` tool so agents can create videos through configured providers and return the generated media directly in the reply.
- Agents/music generation: ignore unsupported optional hints such as `durationSeconds` with a warning instead of hard-failing requests on providers like Google Lyria.
- Providers/ComfyUI: add a bundled `comfy` workflow media plugin for local ComfyUI and Comfy Cloud workflows, including shared `image_generate`, `video_generate`, and workflow-backed `music_generate` support, with prompt injection, optional reference-image upload, live tests, and output download.
- Tools/music generation: add the built-in `music_generate` tool with bundled Google (Lyria) and MiniMax providers plus workflow-backed Comfy support, including async task tracking and follow-up delivery of finished audio.
- Providers: add bundled Qwen, Fireworks AI, and StepFun providers, plus MiniMax TTS, Ollama Web Search, and MiniMax Search integrations for chat, speech, and search workflows. (#60032, #55921, #59318, #54648)
- Providers/Amazon Bedrock: add bundled Mantle support plus inference-profile discovery and automatic request-region injection so Bedrock-hosted Claude, GPT-OSS, Qwen, Kimi, GLM, and similar routes work with less manual setup. (#61296, #61299) Thanks @wirjo.
- Control UI/multilingual: add localized control UI support for Simplified Chinese, Traditional Chinese, Brazilian Portuguese, German, Spanish, Japanese, Korean, French, Turkish, Indonesian, Polish, and Ukrainian. Thanks @vincentkoc.
- Plugins: add plugin-config TUI prompts to guided onboarding/setup flows, and add `openclaw plugins install --force` so existing plugin and hook-pack targets can be replaced without using the dangerous-code override flag. (#60590, #60544)
- Control UI/skills: add ClawHub search, detail, and install flows directly in the Skills panel. (#60134) Thanks @samzong.
- iOS/exec approvals: add generic APNs approval notifications that open an in-app exec approval modal, fetch command details only after authenticated operator reconnect, and clear stale notification state when the approval resolves. (#60239) Thanks @ngutman.
- Matrix/exec approvals: add Matrix-native exec approval prompts with account-scoped approvers, channel-or-DM delivery, and room-thread aware resolution handling. (#58635) Thanks @gumadeiras.
- Channels/context visibility: add configurable `contextVisibility` per channel (`all`, `allowlist`, `allowlist_quote`) so supplemental quote, thread, and fetched history context can be filtered by sender allowlists instead of always passing through as received.
- Providers/request overrides: add shared model and media request transport overrides across OpenAI-, Anthropic-, Google-, and compatible provider paths, including headers, auth, proxy, and TLS controls. (#60200)
- Providers/OpenAI: add forward-compat `openai-codex/gpt-5.4-mini`, an opt-in GPT personality, and provider-owned GPT-5 prompt contributions so Codex/GPT runs stay cache-stable and compatible with bundled catalog lag.
- Agents/Claude CLI: expose OpenClaw tools to background Claude CLI runs through a loopback MCP bridge and switch bundled runs to stdin + `stream-json` partial-message streaming so prompts stop riding argv, long replies show live progress, and final session/usage metadata still land cleanly. (#35676) Thanks @mylukin.
- ACPX/runtime: embed the ACP runtime directly in the bundled `acpx` plugin, remove the extra external ACP CLI hop, harden live ACP session binding and reuse, and add a generic `reply_dispatch` hook so bundled plugins like ACPX can own reply interception without hardcoded ACP paths in core auto-reply routing. (#61319)
- Agents/progress: add experimental structured plan updates and structured execution item events so compatible UIs can show clearer step-by-step progress during long-running runs.
- Providers/Anthropic: remove the Claude CLI backend and setup-token from new onboarding, keep existing configured legacy profiles runnable, and have `openclaw doctor` repair or remove stale `anthropic:claude-cli` state during migration.
- Tools/video generation: add bundled xAI (`grok-imagine-video`), Alibaba Model Studio Wan, and Runway video providers, plus live-test/default model wiring for all three.
- Memory/search: add Amazon Bedrock embeddings for Titan, Cohere, Nova, and TwelveLabs models, with AWS credential-chain auto-detection for `provider: "auto"` and provider-specific dimension controls. Thanks @wirjo.
- Providers/Amazon Bedrock Mantle: generate bearer tokens from the AWS credential chain so Mantle auto-discovery can use IAM auth without manually exporting `AWS_BEARER_TOKEN_BEDROCK`. Thanks @wirjo.
- Memory/dreaming (experimental): add weighted short-term recall promotion, a `/dreaming` command, Dreams UI, multilingual conceptual tagging, and doctor/status repair support, while refactoring dreaming from competing modes into three cooperative phases (light, deep, REM) with independent schedules and recovery behavior so durable memory promotion can run in the background with less manual setup. (#60569, #60697) Thanks @vignesh07.
- Memory/dreaming: add configurable aging controls (`recencyHalfLifeDays`, `maxAgeDays`) plus optional verbose logging so operators can tune recall decay and inspect promotion decisions more easily.
- Memory/dreaming: add REM preview tooling (`openclaw memory rem-harness`, `promote-explain`), surface possible lasting truths during REM staging, and make deep promotion replay-safe so reruns reconcile instead of duplicating `MEMORY.md` entries.
- Memory/dreaming: write dreaming trail content to top-level `dreams.md` instead of daily memory notes, update `/dreaming` help text to point there, and keep `dreams.md` available for explicit reads without pulling it into default recall. Thanks @davemorin.
- Memory/dreaming: add the Dream Diary surface in Dreams, simplify user-facing dreaming config to `enabled` plus optional `frequency`, treat phases as implementation detail in docs/UI, and keep the lobster animation visible above diary content. Thanks @vignesh07.
- Prompt caching: keep prompt prefixes more reusable across transport fallback, deterministic MCP tool ordering, compaction, embedded image history, normalized system-prompt fingerprints, `openclaw status --verbose` cache diagnostics, and the removal of duplicate in-band tool inventories from agent system prompts so follow-up turns hit cache more reliably. (#58036, #58037, #58038, #59054, #60603, #60691) Thanks @bcherny and @vincentkoc.
- Agents/cache: diagnostics: add prompt-cache break diagnostics, trace live cache scenarios through embedded runner paths, and show cache reuse explicitly in `openclaw status --verbose`. Thanks @vincentkoc.
- Agents/cache: stabilize cache-relevant system prompt fingerprints by normalizing equivalent structured prompt whitespace, line endings, hook-added system context, and runtime capability ordering so semantically unchanged prompts reuse KV/cache more reliably. Thanks @vincentkoc.
- Agents/tool prompts: remove the duplicate in-band tool inventory from agent system prompts so tool-calling models rely on the structured tool definitions as the single source of truth, improving prompt stability and reducing stale tool guidance.
- Config/schema: enrich the exported `openclaw config schema` JSON Schema with field titles and descriptions so editors, agents, and other schema consumers receive the same config help metadata. (#60067) Thanks @solavrc.
- Matrix/exec approvals: clarify unavailable-approval replies so Matrix no longer claims chat approvals are unsupported when native exec approvals are merely unconfigured. (#61424) Thanks @gumadeiras.
- Providers/OpenAI Codex: add forward-compat `openai-codex/gpt-5.4-mini` synthesis across provider runtime, model catalog, and model listing so Codex mini works before bundled Pi catalog updates land.
- Providers/OpenAI: add an opt-in GPT personality and move GPT-5 prompt tuning onto provider-owned system-prompt contributions so cache-stable guidance stays above the prompt cache boundary and embedded runner paths reuse the same provider-specific prompt behavior.
- Docs/IRC: replace public IRC hostname examples with `irc.example.com` and recommend private servers for bot coordination while listing common public networks for intentional use.
- Memory/dreaming: group nearby daily-note lines into short coherent chunks before staging them for dreaming, so one-off context from recent notes reaches REM/deep with better evidence and less line-level noise.
- Memory/dreaming: drop generic date/day headings from daily-note chunk prefixes while keeping meaningful section labels, so staged snippets stay cleaner and more reusable. (#61597) Thanks @mbelinky.
- Plugins/Lobster: run bundled Lobster workflows in process instead of spawning the external CLI, reducing transport overhead and unblocking native runtime integration. (#61523) Thanks @mbelinky.
- Plugins/Lobster: harden managed resume validation so invalid TaskFlow resume calls fail earlier, and memoize embedded runtime loading per runner while keeping failed loads retryable. (#61566) Thanks @mbelinky.
- Agents/bootstrap: add opt-in `agents.defaults.contextInjection: "continuation-skip"` so safe continuation turns can skip workspace bootstrap re-injection, while heartbeat runs and post-compaction retries still rebuild context when needed. Fixes #9157. Thanks @cgdusek.
### Fixes
- Control UI/chat: show `/tts` and other local audio-only slash replies in webchat by embedding local audio in the assistant message and rendering `<audio>` controls instead of dropping empty-text finals. Fixes #61564. (#61598) Thanks @neeravmakwana.
- Security: preserve restrictive plugin-only tool allowlists, require owner access for `/allowlist add` and `/allowlist remove`, fail closed when `before_tool_call` hooks crash, block browser SSRF redirect bypasses earlier, and keep non-interactive auth-choice inference scoped to bundled and already-trusted plugins. (#58476, #59836, #59822, #58771, #59120) Thanks @eleqtrizit and @pgondhi987.
- Providers/OpenAI: make GPT-5 and Codex runs act sooner with lower-verbosity defaults, visible progress during tool work, and a one-shot retry when a turn only narrates the plan instead of taking action.
- Providers/OpenAI and reply delivery: preserve native `reasoning.effort: "none"` and strict schemas where supported, add GPT-5.4 assistant `phase` metadata across replay and the Gateway `/v1/responses` layer, and keep commentary buffered until `final_answer` so web chat, session previews, embedded replies, and Telegram partials stop leaking planning text. Fixes #59150, #59643, #61282.
- Telegram: fix current-model checks in the model picker, HTML-format non-default `/model` confirmations, explicit topic replies, persisted reaction ownership across restarts, caption-media placeholder and `file_id` preservation on download failure, and upgraded-install inbound image reads. (#60384, #60042, #59634, #59207, #59948, #59971) Thanks @sfuminya, @GitZhangChi, @dashhuang, @samzong, @v1p0r, and @neeravmakwana.
- Telegram: restore DM voice-note preflight transcription so direct-message audio stops arriving as raw `<media:audio>` placeholders. (#61008) Thanks @manueltarouca.
- Telegram/reasoning: only create a Telegram reasoning preview lane when the session is explicitly `reasoning:stream`, so hidden `<think>` traces from streamed replies stop surfacing as chat previews on normal sessions. Thanks @vincentkoc.
- Telegram/native command menu: trim long menu descriptions before dropping commands so sub-100 command sets can still fit Telegram's payload budget and keep more `/` entries visible. (#61129) Thanks @neeravmakwana.
- Telegram/startup: bound `deleteWebhook`, `getMe`, and `setWebhook` startup requests while keeping the longer `getUpdates` poll timeout, so wedged Telegram control-plane calls stop hanging startup indefinitely. (#61601) Thanks @neeravmakwana.
- Agents/failover: classify Anthropic "extra usage" exhaustion as billing so same-turn model fallback still triggers when Claude blocks long-context requests on usage limits. (#61608) Thanks @neeravmakwana.
- Discord: keep REST, webhook, and monitor traffic on the configured proxy, preserve component-only media sends, honor `@everyone` and `@here` mention gates, keep ACK reactions on the active account, and split voice connect/playback timeouts so auto-join is more reliable. (#57465, #60361, #60345) Thanks @geekhuashan.
- Discord/reply tags: strip leaked `[[reply_to_current]]` control tags from preview text and honor explicit reply-tag threading during final delivery, so Discord replies stay attached to the triggering message instead of printing reply metadata into chat.
- Discord/replies: replace the unshipped `replyToOnlyWhenBatched` flag with `replyToMode: "batched"` so native reply references only attach on debounced multi-message turns while explicit reply tags still work.
- Discord/image generation: include the real generated `MEDIA:` paths in tool output, avoid duplicate plain-output media requeueing, and persist volatile workspace-generated media into durable outbound media before final reply delivery so generated image replies stop pointing at missing local files.
- Slack: route live DM replies back to the concrete inbound DM channel while keeping persisted routing metadata user-scoped, so normal assistant replies stop disappearing when pairing and system messages still arrive. (#59030) Thanks @afurm.
- WhatsApp: restore `channels.whatsapp.blockStreaming` and reset watchdog timeouts after reconnect so quiet chats stop falling into reconnect loops. (#60007, #60069) Thanks @MonkeyLeeT and @mcaxtr.
- Android/Talk Mode: cancel in-flight `talk.speak` playback when speech is explicitly stopped, and restore spoken replies on both node-scoped and gateway-backed sessions by keeping reply routing and embedded transport overrides aligned with the current playback path. (#60306, #61164, #61214)
- Voice-call/OpenAI: pass full plugin config into realtime transcription provider resolution so streaming calls can discover the bundled OpenAI realtime transcription provider again. Fixes #60936. Thanks @sliekens and @vincentkoc.
- Matrix/exec approvals: anchor seeded approval reactions to the primary Matrix prompt event, resolve them from event metadata instead of prompt text, and clean up chunked approval prompts correctly. (#60931) Thanks @gumadeiras.
- Matrix: recover more reliably when secret storage or recovery keys are missing by recreating secret storage during repair and backup reset, hold crypto snapshot locks during persistence, and surface explicit too-large attachment markers. (#59846, #59851, #60599, #60289) Thanks @al3mart, @emonty, and @efe-arv.
- Matrix/DM sessions: add `channels.matrix.dm.sessionScope`, shared-session collision notices, and aligned outbound session reuse so separate Matrix DM rooms can keep distinct context when configured. (#61373) Thanks @gumadeiras.
- Matrix: move legacy top-level `avatarUrl` into the default account during multi-account promotion and keep env-backed account setup avatar config persisted. (#61437) Thanks @gumadeiras.
- MS Teams: download inline DM images via Graph API and preserve channel reply threading in proactive fallback. (#52212, #55198) Thanks @Ted-developer and @hyojin.
- MS Teams: replace the deprecated Teams SDK HttpPlugin stub with `httpServerAdapter` so recurring gateway deprecation warnings stop firing and the Express 5 compatibility workaround stays on the supported SDK path. (#60939) Thanks @coolramukaka-sys.
- Control UI/chat: add a per-session thinking-level picker in the chat header and mobile chat settings, and keep the browser bundle on UI-local thinking/session-key helpers so Safari no longer crashes on Node-only imports before rendering chat controls.
- Sandbox/SSH: reject hardlinked files during cross-device rename fallback so EXDEV file copies preserve the same pinned file-boundary checks as direct reads.
- Control UI: keep Stop visible during tool-only execution, preserve pending-send busy state, and clear stale ClawHub search results as soon as the query changes. (#54528, #59800, #60267) Thanks @chziyue and @frankekn.
- Control UI/avatar: honor `ui.assistant.avatar` when serving `/avatar/:agentId` so Appearance UI avatar paths stop falling back to initials placeholders. (#60778) Thanks @hannasdev.
- Control UI/cron: highlight the Cron refresh button while refresh is in flight so the page's loading state stays visible even when prior data remains on screen. (#60394) Thanks @coder-zhuzm.
- Control UI/Overview: prevent gateway access token/password visibility toggle buttons from overlapping their inputs at narrow widths. (#56924) Thanks @bbddbb1.
- Auto-reply: unify reply lifecycle ownership across preflight compaction, session rotation, CLI-backed runs, and gateway restart handling so `/stop` and same-session overlap checks target the right active turn and restart-interrupted turns return the restart notice instead of being silently dropped. (#61267) Thanks @dutifulbob.
- Reply delivery: prevent duplicate block replies on `text_end` channels so providers that emit explicit text-end boundaries no longer double-send the same final message. (#61530)
- Gateway/startup: default `gateway.mode` to `local` when unset, detect PID recycling in gateway lock files on Windows and macOS, and show startup progress so healthy restarts stop getting blocked by stale locks. (#54801, #60085, #59843) Thanks @BradGroux and @TonyDerek-dot.
- Gateway/macOS: let launchd `KeepAlive` own in-process gateway restarts again, adding a short supervised-exit delay so rapid restarts avoid launchd crash-loop unloads while `openclaw gateway restart` still reports real LaunchAgent errors synchronously.
- Gateway/macOS: re-bootstrap the LaunchAgent if `launchctl kickstart -k` unloads it during restart so failed restarts do not leave the gateway unmanaged until manual repair.
- Gateway/macOS: recover installed-but-unloaded LaunchAgents during `openclaw gateway start` and `restart`, while still preferring live unmanaged gateways during restart recovery. (#43766) Thanks @HenryC-3.
- Gateway/Windows scheduled tasks: preserve Task Scheduler settings on reinstall, fail loudly when `/Run` does not start, and report fast failed restarts accurately instead of pretending they timed out after 60 seconds. (#59335) Thanks @tmimmanuel.
- Windows/restart: fall back to the installed Startup-entry launcher when the scheduled task was never registered, so `/restart` can relaunch the gateway on Windows setups where `schtasks` install fell back during onboarding. (#58943) Thanks @imechZhangLY.
- Windows/restart: clean up stale gateway listeners before Windows self-restart and treat listener and argv probe failures as inconclusive, so scheduled-task relaunch no longer falls into an `EADDRINUSE` retry loop. (#60480) Thanks @arifahmedjoy.
- Update/npm: prefer the npm binary that owns the installed global OpenClaw prefix so mixed Homebrew-plus-nvm setups update the right install. (#60153) Thanks @jayeshp19.
- Agents/music and video generation: add `tools.media.asyncCompletion.directSend` as an opt-in direct-delivery path for finished async media tasks, while keeping the legacy requester-session wake/model-delivery flow as the default.
- CLI/skills JSON: route `skills list --json`, `skills info --json`, and `skills check --json` output to stdout instead of stderr so machine-readable consumers receive JSON on the expected stream again. (#60914; fixes #57599; landed from contributor PR #57611 by @Aftabbs) Thanks @Aftabbs.
- CLI/Commander: preserve Commander-computed exit codes for argument and help-error paths, and cover the user-argv parse mode in the regression tests so invalid CLI invocations no longer report success when exits are intercepted. (#60923) Thanks @Linux2010.
- Cron: replay interrupted recurring jobs on the first gateway restart instead of waiting for a second restart. (#60583) Thanks @joelnishanth.
- Cron: send failure notifications through the job's primary delivery channel using the same session context as successful delivery when no explicit `failureDestination` is configured. (#60622) Thanks @artwalker.
- Exec/remote skills: stop advertising `exec host=node` when the current exec policy cannot route to a node, and clarify blocked exec-host override errors with both the requested host and allowed config path.
- Agents/Claude CLI/security: clear inherited Claude Code config-root and plugin-root env overrides like `CLAUDE_CONFIG_DIR` and `CLAUDE_CODE_PLUGIN_*`, so OpenClaw-launched Claude CLI runs cannot be silently pointed at an alternate Claude config/plugin tree with different hooks, plugins, or auth context. Thanks @vincentkoc.
- Agents/Claude CLI/security: clear inherited Claude Code provider-routing and managed-auth env overrides, and mark OpenClaw-launched Claude CLI runs as host-managed, so Claude CLI backdoor sessions cannot be silently redirected to proxy, Bedrock, Vertex, Foundry, or parent-managed token contexts. Thanks @vincentkoc.
- Agents/Claude CLI/security: force host-managed Claude CLI backdoor runs to `--setting-sources user`, even under custom backend arg overrides, so repo-local `.claude` project/local settings, hooks, and plugin discovery do not silently execute inside non-interactive OpenClaw sessions. Thanks @vincentkoc.
- Agents/Claude CLI: treat malformed bare `--permission-mode` backend overrides as missing and fail safe back to `bypassPermissions`, so custom `cliBackends.claude-cli.args` security config cannot accidentally consume the next flag as a bogus permission mode. Thanks @vincentkoc.
- Gateway/device pairing: require non-admin paired-device sessions to manage only their own device for token rotate/revoke and paired-device removal, blocking cross-device token theft inside pairing-scoped sessions. (#50627) Thanks @coygeek.
- Gateway/plugin routes: keep gateway-auth plugin runtime routes on write-only fallback scopes unless a trusted-proxy caller explicitly declares narrower `x-openclaw-scopes`, so plugin HTTP handlers no longer mint admin-level runtime scopes on missing or untrusted HTTP scope headers. (#59815) Thanks @pgondhi987.
- Build/types: fix the Node `createRequire(...)` helper typing so provider-runtime lazy loads compile cleanly again and `pnpm build` no longer fails in the Pi embedded provider error-pattern path.
- Gateway/security: scope loopback browser-origin auth throttling by normalized origin so one localhost Control UI tab cannot lock out a different localhost browser origin after repeated auth failures.
- Gateway/auth: serialize async shared-secret auth attempts per client so concurrent Tailscale-capable failures cannot overrun the intended auth rate-limit budget. Thanks @Telecaster2147.
- Device pairing/security: keep non-operator device scope checks bound to the requested role prefix so bootstrap verification cannot redeem `operator.*` scopes through `node` auth. (#57258) Thanks @jlapenna.
- Device pairing: reject rotating device tokens into roles that were never approved during pairing, and keep reconnect role checks bounded to the paired device's approved role set. (#60462) Thanks @eleqtrizit.
- Gateway/device auth: reuse cached device-token scopes only for cached-token reconnects, while keeping explicit `deviceToken` scope requests and empty-cache fallbacks intact so reconnects preserve `operator.read` without breaking explicit auth flows. (#46032) Thanks @caicongyang.
- Mobile pairing/security: fail closed for internal `/pair` setup-code issuance, cleanup, and approval paths when gateway pairing scopes are missing, and keep approval-time requested-scope enforcement on the internal command path. (#55996) Thanks @coygeek.
- Mobile pairing/bootstrap: keep QR bootstrap handoff tokens bounded to the mobile-safe contract so node handoff stays unscoped and operator handoff drops mixed `node.*`, `operator.admin`, and `operator.pairing` scopes.
- Mobile pairing/Android: tighten secure endpoint handling so Tailscale and public remote setup reject cleartext endpoints, private LAN pairing still works, merged-role approvals mint both node and operator device tokens, and bootstrap tokens survive node auto-pair until operator approval finishes. (#60128, #60208, #60221) Thanks @obviyus.
- Android/canvas security: require exact normalized A2UI URL matches before forwarding canvas bridge actions, rejecting query mismatches and descendant paths while still allowing fragment-only A2UI navigation.
- Synology Chat/security: default low-level HTTPS helper TLS verification to on so helper/API defaults match the shipped safe account default, and only explicit `allowInsecureSsl: true` opts out.
- Synology Chat/security: route webhook token comparison through the shared constant-time secret helper for consistency with other bundled plugins.
- Plugins/marketplace: block remote marketplace symlink escapes without breaking ordinary local marketplace install paths. (#60556) Thanks @eleqtrizit.
- Telegram/local Bot API: honor `channels.telegram.apiRoot` for buffered media downloads, add `channels.telegram.network.dangerouslyAllowPrivateNetwork` for trusted fake-IP setups, and require `channels.telegram.trustedLocalFileRoots` before reading absolute Bot API `file_path` values. (#59544, #60705) Thanks @SARAMALI15792 and @obviyus.
- Outbound/sanitizer: strip leaked `<tool_call>`, `<function_calls>`, and model special tokens from shared user-visible assistant text, including truncated tool-call streams, so internal scaffolding no longer bleeds into replies across surfaces. (#60619) Thanks @oliviareid-svg.
- Agents/errors: surface an explicit disk-full message when local session or transcript writes fail with `ENOSPC`/`disk full`, so those runs stop degrading into opaque `NO_REPLY`-style failures. Thanks @vincentkoc.
- Exec approvals: remove heuristic command-obfuscation gating from host exec so gateway and node runs rely on explicit policy, allowlist, and strict inline-eval rules only.
- Agents/tool results: cap live tool-result persistence and overflow-recovery truncation at 40k characters so oversized tool output stays bounded without discarding recent context entirely.
- Discord/video replies: split text-plus-video deliveries into a text reply followed by a media-only send, and let live provider auth checks honor manifest-declared API key env vars like `MODELSTUDIO_API_KEY`.
- Config/All Settings: keep the raw config view intact when sensitive fields are blank instead of corrupting or dropping the rendered snapshot. (#28214) Thanks @solodmd.
- Plugin SDK/facades: back-fill bundled plugin facade sentinels before plugin-id tracking re-enters config loading, so CLI/provider startup no longer crashes with `shouldNormalizeGoogleProviderConfig is not a function` or other empty-facade reads during bundled plugin re-entry. Thanks @adam91holt.
- Plugins/facades: back-fill facade sentinels before tracked-plugin resolution re-enters config loading, so facade exports stay defined during circular provider normalization. (#61180) Thanks @adam91holt.
- QA lab: restore typed mock OpenAI gateway config wiring so QA-lab config helpers compile cleanly again and `pnpm check` / `pnpm build` stay green.
- Discord/image generation: include the real generated `MEDIA:` paths in tool output and avoid duplicate plain-output media requeueing so Discord image replies stop pointing at missing local files.
- Slack: route live DM replies back to the concrete inbound DM channel while keeping persisted routing metadata user-scoped, so normal assistant replies stop disappearing when pairing and system messages still arrive. (#59030) Thanks @afurm.
- Discord/reply tags: strip leaked `[[reply_to_current]]` control tags from preview text and honor explicit reply-tag threading during final delivery, so Discord replies stay attached to the triggering message instead of printing reply metadata into chat.
- Telegram: fix current-model checks in the model picker, HTML-format non-default `/model` confirmations, explicit topic replies, persisted reaction ownership across restarts, caption-media placeholder and `file_id` preservation on download failure, and upgraded-install inbound image reads. (#60384, #60042, #59634, #59207, #59948, #59971) Thanks @sfuminya, @GitZhangChi, @dashhuang, @samzong, @v1p0r, and @neeravmakwana.
- Telegram: restore DM voice-note preflight transcription so direct-message audio stops arriving as raw `<media:audio>` placeholders. (#61008) Thanks @manueltarouca.
- Telegram/reasoning: only create a Telegram reasoning preview lane when the session is explicitly `reasoning:stream`, so hidden `<think>` traces from streamed replies stop surfacing as chat previews on normal sessions. Thanks @vincentkoc.
- Telegram/native command menu: trim long menu descriptions before dropping commands so sub-100 command sets can still fit Telegram's payload budget and keep more `/` entries visible. (#61129) Thanks @neeravmakwana.
- Feishu/reasoning: only expose streamed reasoning previews when the session is explicitly `reasoning:stream`, so hidden reasoning traces do not surface on normal streaming sessions. Thanks @vincentkoc.
- Discord: keep REST, webhook, and monitor traffic on the configured proxy, preserve component-only media sends, honor `@everyone` and `@here` mention gates, keep ACK reactions on the active account, and split voice connect/playback timeouts so auto-join is more reliable. (#57465, #60361, #60345) Thanks @geekhuashan.
- WhatsApp: restore `channels.whatsapp.blockStreaming` and reset watchdog timeouts after reconnect so quiet chats stop falling into reconnect loops. (#60007, #60069) Thanks @MonkeyLeeT and @mcaxtr.
- Memory: keep `memory-core` builtin embedding registration on the already-registered path so selecting `memory-core` no longer recurses through plugin discovery and crashes during startup. (#61402) Thanks @ngutman.
- Agents/tool results: keep large `read` outputs visible longer, preserve the latest `read` output when older tool output can absorb the overflow budget, and fall back to Pi's normal overflow compaction/retry path before replacing a fresh `read` with a compacted stub. Thanks @vincentkoc.
- Memory/QMD: prefer modern `qmd collection add --glob`, accept newer single-line JSON hit metadata while keeping legacy line fields, refresh QMD docs/doctor install guidance and model-override guidance, and keep older QMD releases working. Thanks @vincentkoc.
- MS Teams: download inline DM images via Graph API and preserve channel reply threading in proactive fallback. (#52212, #55198) Thanks @Ted-developer and @hyojin.
- MS Teams: replace the deprecated Teams SDK HttpPlugin stub with `httpServerAdapter` so recurring gateway deprecation warnings stop firing and the Express 5 compatibility workaround stays on the supported SDK path. (#60939) Thanks @coolramukaka-sys.
- Matrix/exec approvals: anchor seeded approval reactions to the primary Matrix prompt event, resolve them from event metadata instead of prompt text, and clean up chunked approval prompts correctly. (#60931) Thanks @gumadeiras.
- Matrix: recover more reliably when secret storage or recovery keys are missing by recreating secret storage during repair and backup reset, hold crypto snapshot locks during persistence, and surface explicit too-large attachment markers. (#59846, #59851, #60599, #60289) Thanks @al3mart, @emonty, and @efe-arv.
- Android/Talk Mode: cancel in-flight `talk.speak` playback when speech is explicitly stopped, so stale replies stop starting after barge-in or manual stop. (#61164) Thanks @obviyus.
- Android/Talk Mode: restore spoken assistant replies on node-scoped sessions by keeping reply routing synced to the resolved node session key and pausing mic capture during reply playback. (#60306) Thanks @MKV21.
- Android/Talk Mode: restore voice replies on gateway-backed talk mode sessions by updating embedded runner transport overrides to the current agent transport API. (#61214) Thanks @obviyus.
- Voice-call/OpenAI: pass full plugin config into realtime transcription provider resolution so streaming calls can discover the bundled OpenAI realtime transcription provider again. Fixes #60936. Thanks @sliekens and @vincentkoc.
- Control UI/chat: add a per-session thinking-level picker in the chat header and mobile chat settings, and keep the browser bundle on UI-local thinking/session-key helpers so Safari no longer crashes on Node-only imports before rendering chat controls.
- Control UI: keep Stop visible during tool-only execution, preserve pending-send busy state, and clear stale ClawHub search results as soon as the query changes. (#54528, #59800, #60267) Thanks @chziyue and @frankekn.
- Control UI/avatar: honor `ui.assistant.avatar` when serving `/avatar/:agentId` so Appearance UI avatar paths stop falling back to initials placeholders. (#60778) Thanks @hannasdev.
- Control UI/cron: highlight the Cron refresh button while refresh is in flight so the page's loading state stays visible even when prior data remains on screen. (#60394) Thanks @coder-zhuzm.
- Control UI/Overview: prevent gateway access token/password visibility toggle buttons from overlapping their inputs at narrow widths. (#56924) Thanks @bbddbb1.
- CLI/skills JSON: route `skills list --json`, `skills info --json`, and `skills check --json` output to stdout instead of stderr so machine-readable consumers receive JSON on the expected stream again. (#60914; fixes #57599; landed from contributor PR #57611 by @Aftabbs) Thanks @Aftabbs.
- CLI/Commander: preserve Commander-computed exit codes for argument and help-error paths, and cover the user-argv parse mode in the regression tests so invalid CLI invocations no longer report success when exits are intercepted. (#60923) Thanks @Linux2010.
- Cron: replay interrupted recurring jobs on the first gateway restart instead of waiting for a second restart. (#60583) Thanks @joelnishanth.
- Cron: send failure notifications through the job's primary delivery channel using the same session context as successful delivery when no explicit `failureDestination` is configured. (#60622) Thanks @artwalker.
- Live model switching: only treat explicit user-driven model changes as pending live switches, so fallback rotation, heartbeat overrides, and compaction no longer trip `LiveSessionModelSwitchError` before making an API call. (#60266) Thanks @kiranvk-2011.
- Exec approvals: reuse durable exact-command `allow-always` approvals in allowlist mode so identical reruns stop prompting, and tighten Windows interpreter/path approval handling so wrapper and malformed-path cases fail closed more consistently. (#59880, #59780, #58040, #59182) Thanks @luoyanglang, @SnowSky1, and @pgondhi987.
- Node exec approvals: keep node-host `system.run` approvals bound to the prepared execution plan across async forwarding, so mutable script operands still get approval-time binding and drift revalidation instead of dropping back to unbound execution.
- Agents/exec approvals: let `exec-approvals.json` agent security override stricter gateway tool defaults so approved subagents can use `security: “full”` without falling back to allowlist enforcement again. (#60310) Thanks @lml2468.
- Agents/exec: restore `host=node` routing for node-pinned and `host=auto` sessions, while still blocking sandboxed `auto` sessions from jumping to gateway. (#60788) Thanks @openperf.
- Exec/heartbeat: use the canonical `exec-event` wake reason for `notifyOnExit` so background exec completions still trigger follow-up turns when `HEARTBEAT.md` is empty or comments-only. (#41479) Thanks @rstar327.
- Heartbeat: skip wake delivery when the target session lane is already busy so the pending event is retried instead of getting drained too early. (#40526) Thanks @lucky7323.
- Group chats/agent prompts: tell models to minimize empty lines and use normal chat-style spacing so group replies avoid document-style blank-line formatting.
- Providers/OpenAI GPT: treat short approval turns like `ok do it` and `go ahead` as immediate action turns, and trim overly memo-like GPT-5 chat confirmations so OpenAI replies stay shorter and more conversational by default.
- Providers/OpenAI Codex: split native `contextWindow` from runtime `contextTokens`, keep the default effective cap at `272000`, and expose a per-model `contextTokens` override on `models.providers.*.models[]`.
- Providers/OpenAI-compatible WS: compute fallback token totals from normalized usage when providers omit or zero `total_tokens`, so DashScope-compatible sessions stop storing zero totals after alias normalization. (#54940) Thanks @lyfuci.
- Agents/OpenAI: mark Claude-compatible file tool schemas as `additionalProperties: false` so direct OpenAI GPT-5 routes stop rejecting the `read` tool with invalid strict-schema errors.
- Agents/OpenAI: fall back to `strict: false` for native OpenAI tool calls when a tool schema is not strict-compatible, and normalize empty-object tool schemas to include `required: []`, so direct GPT-5 routes stop failing with invalid strict-schema errors like missing `path` in `required`.
- Agents/GPT: add explicit work-item lifecycle events for embedded runs, use them to surface real progress more reliably, and stop counting tool-started turns as planning-only retries.
- Plugins/OpenAI: enable `gpt-image-1` reference-image edits through `/images/edits` multipart uploads, and stop inferring unsupported resolution overrides when no explicit `size` or `resolution` is provided.
- Agents/replay: remove the malformed assistant-content canonicalization repair from replay history sanitization instead of extending that legacy repair path into replay validation.
- Plugins/OpenAI: tune the OpenAI prompt overlay for live-chat cadence so GPT replies stay shorter, more human, and less wall-of-text by default.
- Providers/compat: stop forcing OpenAI-only defaults on proxy and custom OpenAI-compatible routes, preserve native vendor-specific reasoning/tool/streaming behavior across Anthropic-compatible, Moonshot, Mistral, ModelStudio, OpenRouter, xAI, and Z.ai endpoints, and route GitHub Copilot Claude models through Anthropic Messages instead of OpenAI Responses.
- Providers/GitHub Copilot: send IDE identity headers on runtime model requests and GitHub token exchange so IDE-authenticated Copilot runs stop failing with missing `Editor-Version`. (#60641) Thanks @VACInc and @vincentkoc.
- Providers/OpenRouter failover: classify `403 “Key limit exceeded”` spending-limit responses as billing so model fallback continues instead of stopping on generic auth. (#59892) Thanks @rockcent.
- Providers/Anthropic: keep `claude-cli/*` auth on live Claude CLI credentials at runtime, avoid persisting stale bearer-token profiles, and suppress macOS Keychain prompts during non-interactive Claude CLI setup. (#61234) Thanks @darkamenosa.
- Providers/Anthropic: when Claude CLI auth becomes the default, write a real `claude-cli` auth profile so local and gateway agent runs can use Claude CLI immediately without missing-API-key failures. Thanks @vincentkoc.
- Providers/Anthropic Vertex: honor `cacheRetention: “long”` with the real 1-hour prompt-cache TTL on Vertex AI endpoints, and default `anthropic-vertex` cache retention like direct Anthropic. (#60888) Thanks @affsantos.
- Agents/Anthropic: preserve native `toolu_*` replay ids on direct Anthropic and Anthropic Vertex paths so cache-sensitive history stops rewriting known-valid Anthropic tool-use ids. (#52612)
- Providers/Google: add model-level `cacheRetention` support for direct Gemini system prompts by creating, reusing, and refreshing `cachedContents` automatically on Google AI Studio runs. (#51372) Thanks @rafaelmariano-glitch.
- Google Gemini CLI auth: detect bundled npm installs by scanning packaged bundle files for the Gemini OAuth client config, so `npm install -g @google/gemini-cli` layouts work again. (#60486) Thanks @wzfmini01.
- Google Gemini CLI auth: detect personal OAuth mode from local Gemini settings and skip Code Assist project discovery for those logins, so personal Google accounts stop failing with `loadCodeAssist 400 Bad Request`. (#49226) Thanks @bobworrall.
- Google Gemini CLI auth: improve OAuth credential discovery across Windows nvm and Homebrew libexec installs, and align Code Assist metadata so Gemini login stops failing on packaged CLI layouts. (#40729) Thanks @hughcube.
- Google Gemini CLI models: add forward-compat support for stable `gemini-2.5-*` model ids by letting the bundled CLI provider clone them from Google templates, so `gemini-2.5-flash-lite` and related configured models stop showing up as missing. (#35274) Thanks @mySebbe.
- Google image generation: disable pinned DNS for Gemini image requests and honor explicit `pinDns` overrides in shared provider HTTP helpers so proxy-backed image generation works again. (#59873) Thanks @luoyanglang.
- Providers/Microsoft Foundry: preserve explicit image capability on normalized Foundry deployments, repair stale GPT/o-series text-only model metadata across gateway and runtime paths, and keep unknown fallback models from borrowing unrelated image support.
- Providers/Model Studio: preserve native streaming usage reporting for DashScope-compatible endpoints even when they are configured under a generic provider key, so streamed token totals stop sticking at zero. (#52395) Thanks @IVY-AI-gif.
- Providers/Z.AI: preserve explicitly registered `glm-5-*` variants like `glm-5-turbo` instead of intercepting them with the generic GLM-5 forward-compat shim. (#48185) Thanks @haoyu-haoyu.
- Amazon Bedrock/aws-sdk auth: stop injecting the fake `AWS_PROFILE` apiKey marker when no AWS auth env vars exist, so instance-role and other default-chain setups keep working without poisoning provider config. (#61194) Thanks @wirjo.
- Agents/Kimi tool-call repair: preserve tool arguments that were already present on streamed tool calls when later malformed deltas fail reevaluation, while still dropping stale repair-only state before `toolcall_end`.
- Plugins/Kimi Coding: parse tagged tool calls and keep Anthropic-native tool payloads so Kimi coding endpoints execute tools instead of echoing raw markup. (#60051, #60391) Thanks @obviyus and @Eric-Guo.
- Media understanding: auto-register image-capable config providers for vision routing, so custom GLM-style provider ids with image models stop failing with “no media-understanding provider registered”. (#51418) Thanks @xydt-610.
- Plugins/media understanding: enable bundled Groq and Deepgram providers by default so configured transcription models work without extra plugin activation config. (#59982) Thanks @yxjsxy.
- MiniMax/pricing: keep bundled MiniMax highspeed pricing distinct in provider catalogs and preserve the lower M2.5 cache-read pricing when onboarding older MiniMax models. (#54214) Thanks @octo-patch.
- MiniMax: advertise image input on bundled `MiniMax-M2.7` and `MiniMax-M2.7-highspeed` model definitions so image-capable flows can route through the M2.7 family correctly. (#54843) Thanks @MerlinMiao88888888.
- Models/MiniMax: honor `MINIMAX_API_HOST` for implicit bundled MiniMax provider catalogs so China-hosted API-key setups pick `api.minimaxi.com/anthropic` without manual provider config. (#34524) Thanks @caiqinghua.
- Usage/MiniMax: invert remaining-style `usage_percent` fields when MiniMax reports only remaining percentage data, so usage bars stop showing nearly-full remaining quota as nearly-exhausted usage. (#60254) Thanks @jwchmodx.
- Usage/MiniMax: let usage snapshots treat `minimax-portal` and MiniMax CN aliases as the same MiniMax quota surface, and prefer stored MiniMax OAuth before falling back to Coding Plan keys.
- Usage/MiniMax: prefer the chat-model `model_remains` entry and derive Coding Plan window labels from MiniMax interval timestamps so MiniMax usage snapshots stop picking zero-budget media rows and misreporting 4h windows as `5h`. (#52349) Thanks @IVY-AI-gif.
- Model picker/providers: treat bundled BytePlus and Volcengine plan aliases as their native providers during setup, and expose their bundled standard/coding catalogs before auth so setup can suggest the right models. (#58819) Thanks @Luckymingxuan.
- Tools/web_search (Kimi): when `tools.web.search.kimi.baseUrl` is unset, inherit native Moonshot chat `baseUrl` (`.ai` / `.cn`) so China console keys authenticate on the same host as chat. Fixes #44851. (#56769) Thanks @tonga54.
- Agents/Claude CLI: keep non-interactive `--permission-mode bypassPermissions` when custom `cliBackends.claude-cli.args` override defaults, including fallback resolution before the runtime plugin registry is active, so cron and heartbeat Claude CLI runs do not regress to interactive approval mode. (#61114) Thanks @cathrynlavery and @thewilloftheshadow.
- Agents/Claude CLI: persist explicit `openclaw agent --session-id` runs under a stable session key so follow-ups can reuse the stored CLI binding and resume the same underlying Claude session.
- Agents/Claude CLI: persist routed Claude session bindings, rotate them on `/new` and `/reset`, and keep live Claude CLI model switches moving across the configured Claude family so resumed sessions follow the real active thread and model. Thanks @vincentkoc.
- Agents/CLI backends: invalidate stored CLI session reuse when local CLI login state or the selected auth profile credential changes, so relogin and token rotation stop resuming stale sessions.
- Agents/Claude CLI/images: reuse stable hydrated image file paths and preserve shared media extensions like HEIC when passing image refs to local CLI runs, so Claude CLI image prompts stop thrashing KV cache prefixes and oddball image formats do not fall back to `.bin`. Thanks @vincentkoc.
- Agents/compaction: keep assistant tool calls and displaced tool results in the same compaction chunk so strict summarization providers stop rejecting orphaned tool pairs. (#58849) Thanks @openperf.
- Agents/failover: scope Anthropic `An unknown error occurred` failover matching by provider so generic internal unknown-error text no longer triggers retryable timeout fallback. (#59325) Thanks @aaron-he-zhu.
- Agents/subagents: honor allowlist validation, auth-profile handoff, and session override state when a subagent retries after `LiveSessionModelSwitchError`. (#58178) Thanks @openperf.
- Agents/runtime: make default subagent allowlists, inherited skills/workspaces, and duplicate session-id resolution behave more predictably, and include value-shape hints in missing-parameter tool errors. (#59944, #59992, #59858, #55317) Thanks @hclsys, @gumadeiras, @joelnishanth, and @priyansh19.
- Agents/pairing: merge completion announce delivery context with the requester session fallback so missing `to` still reaches the original channel, and include `operator.talk.secrets` in CLI default operator scopes for node-role device pairing approvals. (#56481) Thanks @maxpetrusenko.
- Agents/scheduling: steer background-now work toward automatic completion wake and treat `process` polling as on-demand inspection or intervention instead of default completion handling. (#60877) Thanks @vincentkoc.
- Agents/skills: skip `.git` and `node_modules` when mirroring skills into sandbox workspaces so read-only sandboxes do not copy repo history or dependency trees. (#61090) Thanks @joelnishanth.
- ACP/agents: inherit the target agent workspace for cross-agent ACP spawns and fall back safely when the inherited workspace no longer exists. (#58438) Thanks @zssggle-rgb.
- ACPX/Windows: preserve backslashes and absolute `.exe` paths in Claude CLI parsing, and fail fast on wrapper-script targets with guidance to use `cmd.exe /c`, `powershell.exe -File`, or `node <script>`. (#60689) Thanks @steipete.
- Auth/failover: persist selected fallback overrides before retrying, shorten `auth_permanent` lockouts, and refresh websocket/shared-auth sessions only when real auth changes occur so retries and secret rotations behave predictably. (#60404, #60323, #60387) Thanks @extrasmall0 and @mappel-nv.
- Gateway/channels: pin the initial startup channel registry before later plugin-registry churn so configured channels stay visible and `channels.status` stops falling back to empty `channelOrder` / `channels` payloads after runtime plugin loads.
- Prompt caching: order stable workspace project-context files before `HEARTBEAT.md` and keep `HEARTBEAT.md` below the system-prompt cache boundary so heartbeat churn does not invalidate the stable project-context prefix. (#58979) Thanks @yozu and @vincentkoc.
- Prompt caching: route Codex Responses and Anthropic Vertex through boundary-aware cache shaping, and report the actual outbound system prompt in cache traces so cache reuse and misses line up with what providers really receive. Thanks @vincentkoc.
- Agents/cache: preserve the full 3-turn prompt-cache image window across tool loops, keep colliding bundled MCP tool definitions deterministic, and reapply Anthropic Vertex cache shaping after payload hook replacements so KV/cache reuse stays stable. Thanks @vincentkoc.
- Status/cache: restore `cacheRead` and `cacheWrite` in transcript fallback so `/status` keeps showing cache hit percentages when session logs are the only complete usage source. (#59247) Thanks @stuartsy.
- Status/usage: let `/status` and `session_status` fall back to transcript token totals when the session meta store stayed at zero, so LM Studio, Ollama, DashScope, and similar OpenAI-compatible providers stop showing `Context: 0/...`. (#55041) Thanks @jjjojoj.
- Mattermost/config schema: accept `groups.*.requireMention` again so existing Mattermost configs no longer fail strict validation after upgrade. (#58271) Thanks @MoerAI.
- Doctor/config: compare normalized `talk` configs by deep structural equality instead of key-order-sensitive serialization so `openclaw doctor --fix` stops repeatedly reporting/applying no-op `talk.provider/providers` normalization. (#59911) Thanks @ejames-dev.
- Anthropic CLI onboarding: rewrite migrated fallback model refs during non-interactive Claude CLI setup too, so onboarding and scripted setup no longer keep stale `anthropic/*` fallbacks after switching the primary model to `claude-cli/*`. Thanks @vincentkoc.
- Models/Anthropic CLI auth: replace migrated `agents.defaults.models` allowlists when `openclaw models auth login --provider anthropic --method cli --set-default` switches to `claude-cli/*`, so stale `anthropic/*` entries do not linger beside the migrated Claude CLI defaults. Thanks @vincentkoc.
- Doctor/Claude CLI: add dedicated Claude CLI health checks so `openclaw doctor` can spot missing local installs or broken auth before agent runs fail. Thanks @vincentkoc.
- Plugins/auth-choice: apply provider-owned auth config patches without recursively preserving replaced default-model maps, so Anthropic Claude CLI and similar migrations can intentionally swap model allowlists during onboarding and setup instead of accumulating stale entries. Thanks @vincentkoc.
- Plugins/onboarding: write dotted plugin uiHint paths like Brave `webSearch.mode` as nested plugin config so `llm-context` setup stops failing validation. (#61159) Thanks @obviyus.
- Plugins/install: preserve unsafe override flags across linked plugin and hook-pack probes so local `--link` installs honor the documented override behavior. (#60624) Thanks @JerrettDavis.
- Plugins/cache: inherit the active gateway workspace for provider, web-search, and web-fetch snapshot loads when callers omit `workspaceDir`, so compatible plugin registries and snapshot caches stop missing on gateway-owned runtime paths. (#61138) Thanks @jzakirov.
- Plugin SDK/context engines: export the missing context-engine result and subagent lifecycle types from `openclaw/plugin-sdk` so context engine plugins can type `ContextEngine` implementations without local workarounds. (#61251) Thanks @DaevMithran.
- Tasks/maintenance: reconcile stale cron and chat-backed CLI task rows against live cron-job and agent-run ownership instead of treating any persisted session key as proof that the task is still running. (#60310) Thanks @lml2468.
- Plugins: suppress trust-warning noise during non-activating snapshot and CLI metadata loads. (#61427) Thanks @gumadeiras.
- Agents/video generation: accept `agents.defaults.videoGenerationModel` in strict config validation and `openclaw config set/get`, so gateways using `video_generate` no longer fail to boot after enabling a video model.
- Matrix/streaming: add a quiet preview mode for streamed Matrix replies, keep legacy `partial` preview-first behavior, and finalize quiet media captions correctly so previews stop notifying early without dropping final text semantics. (#61450) Thanks @gumadeiras.
- Agents/compaction: skip redundant partial summarization when no messages were oversized, so the same transcript is not summarized twice after a full summarization failure. Fixes #61465. (#61603) Thanks @neeravmakwana.
- Gateway/shutdown: bound websocket-server shutdown even when no tracked clients remain, so gateway restarts stop hanging until the watchdog kills the process. (#61565) Thanks @mbelinky.
- Control UI/multilingual: localize the remaining shared channel, instances, nodes, and gateway-confirmation strings so the dashboard stops mixing translated UI with hardcoded English labels. Thanks @vincentkoc.
- Discord/media: raise the default inbound and outbound media cap to `100MB` so Discord matches Telegram more closely and larger attachments stop failing on the old low default.
- Matrix: keep direct transport requests on the pinned dispatcher by routing them through undici runtime fetch, so Matrix clients resume syncing on newer runtimes without dropping the validated address binding. (#61595) Thanks @gumadeiras.
- Plugins/facades: resolve globally installed bundled-plugin runtime facades from registry roots so bundled channels like LINE still boot when the winning plugin install lives under the global extensions directory with an encoded scoped folder name. (#61297) Thanks @openperf.
- Matrix: avoid failing startup when token auth already knows the user ID but still needs optional device metadata, retry transient auth bootstrap requests, and backfill missing device IDs after startup while keeping unknown-device storage reuse conservative until metadata is repaired. (#61383) Thanks @gumadeiras.
- Agents/exec: stop streaming `tool_execution_update` events after an exec session backgrounds, preventing delayed background output from hitting a stale listener and crashing the gateway while keeping the output available through `process poll/log`. (#61627) Thanks @openperf.
- Matrix: pass configured `deviceId` through health probes and keep probe-only client setup out of durable Matrix storage, so health checks preserve the correct device identity without rewriting `storage-meta.json` or related probe state on disk. (#61581) Thanks @MoerAI.
||||||| parent of b4694a4ac7 (Telegram: add outbound chunker regression coverage)
- Image generation/build: write stable runtime alias files into `dist/` and route provider-auth runtime lookups through those aliases so image-generation providers keep resolving auth/runtime modules after rebuilds instead of crashing on missing hashed chunk files.
- Config/runtime: pin the first successful config load in memory for the running process and refresh that snapshot on successful writes/reloads, so hot paths stop reparsing `openclaw.json` between watcher-driven swaps.
- Config/legacy cleanup: stop probing obsolete alternate legacy config names and service labels during local config/service detection, while keeping the active `~/.openclaw/openclaw.json` path canonical.
- ACP/sessions_spawn: register ACP child runs for completion tracking and lifecycle cleanup, and make registration-failure cleanup explicitly best-effort so callers do not assume an already-started ACP turn was fully aborted. (#40885) Thanks @xaeon2026 and @vincentkoc.
- ACP/tasks: mark cleanly exited ACP runs as blocked when they end on deterministic write or authorization blockers, and wake the parent session with a follow-up instead of falsely reporting success.
- ACPX/runtime: derive the bundled ACPX expected version from the extension package metadata instead of hardcoding a separate literal, so plugin-local ACPX installs stop drifting out of health-check parity after version bumps. (#49089) Thanks @jiejiesks and @vincentkoc.
- Gateway/auth: make local-direct `trusted-proxy` fallback require the configured shared token instead of silently authenticating same-host callers, while keeping same-host reverse proxy identity-header flows on the normal trusted-proxy path. Thanks @zhangning-agent and @vincentkoc.
- Memory/QMD: send MCP `query` collection filters as the upstream `collections` array instead of the legacy singular `collection` field, so mcporter-backed QMD 1.1+ searches still scope correctly after the unified `query` tool migration. (#54728) Thanks @armanddp and @vincentkoc.
- Memory/QMD: keep `qmd embed` active in `search` mode too, so BM25-first setups still build a complete index for later vector and hybrid retrieval. (#54509) Thanks @hnshah and @vincentkoc.
- Memory/QMD: point `QMD_CONFIG_DIR` at the nested `xdg-config/qmd` directory so per-agent collection config resolves correctly. (#39078) Thanks @smart-tinker and @vincentkoc.
- Memory/QMD: include deduplicated default plus per-agent `memorySearch.extraPaths` when building QMD custom collections, so shared and agent-specific extra roots both get indexed consistently. (#57315) Thanks @Vitalcheffe and @vincentkoc.
- Memory/session indexer: include `.jsonl.reset.*` and `.jsonl.deleted.*` transcripts in the memory host session scan while still excluding `.jsonl.bak.*` compaction backups and lock files, so memory search sees archived session history without duplicating stale snapshots. Thanks @hclsys and @vincentkoc.
- Agents/sandbox: honor `tools.sandbox.tools.alsoAllow`, let explicit sandbox re-allows remove matching built-in default-deny tools, and keep sandbox explain/error guidance aligned with the effective sandbox tool policy. (#54492) Thanks @ngutman.
- LINE/ACP: add current-conversation binding and inbound binding-routing parity so `/acp spawn ... --thread here`, configured ACP bindings, and active conversation-bound ACP sessions work on LINE like the other conversation channels.
- LINE/markdown: preserve underscores inside Latin, Cyrillic, and CJK words when stripping markdown, while still removing standalone `_italic_` markers on the shared text-runtime path used by LINE and TTS. (#47465) Thanks @jackjin1997.
- TTS/Microsoft: auto-switch the default Edge voice to Chinese for CJK-dominant text without overriding explicitly selected Microsoft voices. (#52355) Thanks @extrasmall0.
- Agents/context pruning: count supplementary-plane CJK characters with the shared code-point-aware estimator so context pruning stops underestimating Japanese and Chinese text that uses Extension B ideographs. (#39985) Thanks @Edward-Qiang-2024.
- Slack/status reactions: add a reaction lifecycle for queued, thinking, tool, done, and error phases in Slack monitors, with safer cleanup so queued ack reactions stay correct across silent runs, pre-reply failures, and delayed transitions. (#56430) Thanks @hsiaoa.
- macOS/local gateway: stop OpenClaw.app from killing healthy local gateway listeners after startup by recognizing the current `openclaw-gateway` process title and using the current `openclaw gateway` launch shape.
- Gateway/OpenAI compatibility: accept flat Responses API function tool definitions on `/v1/responses` and preserve `strict` when normalizing hosted tools into the embedded runner, so spec-compliant clients like Codex no longer fail validation or silently lose strict tool enforcement. Thanks @malaiwah and @vincentkoc.
- Memory/QMD: resolve slugified `memory_search` file hints back to the indexed filesystem path before returning search hits, so `memory_get` works again for mixed-case and spaced paths. (#50313) Thanks @erra9x.
- OpenAI/Codex fast mode: map `/fast` to priority processing on native OpenAI and Codex Responses endpoints instead of rewriting reasoning settings, and document the exact endpoint and override behavior.
- Memory/QMD: weight CJK-heavy text correctly when estimating chunk sizes, preserve surrogate-pair characters during fine splits, and keep long Latin lines on the old chunk boundaries so memory indexing produces better-sized chunks for CJK notes. (#40271) Thanks @AaronLuo00.
- Security/LINE: make webhook signature validation run the timing-safe compare even when the supplied signature length is wrong, closing a small timing side-channel. (#55663) Thanks @gavyngong.
- LINE/status: stop `openclaw status` from warning about missing credentials when sanitized LINE snapshots are already configured, while still surfacing whether the missing field is the token or secret. (#45701) Thanks @tamaosamu.
- Gateway/health: carry webhook-vs-polling account mode from channel descriptors into runtime snapshots so passive channels like LINE and BlueBubbles skip false stale-socket health failures. (#47488) Thanks @karesansui-u.
- Agents/MCP: reuse bundled MCP runtimes across turns in the same session, while recreating them when MCP config changes and disposing stale runtimes cleanly on session rollover. (#55090) Thanks @allan0509.
- Memory/QMD: honor `memory.qmd.update.embedInterval` even when regular QMD update cadence is disabled or slower by arming a dedicated embed-cadence maintenance timer, while avoiding redundant timers when regular updates are already frequent enough. (#37326) Thanks @barronlroth.
- Memory/QMD: add `memory.qmd.searchTool` as an exact mcporter tool override, so custom QMD MCP tools such as `hybrid_search` can be used without weakening the validated `searchMode` config surface. (#27801) Thanks @keramblock.
- Memory/QMD: keep reset and deleted session transcripts in QMD session export so daily session resets do not silently drop most historical recall from `memory_search`. (#30220) Thanks @pushkarsingh32.
- Memory/QMD: rebind collections when QMD reports a changed pattern but omits path metadata, so config pattern changes stop being silently ignored on restart. (#49897) Thanks @Madruru.
- Memory/QMD: warn explicitly when `memory.backend=qmd` is configured but the `qmd` binary is missing, so doctor and runtime fallback no longer fail as a silent builtin downgrade. (#50439) Thanks @Jimmy-xuzimo and @vincentkoc.
- Memory/QMD: pass a direct-session key on `openclaw memory search` so CLI QMD searches no longer get denied as `session=<none>` under direct-only scope defaults. (#43517) Thanks @waynecc-at and @vincentkoc.
- Memory/QMD: keep `memory_search` session-hit paths roundtrip-safe when exported session markdown lives under the workspace `qmd/` directory, so `memory_get` can read the exact returned path instead of failing on the generic `qmd/sessions/...` alias. (#43519) Thanks @holgergruenhagen and @vincentkoc.
- Agents/memory flush: keep daily memory flush files append-only during embedded attempts so compaction writes do not overwrite earlier notes. (#53725) Thanks @HPluseven.
- Web UI/markdown: stop bare auto-links from swallowing adjacent CJK text while preserving valid mixed-script path and query characters in rendered links. (#48410) Thanks @jnuyao.
- BlueBubbles/iMessage: coalesce URL-only inbound messages with their link-preview balloon again so sharing a bare link no longer drops the URL from agent context. Thanks @vincentkoc.
- Sandbox/browser: install `fonts-noto-cjk` in the sandbox browser image so screenshots render Chinese, Japanese, and Korean text correctly instead of tofu boxes. Fixes #35597. Thanks @carrotRakko and @vincentkoc.
- Memory/FTS: add configurable trigram tokenization plus short-CJK substring fallback so memory search can find Chinese, Japanese, and Korean text without breaking mixed long-and-short queries. Thanks @carrotRakko.
- Hooks/config: accept runtime channel plugin ids in `hooks.mappings[].channel` (for example `feishu`) instead of rejecting non-core channels during config validation. (#56226) Thanks @AiKrai001.
- TUI/chat: keep optimistic outbound user messages visible during active runs by deferring local-run binding until the first gateway chat event reveals the real run id, preventing premature history reloads from wiping pending local sends. (#54722) Thanks @seanturner001.
- TUI/model picker: keep searchable `/model` and `/models` input mode from hijacking `j`/`k` as navigation keys, and harden width bounds under `m`-filtered model lists so search no longer crashes on long rows. (#30156) Thanks @briannicholls.
- Agents/Kimi: preserve already-valid Anthropic-compatible tool call argument objects while still clearing cached repairs when later trailing junk exceeds the repair allowance. (#54491) Thanks @yuanaichi.
- Docker/setup: force BuildKit for local image builds (including sandbox image builds) so `./docker-setup.sh` no longer fails on `RUN --mount=...` when hosts default to Docker's legacy builder. (#56681) Thanks @zhanghui-china.
- Control UI/agents: auto-load agent workspace files on initial Files panel open, and populate overview model/workspace/fallbacks from effective runtime agent metadata so defaulted models no longer show as `Not set`. (#56637) Thanks @dxsx84.
- Control UI/slash commands: make `/steer` and `/redirect` work from the chat command palette with visible pending state for active-run `/steer`, correct redirected-run tracking, and a single canonical `/steer` entry in the command menu. (#54625) Thanks @fuller-stack-dev.
- Exec/runtime: default implicit exec to `host=auto`, resolve that target to sandbox only when a sandbox runtime exists, keep explicit `host=sandbox` fail-closed without sandbox, and show `/exec` effective host state in runtime status/docs.
- Exec: fail closed when the implicit sandbox host has no sandbox runtime, and stop denied async approval followups from reusing prior command output from the same session. (#56800) Thanks @scoootscooob.
- Exec/approvals: infer Discord and Telegram exec approvers from existing owner config when `execApprovals.approvers` is unset, extend the default approval window to 30 minutes, and clarify approval-unavailable guidance so approvals do not appear to silently disappear.
- Exec/node: stop gateway-side workdir fallback from rewriting explicit `host=node` cwd values to the gateway filesystem, so remote node exec approval and runs keep using the intended node-local directory. (#50961) Thanks @openperf.
- Plugins/ClawHub: sanitize temporary archive filenames for scoped package names and slash-containing skill slugs so `openclaw plugins install @scope/name` no longer fails with `ENOENT` during archive download. (#56452) Thanks @soimy.
- Telegram/polling: keep the watchdog from aborting long-running reply delivery by treating recent non-polling API activity as bounded liveness instead of a hard stall. (#56343) Thanks @openperf.
- Memory/FTS: keep provider-less keyword hits visible at the default memory-search threshold, so FTS-only recall works without requiring `--min-score 0`. (#56473) Thanks @opriz.
- Memory/LanceDB: resolve runtime dependency manifest lookup from the bundled `extensions/memory-lancedb` path (including flattened dist chunks) so startup no longer fails with a missing `@lancedb/lancedb` dependency error. (#56623) Thanks @LUKSOAgent.
- Tools/web_search: localize the shared search cache to module scope so same-process global symbol lookups can no longer inspect or mutate cached web-search responses. Thanks @vincentkoc.
- Agents/silent turns: fail closed on silent memory-flush runs so narrated `NO_REPLY` self-talk cannot stream or finalize into external replies even when block streaming is enabled. (#52593)
- Browser/plugins: auto-enable the bundled browser plugin when browser config or browser tool policy already references it, and show a clearer CLI error when `plugins.allow` excludes `browser`.
- Matrix/plugin loading: ship and source-load the crypto bootstrap runtime sidecar correctly so current `main` stops warning about failed Matrix bootstrap loads and `matrix/index` plugin-id mismatches on every invocation. (#53298) thanks @keithce.
- iOS/Live Activities: mark the `ActivityKit` import in `LiveActivityManager.swift` as `@preconcurrency` so Xcode 26.4 / Swift 6 builds stop failing on strict concurrency checks. (#57180) Thanks @ngutman.
- Plugins/Matrix: mirror the Matrix crypto WASM runtime dependency into the root packaged install and enforce root/plugin dependency parity so bundled Matrix E2EE crypto resolves correctly in shipped builds. (#57163) Thanks @gumadeiras.
- Plugins/CLI: add descriptor-backed lazy plugin CLI registration so Matrix can keep its CLI module lazy-loaded without dropping `openclaw matrix ...` from parse-time command registration. (#57165) Thanks @gumadeiras.
- Plugins/CLI: collect root-help plugin descriptors through a dedicated non-activating CLI metadata path so enabled plugins keep validated config semantics without triggering runtime-only plugin registration work, while preserving runtime CLI command registration for legacy channel plugins that still wire commands from full registration. (#57294) thanks @gumadeiras.
- Anthropic/OAuth: inject `/fast` `service_tier` hints for direct `sk-ant-oat-*` requests so OAuth-authenticated Anthropic runs stop missing the same overload-routing signal as API-key traffic. Fixes #55758. Thanks @Cypherm and @vincentkoc.
- Anthropic/service tiers: support explicit `serviceTier` model params for direct Anthropic requests and let them override `/fast` defaults when both are set. (#45453) Thanks @vincentkoc.
- Auto-reply/fast: accept `/fast status` on the directive-only path, align help/status text with the documented `status|on|off` syntax, and keep current-state replies consistent across command surfaces. Fixes #46095. Thanks @weissfl and @vincentkoc.
- Telegram/native commands: prefix native command menu callback payloads and preserve `CommandSource: "native"` when Telegram replays them through callback queries, so `/fast` and other native command menus keep working even when text-command routing is disabled. Thanks @vincentkoc.
- Docs/anchors: fix broken English docs links and make Mint anchor audits run against the English-source docs tree. (#57039) thanks @velvet-shark.
- Cron/announce: preserve all deliverable text payloads for announce mode instead of collapsing to the last chunk, so multi-line cron reports deliver in full to Telegram forum topics.
- Harden async approval followup delivery in webchat-only sessions (#57359) Thanks @joshavant.
- Status: fix cache hit rate exceeding 100% by deriving denominator from prompt-side token fields instead of potentially undersized totalTokens. Fixes #26643.
- Config/update: stop `openclaw doctor` write-backs from persisting plugin-injected channel defaults, so `openclaw update` no longer seeds config keys that later break service refresh validation. (#56834) Thanks @openperf.
- Agents/Anthropic failover: treat Anthropic `api_error` payloads with `An unexpected error occurred while processing the response` as transient so retry/fallback can engage instead of surfacing a terminal failure. (#57441) Thanks @zijiess and @vincentkoc.
- Agents/compaction: keep late compaction-retry rejections handled after the aggregate timeout path wins without swallowing real pre-timeout wait failures, so timed-out retries no longer surface an unhandled rejection on later unsubscribe. (#57451) Thanks @mpz4life and @vincentkoc.
- Matrix/delivery recovery: treat Synapse `User not in room` replay failures as permanent during startup recovery so poisoned queued messages move to `failed/` instead of crash-looping Matrix after restart. (#57426) thanks @dlardo.
- Plugins/facades: guard bundled plugin facade loads with a cache-first sentinel so circular re-entry stops crashing `xai`, `sglang`, and `vllm` during gateway plugin startup. (#57508) Thanks @openperf.
- Agents/MCP: dispose bundled MCP runtimes after one-shot `openclaw agent --local` runs finish, while preserving bundled MCP state across in-run retries so local JSON runs exit cleanly without restarting stateful MCP tools mid-run.
- Gateway/OpenAI HTTP: restore default operator scopes for bearer-authenticated requests that omit `x-openclaw-scopes`, so headless `/v1/chat/completions` and session-history callers work again after the recent method-scope hardening. (#57596) Thanks @openperf.
- Gateway/attachments: offload large inbound images without leaking `media://` markers into text-only runs, preserve mixed attachment order for model input/transcripts, and fail closed when model image capability cannot be resolved. (#55513) Thanks @Syysean.
- Telegram/outbound chunking: use static markdown chunking when Telegram runtime state is unavailable so long outbound Telegram messages still split correctly after cold starts. (#57816) Thanks @ForestDengHK.
## 2026.4.2
@@ -132,6 +414,9 @@ Docs: https://docs.openclaw.ai
### Fixes
- Sandbox/security: block credential-path binds even when sandbox home paths resolve through canonical aliases, so agent containers cannot mount user secret stores through alternate home-directory paths. (#59157) Thanks @eleqtrizit.
- Gateway/Windows scheduled tasks: preserve Task Scheduler settings on reinstall, fail loud when Scheduled Task `/Run` does not start, and report fast failed restarts with the actual elapsed time instead of a fake 60s timeout. (#59335) Thanks @tmimmanuel.
- Control UI/model picker: preserve already-qualified `provider/model` refs from the server so models whose ids already contain slashes stop being double-prefixed and remapped to the wrong provider. (#49874) Thanks @ShionEria.
- Models/selection: resolve bare model ids in session model switches against the configured allowlist before falling back to the current session provider, so Control UI model picks stop drifting into `google/k2p5` and similar wrong-provider refs. (#51580) Thanks @honwee.
## 2026.4.1-beta.1
@@ -162,6 +447,7 @@ Docs: https://docs.openclaw.ai
- Image generation/providers: stop inferring private-network access from configured OpenAI, MiniMax, and fal image base URLs, and cap shared HTTP error-body reads so hostile or misconfigured endpoints fail closed without relaxing SSRF policy or buffering unbounded error payloads. Thanks @vincentkoc.
- Browser/host inspection: keep static Chrome inspection helpers out of the activated browser runtime so `openclaw doctor browser` and related checks do not eagerly load the bundled browser plugin. (#59471) Thanks @vincentkoc.
- Browser/CDP: normalize trailing-dot localhost absolute-form hosts before loopback checks so remote CDP websocket URLs like `ws://localhost.:...` rewrite back to the configured remote host. (#59236) Thanks @mappel-nv.
- Browser/attach-only profiles: disconnect cached Playwright CDP sessions when stopping attach-only or remote CDP profiles, while still reporting never-started local managed profiles as not stopped. (#60097) Thanks @pedh.
- Agents/output sanitization: strip namespaced `antml:thinking` blocks from user-visible text so Anthropic-style internal monologue tags do not leak into replies. (#59550) Thanks @obviyus.
- Kimi Coding/tools: normalize Anthropic tool payloads into the OpenAI-compatible function shape Kimi Coding expects so tool calls stop losing required arguments. (#59440) Thanks @obviyus.
- Image tool/paths: resolve relative local media paths against the agent `workspaceDir` instead of `process.cwd()` so inputs like `inbox/receipt.png` pass the local-path allowlist reliably. (#57222) Thanks Priyansh Gupta.
@@ -187,6 +473,16 @@ Docs: https://docs.openclaw.ai
- Exec/node hosts: stop forwarding the gateway workspace cwd to remote node exec when no workdir was explicitly requested, so cross-platform node approvals fall back to the node default cwd instead of failing with `SYSTEM_RUN_DENIED`. (#58977) Thanks @Starhappysh.
- TUI/chat: keep pending local sends visible and reconciled across history reloads, make busy/error recovery clearer through fallback and terminal-error paths, and reclaim transcript width for long links and paths. (#59800) Thanks @vincentkoc.
- Exec approvals/channels: decouple initiating-surface approval availability from native delivery enablement so Telegram, Slack, and Discord still expose approvals when approvers exist and native target routing is configured separately. (#59776) Thanks @joelnishanth.
- Agents/logging: keep orphaned-user transcript repair warnings focused on interactive runs, and downgrade background-trigger repairs (`heartbeat`, `cron`, `memory`, `overflow`) to debug logs to reduce false-alarm gateway noise.
- Gateway/node pairing: require `operator.pairing` for node approvals end-to-end, while still requiring `operator.write` or `operator.admin` when the pending node commands need those higher scopes. (#60461) Thanks @eleqtrizit.
- Providers/OpenRouter: gate Anthropic prompt-cache `cache_control` markers to native/default OpenRouter routes and preserve them for native OpenRouter hosts behind custom provider ids. Thanks @vincentkoc.
- Browser/CDP: validate both initial and discovered CDP websocket endpoints before connect so strict SSRF policy blocks cross-host pivots and direct websocket targets. (#60469) Thanks @eleqtrizit.
- Browser/profiles: reject remote browser profile `cdpUrl` values that violate strict SSRF policy before saving config, with clearer validation errors for blocked endpoints. (#60477) Thanks @eleqtrizit.
- Browser/screenshots: stop sending `fromSurface: false` on CDP screenshots so managed Chrome 146+ browsers can capture images again. (#60682) Thanks @mvanhorn.
- Mattermost/slash commands: harden native slash-command callback token validation to use constant-time secret comparison, matching the existing interaction-token path.
- Control UI/mobile chat: reduce narrow-screen overflow by shrinking the chat pane minimum width, removing extra mobile padding, widening message groups, and hiding avatars on very small screens. (#60220) Thanks @macdao.
- Android/Talk Mode: route spoken replies through `talk.speak`, keep compressed playback cleanup deterministic, and fall back to local TTS for legacy gateways that omit Talk error reasons. (#60954) Thanks @obviyus.
- Android/Talk Mode: keep reply-speaker routing and teardown behavior aligned with the new remote playback path. (#60954) Thanks @MKV21.
## 2026.4.1
@@ -216,6 +512,12 @@ Docs: https://docs.openclaw.ai
- Channels/WhatsApp: pass inbound message timestamp to model context so the AI can see when WhatsApp messages were sent. (#58590) Thanks @Maninae
- QQBot/voice: lazy-load `silk-wasm` in `audio-convert.ts` so qqbot still starts when the optional voice dependency is missing, while voice encode/decode degrades gracefully instead of crashing at module load time. (#58829) Thanks @WideLee.
- WhatsApp/groups: fix bot waking up on self-number quoted replies in groups with `selfChatMode` enabled. (#60148) Thanks @lurebat
- Device pairing: require `operator.pairing` or `operator.admin` for internal `/pair` setup-code, QR, and cleanup commands so lower-privilege gateway callers cannot mint or revoke pairing bootstrap material. (#60491) Thanks @eleqtrizit.
- Agents/failover: unify structured and raw provider error classification so provider-specific `400`/`422` payloads no longer get forced into generic format failures before retry, billing, or compaction logic can inspect them. (#58856) Thanks @aaron-he-zhu.
- Auth profiles/store: coerce misplaced SecretRef objects out of plaintext `key` and `token` fields during store load so agents without ACP runtime stop crashing on `.trim()` after upgrade. (#58923) Thanks @openperf.
- ACPX/runtime: repair `queue owner unavailable` session recovery by replacing dead named sessions and resuming the backend session when ACPX exposes a stable session id, so the first ACP prompt no longer inherits a dead handle. (#58669) Thanks @neeravmakwana
- ACPX/runtime: retry dead-session queue-owner repair without `--resume-session` when the reported ACPX session id is stale, so recovery still creates a fresh named session instead of failing session init. Thanks @obviyus.
- Tools/web_search (Kimi): replay native Moonshot `$web_search` arguments verbatim, disable thinking for `kimi-k2.5`, and add Moonshot region/model setup prompts so bundled Kimi web search works again. (#59356) Thanks @Innocent-children.
## 2026.3.31
@@ -589,6 +891,9 @@ Docs: https://docs.openclaw.ai
- Plugins/Matrix: encrypt E2EE image thumbnails with `thumbnail_file` while keeping unencrypted-room previews on `thumbnail_url`, so encrypted Matrix image events keep thumbnail metadata without leaking plaintext previews. (#54711) thanks @frischeDaten.
- Telegram/forum topics: keep native `/new` and `/reset` routed to the active topic by preserving the topic target on forum-thread command context. (#35963)
- Status/port diagnostics: treat single-process dual-stack loopback gateway listeners as healthy in `openclaw status --all`, suppressing false "port already in use" conflict warnings. (#53398) Thanks @DanWebb1949.
- CLI/Docker: treat loopback private-host CLI gateway connects as local for silent pairing auto-approval, while keeping remote backend and public-host CLI connects behind pairing. (#55113) Thanks @sar618.
## 2026.3.24
### Breaking

View File

@@ -85,6 +85,12 @@ Welcome to the lobster tank! 🦞
4. **Test/CI-only PRs for known `main` failures** → Don't open a PR. The Maintainer team is already tracking those failures, and PRs that only tweak tests or CI to chase them will be closed unless they are required to validate a new fix.
5. **Questions** → Discord [#help](https://discord.com/channels/1456350064065904867/1459642797895319552) / [#users-helping-users](https://discord.com/channels/1456350064065904867/1459007081603403828)
## PR Limits
We cap at **10 open PRs per author**. If you exceed this, the `r: too-many-prs` label is added and your PR is auto-closed. This is a hard limit.
For coordinated change sets that genuinely need more than 10 PRs, join the **#clawtributors** channel in Discord and talk to maintainers first.
## Before You PR
- Test locally with your OpenClaw instance

View File

@@ -64,7 +64,7 @@ WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY ui/package.json ./ui/package.json
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/npm-runner.mjs ./scripts/
COPY scripts/postinstall-bundled-plugins.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
COPY --from=ext-deps /out/ ./${OPENCLAW_BUNDLED_PLUGIN_DIR}/
@@ -97,6 +97,7 @@ RUN pnpm build:docker
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
ENV OPENCLAW_PREFER_PNPM=1
RUN pnpm ui:build
RUN pnpm qa:lab:build
# Prune dev dependencies and strip build-only metadata before copying
# runtime assets into the final image.
@@ -156,6 +157,7 @@ COPY --from=runtime-assets --chown=node:node /app/openclaw.mjs .
COPY --from=runtime-assets --chown=node:node /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} ./${OPENCLAW_BUNDLED_PLUGIN_DIR}
COPY --from=runtime-assets --chown=node:node /app/skills ./skills
COPY --from=runtime-assets --chown=node:node /app/docs ./docs
COPY --from=runtime-assets --chown=node:node /app/qa ./qa
# In npm-installed Docker images, prefer the copied source extension tree for
# bundled discovery so package metadata that points at source entries stays valid.

View File

@@ -34,7 +34,7 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
<table>
<tr>
<td align="center" width="20%">
<td align="center" width="16.66%">
<a href="https://openai.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/openai-light.svg">
@@ -42,7 +42,15 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
</picture>
</a>
</td>
<td align="center" width="20%">
<td align="center" width="16.66%">
<a href="https://github.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/github-light.svg">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/github.svg" alt="GitHub" height="28">
</picture>
</a>
</td>
<td align="center" width="16.66%">
<a href="https://www.nvidia.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/nvidia.svg">
@@ -50,7 +58,7 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
</picture>
</a>
</td>
<td align="center" width="20%">
<td align="center" width="16.66%">
<a href="https://vercel.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/vercel-light.svg">
@@ -58,7 +66,7 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
</picture>
</a>
</td>
<td align="center" width="20%">
<td align="center" width="16.66%">
<a href="https://blacksmith.sh/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/blacksmith-light.svg">
@@ -66,7 +74,7 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
</picture>
</a>
</td>
<td align="center" width="20%">
<td align="center" width="16.66%">
<a href="https://www.convex.dev/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/convex-light.svg">

View File

@@ -97,6 +97,7 @@ When patching a GHSA via `gh api`, include `X-GitHub-Api-Version: 2022-11-28` (o
OpenClaw does **not** model one gateway as a multi-tenant, adversarial user boundary.
- Authenticated Gateway callers are treated as trusted operators for that gateway instance.
- Direct localhost/loopback Control UI and Gateway WebSocket sessions authenticated with the shared gateway secret (`token` / `password`) are in that same trusted-operator bucket. Local auto-paired device sessions on that path are expected to retain full localhost operator capability; they do not create a separate `operator.write` vs `operator.admin` security boundary.
- The HTTP compatibility endpoints (`POST /v1/chat/completions`, `POST /v1/responses`) and direct tool endpoint (`POST /tools/invoke`) are in that same trusted-operator bucket. Passing Gateway bearer auth there is equivalent to operator access for that gateway; they do not implement a narrower `operator.write` vs `operator.admin` trust split.
- Concretely, on the OpenAI-compatible HTTP surface:
- shared-secret bearer auth (`token` / `password`) authenticates possession of the gateway operator secret

View File

@@ -2,6 +2,254 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.4.5</title>
<pubDate>Mon, 06 Apr 2026 04:55:17 +0100</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026040501</sparkle:version>
<sparkle:shortVersionString>2026.4.5</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.5</h2>
<h3>Breaking</h3>
<ul>
<li>Config: remove legacy public config aliases such as <code>talk.voiceId</code> / <code>talk.apiKey</code>, <code>agents.*.sandbox.perSession</code>, <code>browser.ssrfPolicy.allowPrivateNetwork</code>, <code>hooks.internal.handlers</code>, and channel/group/room <code>allow</code> toggles in favor of the canonical public paths and <code>enabled</code>, while keeping load-time compatibility and <code>openclaw doctor --fix</code> migration support for existing configs. (#60726) Thanks @vincentkoc.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Agents/video generation: add the built-in <code>video_generate</code> tool so agents can create videos through configured providers and return the generated media directly in the reply.</li>
<li>Agents/music generation: ignore unsupported optional hints such as <code>durationSeconds</code> with a warning instead of hard-failing requests on providers like Google Lyria.</li>
<li>Providers/ComfyUI: add a bundled <code>comfy</code> workflow media plugin for local ComfyUI and Comfy Cloud workflows, including shared <code>image_generate</code>, <code>video_generate</code>, and workflow-backed <code>music_generate</code> support, with prompt injection, optional reference-image upload, live tests, and output download.</li>
<li>Tools/music generation: add the built-in <code>music_generate</code> tool with bundled Google (Lyria) and MiniMax providers plus workflow-backed Comfy support, including async task tracking and follow-up delivery of finished audio.</li>
<li>Providers: add bundled Qwen, Fireworks AI, and StepFun providers, plus MiniMax TTS, Ollama Web Search, and MiniMax Search integrations for chat, speech, and search workflows. (#60032, #55921, #59318, #54648)</li>
<li>Providers/Amazon Bedrock: add bundled Mantle support plus inference-profile discovery and automatic request-region injection so Bedrock-hosted Claude, GPT-OSS, Qwen, Kimi, GLM, and similar routes work with less manual setup. (#61296, #61299) Thanks @wirjo.</li>
<li>Control UI/multilingual: add localized control UI support for Simplified Chinese, Traditional Chinese, Brazilian Portuguese, German, Spanish, Japanese, Korean, French, Turkish, Indonesian, Polish, and Ukrainian. Thanks @vincentkoc.</li>
<li>Plugins: add plugin-config TUI prompts to guided onboarding/setup flows, and add <code>openclaw plugins install --force</code> so existing plugin and hook-pack targets can be replaced without using the dangerous-code override flag. (#60590, #60544)</li>
<li>Control UI/skills: add ClawHub search, detail, and install flows directly in the Skills panel. (#60134) Thanks @samzong.</li>
<li>iOS/exec approvals: add generic APNs approval notifications that open an in-app exec approval modal, fetch command details only after authenticated operator reconnect, and clear stale notification state when the approval resolves. (#60239) Thanks @ngutman.</li>
<li>Matrix/exec approvals: add Matrix-native exec approval prompts with account-scoped approvers, channel-or-DM delivery, and room-thread aware resolution handling. (#58635) Thanks @gumadeiras.</li>
<li>Channels/context visibility: add configurable <code>contextVisibility</code> per channel (<code>all</code>, <code>allowlist</code>, <code>allowlist_quote</code>) so supplemental quote, thread, and fetched history context can be filtered by sender allowlists instead of always passing through as received.</li>
<li>Providers/request overrides: add shared model and media request transport overrides across OpenAI-, Anthropic-, Google-, and compatible provider paths, including headers, auth, proxy, and TLS controls. (#60200)</li>
<li>Providers/OpenAI: add forward-compat <code>openai-codex/gpt-5.4-mini</code>, an opt-in GPT personality, and provider-owned GPT-5 prompt contributions so Codex/GPT runs stay cache-stable and compatible with bundled catalog lag.</li>
<li>Agents/Claude CLI: expose OpenClaw tools to background Claude CLI runs through a loopback MCP bridge and switch bundled runs to stdin + <code>stream-json</code> partial-message streaming so prompts stop riding argv, long replies show live progress, and final session/usage metadata still land cleanly. (#35676) Thanks @mylukin.</li>
<li>ACPX/runtime: embed the ACP runtime directly in the bundled <code>acpx</code> plugin, remove the extra external ACP CLI hop, harden live ACP session binding and reuse, and add a generic <code>reply_dispatch</code> hook so bundled plugins like ACPX can own reply interception without hardcoded ACP paths in core auto-reply routing. (#61319)</li>
<li>Agents/progress: add experimental structured plan updates and structured execution item events so compatible UIs can show clearer step-by-step progress during long-running runs.</li>
<li>Providers/Anthropic: remove the Claude CLI backend and setup-token from new onboarding, keep existing configured legacy profiles runnable, and have <code>openclaw doctor</code> repair or remove stale <code>anthropic:claude-cli</code> state during migration.</li>
<li>Tools/video generation: add bundled xAI (<code>grok-imagine-video</code>), Alibaba Model Studio Wan, and Runway video providers, plus live-test/default model wiring for all three.</li>
<li>Memory/search: add Amazon Bedrock embeddings for Titan, Cohere, Nova, and TwelveLabs models, with AWS credential-chain auto-detection for <code>provider: "auto"</code> and provider-specific dimension controls. Thanks @wirjo.</li>
<li>Providers/Amazon Bedrock Mantle: generate bearer tokens from the AWS credential chain so Mantle auto-discovery can use IAM auth without manually exporting <code>AWS_BEARER_TOKEN_BEDROCK</code>. Thanks @wirjo.</li>
<li>Memory/dreaming (experimental): add weighted short-term recall promotion, a <code>/dreaming</code> command, Dreams UI, multilingual conceptual tagging, and doctor/status repair support, while refactoring dreaming from competing modes into three cooperative phases (light, deep, REM) with independent schedules and recovery behavior so durable memory promotion can run in the background with less manual setup. (#60569, #60697) Thanks @vignesh07.</li>
<li>Memory/dreaming: add configurable aging controls (<code>recencyHalfLifeDays</code>, <code>maxAgeDays</code>) plus optional verbose logging so operators can tune recall decay and inspect promotion decisions more easily.</li>
<li>Memory/dreaming: add REM preview tooling (<code>openclaw memory rem-harness</code>, <code>promote-explain</code>), surface possible lasting truths during REM staging, and make deep promotion replay-safe so reruns reconcile instead of duplicating <code>MEMORY.md</code> entries.</li>
<li>Memory/dreaming: write dreaming trail content to top-level <code>dreams.md</code> instead of daily memory notes, update <code>/dreaming</code> help text to point there, and keep <code>dreams.md</code> available for explicit reads without pulling it into default recall. Thanks @davemorin.</li>
<li>Memory/dreaming: add the Dream Diary surface in Dreams, simplify user-facing dreaming config to <code>enabled</code> plus optional <code>frequency</code>, treat phases as implementation detail in docs/UI, and keep the lobster animation visible above diary content. Thanks @vignesh07.</li>
<li>Prompt caching: keep prompt prefixes more reusable across transport fallback, deterministic MCP tool ordering, compaction, embedded image history, normalized system-prompt fingerprints, <code>openclaw status --verbose</code> cache diagnostics, and the removal of duplicate in-band tool inventories from agent system prompts so follow-up turns hit cache more reliably. (#58036, #58037, #58038, #59054, #60603, #60691) Thanks @bcherny and @vincentkoc.</li>
<li>Agents/cache: diagnostics: add prompt-cache break diagnostics, trace live cache scenarios through embedded runner paths, and show cache reuse explicitly in <code>openclaw status --verbose</code>. Thanks @vincentkoc.</li>
<li>Agents/cache: stabilize cache-relevant system prompt fingerprints by normalizing equivalent structured prompt whitespace, line endings, hook-added system context, and runtime capability ordering so semantically unchanged prompts reuse KV/cache more reliably. Thanks @vincentkoc.</li>
<li>Agents/tool prompts: remove the duplicate in-band tool inventory from agent system prompts so tool-calling models rely on the structured tool definitions as the single source of truth, improving prompt stability and reducing stale tool guidance.</li>
<li>Config/schema: enrich the exported <code>openclaw config schema</code> JSON Schema with field titles and descriptions so editors, agents, and other schema consumers receive the same config help metadata. (#60067) Thanks @solavrc.</li>
<li>Providers/CLI: remove bundled CLI text-provider backends and the <code>agents.defaults.cliBackends</code> surface, while keeping ACP harness sessions and Gemini media understanding on the native bundled providers.</li>
<li>Matrix/exec approvals: clarify unavailable-approval replies so Matrix no longer claims chat approvals are unsupported when native exec approvals are merely unconfigured. (#61424) Thanks @gumadeiras.</li>
<li>Docs/IRC: replace public IRC hostname examples with <code>irc.example.com</code> and recommend private servers for bot coordination while listing common public networks for intentional use.</li>
<li>Memory/dreaming: group nearby daily-note lines into short coherent chunks before staging them for dreaming, so one-off context from recent notes reaches REM/deep with better evidence and less line-level noise.</li>
<li>Memory/dreaming: drop generic date/day headings from daily-note chunk prefixes while keeping meaningful section labels, so staged snippets stay cleaner and more reusable. (#61597) Thanks @mbelinky.</li>
<li>Plugins/Lobster: run bundled Lobster workflows in process instead of spawning the external CLI, reducing transport overhead and unblocking native runtime integration. (#61523) Thanks @mbelinky.</li>
<li>Plugins/Lobster: harden managed resume validation so invalid TaskFlow resume calls fail earlier, and memoize embedded runtime loading per runner while keeping failed loads retryable. (#61566) Thanks @mbelinky.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Security: preserve restrictive plugin-only tool allowlists, require owner access for <code>/allowlist add</code> and <code>/allowlist remove</code>, fail closed when <code>before_tool_call</code> hooks crash, block browser SSRF redirect bypasses earlier, and keep non-interactive auth-choice inference scoped to bundled and already-trusted plugins. (#58476, #59836, #59822, #58771, #59120) Thanks @eleqtrizit and @pgondhi987.</li>
<li>Providers/OpenAI: make GPT-5 and Codex runs act sooner with lower-verbosity defaults, visible progress during tool work, and a one-shot retry when a turn only narrates the plan instead of taking action.</li>
<li>Providers/OpenAI and reply delivery: preserve native <code>reasoning.effort: "none"</code> and strict schemas where supported, add GPT-5.4 assistant <code>phase</code> metadata across replay and the Gateway <code>/v1/responses</code> layer, and keep commentary buffered until <code>final_answer</code> so web chat, session previews, embedded replies, and Telegram partials stop leaking planning text. Fixes #59150, #59643, #61282.</li>
<li>Telegram: fix current-model checks in the model picker, HTML-format non-default <code>/model</code> confirmations, explicit topic replies, persisted reaction ownership across restarts, caption-media placeholder and <code>file_id</code> preservation on download failure, and upgraded-install inbound image reads. (#60384, #60042, #59634, #59207, #59948, #59971) Thanks @sfuminya, @GitZhangChi, @dashhuang, @samzong, @v1p0r, and @neeravmakwana.</li>
<li>Telegram: restore DM voice-note preflight transcription so direct-message audio stops arriving as raw <code><media:audio></code> placeholders. (#61008) Thanks @manueltarouca.</li>
<li>Telegram/reasoning: only create a Telegram reasoning preview lane when the session is explicitly <code>reasoning:stream</code>, so hidden <code><think></code> traces from streamed replies stop surfacing as chat previews on normal sessions. Thanks @vincentkoc.</li>
<li>Telegram/native command menu: trim long menu descriptions before dropping commands so sub-100 command sets can still fit Telegram's payload budget and keep more <code>/</code> entries visible. (#61129) Thanks @neeravmakwana.</li>
<li>Discord: keep REST, webhook, and monitor traffic on the configured proxy, preserve component-only media sends, honor <code>@everyone</code> and <code>@here</code> mention gates, keep ACK reactions on the active account, and split voice connect/playback timeouts so auto-join is more reliable. (#57465, #60361, #60345) Thanks @geekhuashan.</li>
<li>Discord/reply tags: strip leaked <code>[[reply_to_current]]</code> control tags from preview text and honor explicit reply-tag threading during final delivery, so Discord replies stay attached to the triggering message instead of printing reply metadata into chat.</li>
<li>Discord/replies: replace the unshipped <code>replyToOnlyWhenBatched</code> flag with <code>replyToMode: "batched"</code> so native reply references only attach on debounced multi-message turns while explicit reply tags still work.</li>
<li>Discord/image generation: include the real generated <code>MEDIA:</code> paths in tool output, avoid duplicate plain-output media requeueing, and persist volatile workspace-generated media into durable outbound media before final reply delivery so generated image replies stop pointing at missing local files.</li>
<li>Slack: route live DM replies back to the concrete inbound DM channel while keeping persisted routing metadata user-scoped, so normal assistant replies stop disappearing when pairing and system messages still arrive. (#59030) Thanks @afurm.</li>
<li>WhatsApp: restore <code>channels.whatsapp.blockStreaming</code> and reset watchdog timeouts after reconnect so quiet chats stop falling into reconnect loops. (#60007, #60069) Thanks @MonkeyLeeT and @mcaxtr.</li>
<li>Android/Talk Mode: cancel in-flight <code>talk.speak</code> playback when speech is explicitly stopped, and restore spoken replies on both node-scoped and gateway-backed sessions by keeping reply routing and embedded transport overrides aligned with the current playback path. (#60306, #61164, #61214)</li>
<li>Voice-call/OpenAI: pass full plugin config into realtime transcription provider resolution so streaming calls can discover the bundled OpenAI realtime transcription provider again. Fixes #60936. Thanks @sliekens and @vincentkoc.</li>
<li>Matrix/exec approvals: anchor seeded approval reactions to the primary Matrix prompt event, resolve them from event metadata instead of prompt text, and clean up chunked approval prompts correctly. (#60931) Thanks @gumadeiras.</li>
<li>Matrix: recover more reliably when secret storage or recovery keys are missing by recreating secret storage during repair and backup reset, hold crypto snapshot locks during persistence, and surface explicit too-large attachment markers. (#59846, #59851, #60599, #60289) Thanks @al3mart, @emonty, and @efe-arv.</li>
<li>Matrix/DM sessions: add <code>channels.matrix.dm.sessionScope</code>, shared-session collision notices, and aligned outbound session reuse so separate Matrix DM rooms can keep distinct context when configured. (#61373) Thanks @gumadeiras.</li>
<li>Matrix: move legacy top-level <code>avatarUrl</code> into the default account during multi-account promotion and keep env-backed account setup avatar config persisted. (#61437) Thanks @gumadeiras.</li>
<li>MS Teams: download inline DM images via Graph API and preserve channel reply threading in proactive fallback. (#52212, #55198) Thanks @Ted-developer and @hyojin.</li>
<li>MS Teams: replace the deprecated Teams SDK HttpPlugin stub with <code>httpServerAdapter</code> so recurring gateway deprecation warnings stop firing and the Express 5 compatibility workaround stays on the supported SDK path. (#60939) Thanks @coolramukaka-sys.</li>
<li>Control UI/chat: add a per-session thinking-level picker in the chat header and mobile chat settings, and keep the browser bundle on UI-local thinking/session-key helpers so Safari no longer crashes on Node-only imports before rendering chat controls.</li>
<li>Sandbox/SSH: reject hardlinked files during cross-device rename fallback so EXDEV file copies preserve the same pinned file-boundary checks as direct reads.</li>
<li>Control UI: keep Stop visible during tool-only execution, preserve pending-send busy state, and clear stale ClawHub search results as soon as the query changes. (#54528, #59800, #60267) Thanks @chziyue and @frankekn.</li>
<li>Control UI/avatar: honor <code>ui.assistant.avatar</code> when serving <code>/avatar/:agentId</code> so Appearance UI avatar paths stop falling back to initials placeholders. (#60778) Thanks @hannasdev.</li>
<li>Control UI/cron: highlight the Cron refresh button while refresh is in flight so the page's loading state stays visible even when prior data remains on screen. (#60394) Thanks @coder-zhuzm.</li>
<li>Control UI/Overview: prevent gateway access token/password visibility toggle buttons from overlapping their inputs at narrow widths. (#56924) Thanks @bbddbb1.</li>
<li>Auto-reply: unify reply lifecycle ownership across preflight compaction, session rotation, CLI-backed runs, and gateway restart handling so <code>/stop</code> and same-session overlap checks target the right active turn and restart-interrupted turns return the restart notice instead of being silently dropped. (#61267) Thanks @dutifulbob.</li>
<li>Reply delivery: prevent duplicate block replies on <code>text_end</code> channels so providers that emit explicit text-end boundaries no longer double-send the same final message. (#61530)</li>
<li>Gateway/startup: default <code>gateway.mode</code> to <code>local</code> when unset, detect PID recycling in gateway lock files on Windows and macOS, and show startup progress so healthy restarts stop getting blocked by stale locks. (#54801, #60085, #59843) Thanks @BradGroux and @TonyDerek-dot.</li>
<li>Gateway/macOS: let launchd <code>KeepAlive</code> own in-process gateway restarts again, adding a short supervised-exit delay so rapid restarts avoid launchd crash-loop unloads while <code>openclaw gateway restart</code> still reports real LaunchAgent errors synchronously.</li>
<li>Gateway/macOS: re-bootstrap the LaunchAgent if <code>launchctl kickstart -k</code> unloads it during restart so failed restarts do not leave the gateway unmanaged until manual repair.</li>
<li>Gateway/macOS: recover installed-but-unloaded LaunchAgents during <code>openclaw gateway start</code> and <code>restart</code>, while still preferring live unmanaged gateways during restart recovery. (#43766) Thanks @HenryC-3.</li>
<li>Gateway/Windows scheduled tasks: preserve Task Scheduler settings on reinstall, fail loudly when <code>/Run</code> does not start, and report fast failed restarts accurately instead of pretending they timed out after 60 seconds. (#59335) Thanks @tmimmanuel.</li>
<li>Windows/restart: fall back to the installed Startup-entry launcher when the scheduled task was never registered, so <code>/restart</code> can relaunch the gateway on Windows setups where <code>schtasks</code> install fell back during onboarding. (#58943) Thanks @imechZhangLY.</li>
<li>Windows/restart: clean up stale gateway listeners before Windows self-restart and treat listener and argv probe failures as inconclusive, so scheduled-task relaunch no longer falls into an <code>EADDRINUSE</code> retry loop. (#60480) Thanks @arifahmedjoy.</li>
<li>Update/npm: prefer the npm binary that owns the installed global OpenClaw prefix so mixed Homebrew-plus-nvm setups update the right install. (#60153) Thanks @jayeshp19.</li>
<li>Agents/music and video generation: add <code>tools.media.asyncCompletion.directSend</code> as an opt-in direct-delivery path for finished async media tasks, while keeping the legacy requester-session wake/model-delivery flow as the default.</li>
<li>CLI/skills JSON: route <code>skills list --json</code>, <code>skills info --json</code>, and <code>skills check --json</code> output to stdout instead of stderr so machine-readable consumers receive JSON on the expected stream again. (#60914; fixes #57599; landed from contributor PR #57611 by @Aftabbs) Thanks @Aftabbs.</li>
<li>CLI/Commander: preserve Commander-computed exit codes for argument and help-error paths, and cover the user-argv parse mode in the regression tests so invalid CLI invocations no longer report success when exits are intercepted. (#60923) Thanks @Linux2010.</li>
<li>Cron: replay interrupted recurring jobs on the first gateway restart instead of waiting for a second restart. (#60583) Thanks @joelnishanth.</li>
<li>Cron: send failure notifications through the job's primary delivery channel using the same session context as successful delivery when no explicit <code>failureDestination</code> is configured. (#60622) Thanks @artwalker.</li>
<li>Exec/remote skills: stop advertising <code>exec host=node</code> when the current exec policy cannot route to a node, and clarify blocked exec-host override errors with both the requested host and allowed config path.</li>
<li>Agents/Claude CLI/security: clear inherited Claude Code config-root and plugin-root env overrides like <code>CLAUDE_CONFIG_DIR</code> and <code>CLAUDE_CODE_PLUGIN_*</code>, so OpenClaw-launched Claude CLI runs cannot be silently pointed at an alternate Claude config/plugin tree with different hooks, plugins, or auth context. Thanks @vincentkoc.</li>
<li>Agents/Claude CLI/security: clear inherited Claude Code provider-routing and managed-auth env overrides, and mark OpenClaw-launched Claude CLI runs as host-managed, so Claude CLI backdoor sessions cannot be silently redirected to proxy, Bedrock, Vertex, Foundry, or parent-managed token contexts. Thanks @vincentkoc.</li>
<li>Agents/Claude CLI/security: force host-managed Claude CLI backdoor runs to <code>--setting-sources user</code>, even under custom backend arg overrides, so repo-local <code>.claude</code> project/local settings, hooks, and plugin discovery do not silently execute inside non-interactive OpenClaw sessions. Thanks @vincentkoc.</li>
<li>Agents/Claude CLI: treat malformed bare <code>--permission-mode</code> backend overrides as missing and fail safe back to <code>bypassPermissions</code>, so custom <code>cliBackends.claude-cli.args</code> security config cannot accidentally consume the next flag as a bogus permission mode. Thanks @vincentkoc.</li>
<li>Gateway/device pairing: require non-admin paired-device sessions to manage only their own device for token rotate/revoke and paired-device removal, blocking cross-device token theft inside pairing-scoped sessions. (#50627) Thanks @coygeek.</li>
<li>Gateway/plugin routes: keep gateway-auth plugin runtime routes on write-only fallback scopes unless a trusted-proxy caller explicitly declares narrower <code>x-openclaw-scopes</code>, so plugin HTTP handlers no longer mint admin-level runtime scopes on missing or untrusted HTTP scope headers. (#59815) Thanks @pgondhi987.</li>
<li>Build/types: fix the Node <code>createRequire(...)</code> helper typing so provider-runtime lazy loads compile cleanly again and <code>pnpm build</code> no longer fails in the Pi embedded provider error-pattern path.</li>
<li>Gateway/security: scope loopback browser-origin auth throttling by normalized origin so one localhost Control UI tab cannot lock out a different localhost browser origin after repeated auth failures.</li>
<li>Gateway/auth: serialize async shared-secret auth attempts per client so concurrent Tailscale-capable failures cannot overrun the intended auth rate-limit budget. Thanks @Telecaster2147.</li>
<li>Device pairing/security: keep non-operator device scope checks bound to the requested role prefix so bootstrap verification cannot redeem <code>operator.*</code> scopes through <code>node</code> auth. (#57258) Thanks @jlapenna.</li>
<li>Device pairing: reject rotating device tokens into roles that were never approved during pairing, and keep reconnect role checks bounded to the paired device's approved role set. (#60462) Thanks @eleqtrizit.</li>
<li>Gateway/device auth: reuse cached device-token scopes only for cached-token reconnects, while keeping explicit <code>deviceToken</code> scope requests and empty-cache fallbacks intact so reconnects preserve <code>operator.read</code> without breaking explicit auth flows. (#46032) Thanks @caicongyang.</li>
<li>Mobile pairing/security: fail closed for internal <code>/pair</code> setup-code issuance, cleanup, and approval paths when gateway pairing scopes are missing, and keep approval-time requested-scope enforcement on the internal command path. (#55996) Thanks @coygeek.</li>
<li>Mobile pairing/bootstrap: keep QR bootstrap handoff tokens bounded to the mobile-safe contract so node handoff stays unscoped and operator handoff drops mixed <code>node.*</code>, <code>operator.admin</code>, and <code>operator.pairing</code> scopes.</li>
<li>Mobile pairing/Android: tighten secure endpoint handling so Tailscale and public remote setup reject cleartext endpoints, private LAN pairing still works, merged-role approvals mint both node and operator device tokens, and bootstrap tokens survive node auto-pair until operator approval finishes. (#60128, #60208, #60221) Thanks @obviyus.</li>
<li>Android/canvas security: require exact normalized A2UI URL matches before forwarding canvas bridge actions, rejecting query mismatches and descendant paths while still allowing fragment-only A2UI navigation.</li>
<li>Synology Chat/security: default low-level HTTPS helper TLS verification to on so helper/API defaults match the shipped safe account default, and only explicit <code>allowInsecureSsl: true</code> opts out.</li>
<li>Synology Chat/security: route webhook token comparison through the shared constant-time secret helper for consistency with other bundled plugins.</li>
<li>Plugins/marketplace: block remote marketplace symlink escapes without breaking ordinary local marketplace install paths. (#60556) Thanks @eleqtrizit.</li>
<li>Telegram/local Bot API: honor <code>channels.telegram.apiRoot</code> for buffered media downloads, add <code>channels.telegram.network.dangerouslyAllowPrivateNetwork</code> for trusted fake-IP setups, and require <code>channels.telegram.trustedLocalFileRoots</code> before reading absolute Bot API <code>file_path</code> values. (#59544, #60705) Thanks @SARAMALI15792 and @obviyus.</li>
<li>Outbound/sanitizer: strip leaked <code><tool_call></code>, <code><function_calls></code>, and model special tokens from shared user-visible assistant text, including truncated tool-call streams, so internal scaffolding no longer bleeds into replies across surfaces. (#60619) Thanks @oliviareid-svg.</li>
<li>Agents/errors: surface an explicit disk-full message when local session or transcript writes fail with <code>ENOSPC</code>/<code>disk full</code>, so those runs stop degrading into opaque <code>NO_REPLY</code>-style failures. Thanks @vincentkoc.</li>
<li>Exec approvals: remove heuristic command-obfuscation gating from host exec so gateway and node runs rely on explicit policy, allowlist, and strict inline-eval rules only.</li>
<li>Agents/tool results: cap live tool-result persistence and overflow-recovery truncation at 40k characters so oversized tool output stays bounded without discarding recent context entirely.</li>
<li>Discord/video replies: split text-plus-video deliveries into a text reply followed by a media-only send, and let live provider auth checks honor manifest-declared API key env vars like <code>MODELSTUDIO_API_KEY</code>.</li>
<li>Config/All Settings: keep the raw config view intact when sensitive fields are blank instead of corrupting or dropping the rendered snapshot. (#28214) Thanks @solodmd.</li>
<li>Plugin SDK/facades: back-fill bundled plugin facade sentinels before plugin-id tracking re-enters config loading, so CLI/provider startup no longer crashes with <code>shouldNormalizeGoogleProviderConfig is not a function</code> or other empty-facade reads during bundled plugin re-entry. Thanks @adam91holt.</li>
<li>Plugins/facades: back-fill facade sentinels before tracked-plugin resolution re-enters config loading, so facade exports stay defined during circular provider normalization. (#61180) Thanks @adam91holt.</li>
<li>QA lab: restore typed mock OpenAI gateway config wiring so QA-lab config helpers compile cleanly again and <code>pnpm check</code> / <code>pnpm build</code> stay green.</li>
<li>Discord/image generation: include the real generated <code>MEDIA:</code> paths in tool output and avoid duplicate plain-output media requeueing so Discord image replies stop pointing at missing local files.</li>
<li>Slack: route live DM replies back to the concrete inbound DM channel while keeping persisted routing metadata user-scoped, so normal assistant replies stop disappearing when pairing and system messages still arrive. (#59030) Thanks @afurm.</li>
<li>Discord/reply tags: strip leaked <code>[[reply_to_current]]</code> control tags from preview text and honor explicit reply-tag threading during final delivery, so Discord replies stay attached to the triggering message instead of printing reply metadata into chat.</li>
<li>Telegram: fix current-model checks in the model picker, HTML-format non-default <code>/model</code> confirmations, explicit topic replies, persisted reaction ownership across restarts, caption-media placeholder and <code>file_id</code> preservation on download failure, and upgraded-install inbound image reads. (#60384, #60042, #59634, #59207, #59948, #59971) Thanks @sfuminya, @GitZhangChi, @dashhuang, @samzong, @v1p0r, and @neeravmakwana.</li>
<li>Telegram: restore DM voice-note preflight transcription so direct-message audio stops arriving as raw <code><media:audio></code> placeholders. (#61008) Thanks @manueltarouca.</li>
<li>Telegram/reasoning: only create a Telegram reasoning preview lane when the session is explicitly <code>reasoning:stream</code>, so hidden <code><think></code> traces from streamed replies stop surfacing as chat previews on normal sessions. Thanks @vincentkoc.</li>
<li>Telegram/native command menu: trim long menu descriptions before dropping commands so sub-100 command sets can still fit Telegram's payload budget and keep more <code>/</code> entries visible. (#61129) Thanks @neeravmakwana.</li>
<li>Feishu/reasoning: only expose streamed reasoning previews when the session is explicitly <code>reasoning:stream</code>, so hidden reasoning traces do not surface on normal streaming sessions. Thanks @vincentkoc.</li>
<li>Discord: keep REST, webhook, and monitor traffic on the configured proxy, preserve component-only media sends, honor <code>@everyone</code> and <code>@here</code> mention gates, keep ACK reactions on the active account, and split voice connect/playback timeouts so auto-join is more reliable. (#57465, #60361, #60345) Thanks @geekhuashan.</li>
<li>WhatsApp: restore <code>channels.whatsapp.blockStreaming</code> and reset watchdog timeouts after reconnect so quiet chats stop falling into reconnect loops. (#60007, #60069) Thanks @MonkeyLeeT and @mcaxtr.</li>
<li>Memory: keep <code>memory-core</code> builtin embedding registration on the already-registered path so selecting <code>memory-core</code> no longer recurses through plugin discovery and crashes during startup. (#61402) Thanks @ngutman.</li>
<li>Agents/tool results: keep large <code>read</code> outputs visible longer, preserve the latest <code>read</code> output when older tool output can absorb the overflow budget, and fall back to Pi's normal overflow compaction/retry path before replacing a fresh <code>read</code> with a compacted stub. Thanks @vincentkoc.</li>
<li>Memory/QMD: prefer modern <code>qmd collection add --glob</code>, accept newer single-line JSON hit metadata while keeping legacy line fields, refresh QMD docs/doctor install guidance and model-override guidance, and keep older QMD releases working. Thanks @vincentkoc.</li>
<li>MS Teams: download inline DM images via Graph API and preserve channel reply threading in proactive fallback. (#52212, #55198) Thanks @Ted-developer and @hyojin.</li>
<li>MS Teams: replace the deprecated Teams SDK HttpPlugin stub with <code>httpServerAdapter</code> so recurring gateway deprecation warnings stop firing and the Express 5 compatibility workaround stays on the supported SDK path. (#60939) Thanks @coolramukaka-sys.</li>
<li>Matrix/exec approvals: anchor seeded approval reactions to the primary Matrix prompt event, resolve them from event metadata instead of prompt text, and clean up chunked approval prompts correctly. (#60931) Thanks @gumadeiras.</li>
<li>Matrix: recover more reliably when secret storage or recovery keys are missing by recreating secret storage during repair and backup reset, hold crypto snapshot locks during persistence, and surface explicit too-large attachment markers. (#59846, #59851, #60599, #60289) Thanks @al3mart, @emonty, and @efe-arv.</li>
<li>Android/Talk Mode: cancel in-flight <code>talk.speak</code> playback when speech is explicitly stopped, so stale replies stop starting after barge-in or manual stop. (#61164) Thanks @obviyus.</li>
<li>Android/Talk Mode: restore spoken assistant replies on node-scoped sessions by keeping reply routing synced to the resolved node session key and pausing mic capture during reply playback. (#60306) Thanks @MKV21.</li>
<li>Android/Talk Mode: restore voice replies on gateway-backed talk mode sessions by updating embedded runner transport overrides to the current agent transport API. (#61214) Thanks @obviyus.</li>
<li>Voice-call/OpenAI: pass full plugin config into realtime transcription provider resolution so streaming calls can discover the bundled OpenAI realtime transcription provider again. Fixes #60936. Thanks @sliekens and @vincentkoc.</li>
<li>Control UI/chat: add a per-session thinking-level picker in the chat header and mobile chat settings, and keep the browser bundle on UI-local thinking/session-key helpers so Safari no longer crashes on Node-only imports before rendering chat controls.</li>
<li>Control UI: keep Stop visible during tool-only execution, preserve pending-send busy state, and clear stale ClawHub search results as soon as the query changes. (#54528, #59800, #60267) Thanks @chziyue and @frankekn.</li>
<li>Control UI/avatar: honor <code>ui.assistant.avatar</code> when serving <code>/avatar/:agentId</code> so Appearance UI avatar paths stop falling back to initials placeholders. (#60778) Thanks @hannasdev.</li>
<li>Control UI/cron: highlight the Cron refresh button while refresh is in flight so the page's loading state stays visible even when prior data remains on screen. (#60394) Thanks @coder-zhuzm.</li>
<li>Control UI/Overview: prevent gateway access token/password visibility toggle buttons from overlapping their inputs at narrow widths. (#56924) Thanks @bbddbb1.</li>
<li>CLI/skills JSON: route <code>skills list --json</code>, <code>skills info --json</code>, and <code>skills check --json</code> output to stdout instead of stderr so machine-readable consumers receive JSON on the expected stream again. (#60914; fixes #57599; landed from contributor PR #57611 by @Aftabbs) Thanks @Aftabbs.</li>
<li>CLI/Commander: preserve Commander-computed exit codes for argument and help-error paths, and cover the user-argv parse mode in the regression tests so invalid CLI invocations no longer report success when exits are intercepted. (#60923) Thanks @Linux2010.</li>
<li>Cron: replay interrupted recurring jobs on the first gateway restart instead of waiting for a second restart. (#60583) Thanks @joelnishanth.</li>
<li>Cron: send failure notifications through the job's primary delivery channel using the same session context as successful delivery when no explicit <code>failureDestination</code> is configured. (#60622) Thanks @artwalker.</li>
<li>Live model switching: only treat explicit user-driven model changes as pending live switches, so fallback rotation, heartbeat overrides, and compaction no longer trip <code>LiveSessionModelSwitchError</code> before making an API call. (#60266) Thanks @kiranvk-2011.</li>
<li>Exec approvals: reuse durable exact-command <code>allow-always</code> approvals in allowlist mode so identical reruns stop prompting, and tighten Windows interpreter/path approval handling so wrapper and malformed-path cases fail closed more consistently. (#59880, #59780, #58040, #59182) Thanks @luoyanglang, @SnowSky1, and @pgondhi987.</li>
<li>Node exec approvals: keep node-host <code>system.run</code> approvals bound to the prepared execution plan across async forwarding, so mutable script operands still get approval-time binding and drift revalidation instead of dropping back to unbound execution.</li>
<li>Agents/exec approvals: let <code>exec-approvals.json</code> agent security override stricter gateway tool defaults so approved subagents can use <code>security: “full”</code> without falling back to allowlist enforcement again. (#60310) Thanks @lml2468.</li>
<li>Agents/exec: restore <code>host=node</code> routing for node-pinned and <code>host=auto</code> sessions, while still blocking sandboxed <code>auto</code> sessions from jumping to gateway. (#60788) Thanks @openperf.</li>
<li>Exec/heartbeat: use the canonical <code>exec-event</code> wake reason for <code>notifyOnExit</code> so background exec completions still trigger follow-up turns when <code>HEARTBEAT.md</code> is empty or comments-only. (#41479) Thanks @rstar327.</li>
<li>Heartbeat: skip wake delivery when the target session lane is already busy so the pending event is retried instead of getting drained too early. (#40526) Thanks @lucky7323.</li>
<li>Group chats/agent prompts: tell models to minimize empty lines and use normal chat-style spacing so group replies avoid document-style blank-line formatting.</li>
<li>Providers/OpenAI GPT: treat short approval turns like <code>ok do it</code> and <code>go ahead</code> as immediate action turns, and trim overly memo-like GPT-5 chat confirmations so OpenAI replies stay shorter and more conversational by default.</li>
<li>Providers/OpenAI Codex: split native <code>contextWindow</code> from runtime <code>contextTokens</code>, keep the default effective cap at <code>272000</code>, and expose a per-model <code>contextTokens</code> override on <code>models.providers.*.models[]</code>.</li>
<li>Providers/OpenAI-compatible WS: compute fallback token totals from normalized usage when providers omit or zero <code>total_tokens</code>, so DashScope-compatible sessions stop storing zero totals after alias normalization. (#54940) Thanks @lyfuci.</li>
<li>Agents/OpenAI: mark Claude-compatible file tool schemas as <code>additionalProperties: false</code> so direct OpenAI GPT-5 routes stop rejecting the <code>read</code> tool with invalid strict-schema errors.</li>
<li>Agents/OpenAI: fall back to <code>strict: false</code> for native OpenAI tool calls when a tool schema is not strict-compatible, and normalize empty-object tool schemas to include <code>required: []</code>, so direct GPT-5 routes stop failing with invalid strict-schema errors like missing <code>path</code> in <code>required</code>.</li>
<li>Agents/GPT: add explicit work-item lifecycle events for embedded runs, use them to surface real progress more reliably, and stop counting tool-started turns as planning-only retries.</li>
<li>Plugins/OpenAI: enable <code>gpt-image-1</code> reference-image edits through <code>/images/edits</code> multipart uploads, and stop inferring unsupported resolution overrides when no explicit <code>size</code> or <code>resolution</code> is provided.</li>
<li>Agents/replay: remove the malformed assistant-content canonicalization repair from replay history sanitization instead of extending that legacy repair path into replay validation.</li>
<li>Plugins/OpenAI: tune the OpenAI prompt overlay for live-chat cadence so GPT replies stay shorter, more human, and less wall-of-text by default.</li>
<li>Providers/compat: stop forcing OpenAI-only defaults on proxy and custom OpenAI-compatible routes, preserve native vendor-specific reasoning/tool/streaming behavior across Anthropic-compatible, Moonshot, Mistral, ModelStudio, OpenRouter, xAI, and Z.ai endpoints, and route GitHub Copilot Claude models through Anthropic Messages instead of OpenAI Responses.</li>
<li>Providers/GitHub Copilot: send IDE identity headers on runtime model requests and GitHub token exchange so IDE-authenticated Copilot runs stop failing with missing <code>Editor-Version</code>. (#60641) Thanks @VACInc and @vincentkoc.</li>
<li>Providers/OpenRouter failover: classify <code>403 “Key limit exceeded”</code> spending-limit responses as billing so model fallback continues instead of stopping on generic auth. (#59892) Thanks @rockcent.</li>
<li>Providers/Anthropic: keep <code>claude-cli/*</code> auth on live Claude CLI credentials at runtime, avoid persisting stale bearer-token profiles, and suppress macOS Keychain prompts during non-interactive Claude CLI setup. (#61234) Thanks @darkamenosa.</li>
<li>Providers/Anthropic: when Claude CLI auth becomes the default, write a real <code>claude-cli</code> auth profile so local and gateway agent runs can use Claude CLI immediately without missing-API-key failures. Thanks @vincentkoc.</li>
<li>Providers/Anthropic Vertex: honor <code>cacheRetention: “long”</code> with the real 1-hour prompt-cache TTL on Vertex AI endpoints, and default <code>anthropic-vertex</code> cache retention like direct Anthropic. (#60888) Thanks @affsantos.</li>
<li>Agents/Anthropic: preserve native <code>toolu_*</code> replay ids on direct Anthropic and Anthropic Vertex paths so cache-sensitive history stops rewriting known-valid Anthropic tool-use ids. (#52612)</li>
<li>Providers/Google: add model-level <code>cacheRetention</code> support for direct Gemini system prompts by creating, reusing, and refreshing <code>cachedContents</code> automatically on Google AI Studio runs. (#51372) Thanks @rafaelmariano-glitch.</li>
<li>Google Gemini CLI auth: detect bundled npm installs by scanning packaged bundle files for the Gemini OAuth client config, so <code>npm install -g @google/gemini-cli</code> layouts work again. (#60486) Thanks @wzfmini01.</li>
<li>Google Gemini CLI auth: detect personal OAuth mode from local Gemini settings and skip Code Assist project discovery for those logins, so personal Google accounts stop failing with <code>loadCodeAssist 400 Bad Request</code>. (#49226) Thanks @bobworrall.</li>
<li>Google Gemini CLI auth: improve OAuth credential discovery across Windows nvm and Homebrew libexec installs, and align Code Assist metadata so Gemini login stops failing on packaged CLI layouts. (#40729) Thanks @hughcube.</li>
<li>Google Gemini CLI models: add forward-compat support for stable <code>gemini-2.5-*</code> model ids by letting the bundled CLI provider clone them from Google templates, so <code>gemini-2.5-flash-lite</code> and related configured models stop showing up as missing. (#35274) Thanks @mySebbe.</li>
<li>Google image generation: disable pinned DNS for Gemini image requests and honor explicit <code>pinDns</code> overrides in shared provider HTTP helpers so proxy-backed image generation works again. (#59873) Thanks @luoyanglang.</li>
<li>Providers/Microsoft Foundry: preserve explicit image capability on normalized Foundry deployments, repair stale GPT/o-series text-only model metadata across gateway and runtime paths, and keep unknown fallback models from borrowing unrelated image support.</li>
<li>Providers/Model Studio: preserve native streaming usage reporting for DashScope-compatible endpoints even when they are configured under a generic provider key, so streamed token totals stop sticking at zero. (#52395) Thanks @IVY-AI-gif.</li>
<li>Providers/Z.AI: preserve explicitly registered <code>glm-5-*</code> variants like <code>glm-5-turbo</code> instead of intercepting them with the generic GLM-5 forward-compat shim. (#48185) Thanks @haoyu-haoyu.</li>
<li>Amazon Bedrock/aws-sdk auth: stop injecting the fake <code>AWS_PROFILE</code> apiKey marker when no AWS auth env vars exist, so instance-role and other default-chain setups keep working without poisoning provider config. (#61194) Thanks @wirjo.</li>
<li>Agents/Kimi tool-call repair: preserve tool arguments that were already present on streamed tool calls when later malformed deltas fail reevaluation, while still dropping stale repair-only state before <code>toolcall_end</code>.</li>
<li>Plugins/Kimi Coding: parse tagged tool calls and keep Anthropic-native tool payloads so Kimi coding endpoints execute tools instead of echoing raw markup. (#60051, #60391) Thanks @obviyus and @Eric-Guo.</li>
<li>Media understanding: auto-register image-capable config providers for vision routing, so custom GLM-style provider ids with image models stop failing with “no media-understanding provider registered”. (#51418) Thanks @xydt-610.</li>
<li>Plugins/media understanding: enable bundled Groq and Deepgram providers by default so configured transcription models work without extra plugin activation config. (#59982) Thanks @yxjsxy.</li>
<li>MiniMax/pricing: keep bundled MiniMax highspeed pricing distinct in provider catalogs and preserve the lower M2.5 cache-read pricing when onboarding older MiniMax models. (#54214) Thanks @octo-patch.</li>
<li>MiniMax: advertise image input on bundled <code>MiniMax-M2.7</code> and <code>MiniMax-M2.7-highspeed</code> model definitions so image-capable flows can route through the M2.7 family correctly. (#54843) Thanks @MerlinMiao88888888.</li>
<li>Models/MiniMax: honor <code>MINIMAX_API_HOST</code> for implicit bundled MiniMax provider catalogs so China-hosted API-key setups pick <code>api.minimaxi.com/anthropic</code> without manual provider config. (#34524) Thanks @caiqinghua.</li>
<li>Usage/MiniMax: invert remaining-style <code>usage_percent</code> fields when MiniMax reports only remaining percentage data, so usage bars stop showing nearly-full remaining quota as nearly-exhausted usage. (#60254) Thanks @jwchmodx.</li>
<li>Usage/MiniMax: let usage snapshots treat <code>minimax-portal</code> and MiniMax CN aliases as the same MiniMax quota surface, and prefer stored MiniMax OAuth before falling back to Coding Plan keys.</li>
<li>Usage/MiniMax: prefer the chat-model <code>model_remains</code> entry and derive Coding Plan window labels from MiniMax interval timestamps so MiniMax usage snapshots stop picking zero-budget media rows and misreporting 4h windows as <code>5h</code>. (#52349) Thanks @IVY-AI-gif.</li>
<li>Model picker/providers: treat bundled BytePlus and Volcengine plan aliases as their native providers during setup, and expose their bundled standard/coding catalogs before auth so setup can suggest the right models. (#58819) Thanks @Luckymingxuan.</li>
<li>Tools/web_search (Kimi): when <code>tools.web.search.kimi.baseUrl</code> is unset, inherit native Moonshot chat <code>baseUrl</code> (<code>.ai</code> / <code>.cn</code>) so China console keys authenticate on the same host as chat. Fixes #44851. (#56769) Thanks @tonga54.</li>
<li>Agents/Claude CLI: keep non-interactive <code>--permission-mode bypassPermissions</code> when custom <code>cliBackends.claude-cli.args</code> override defaults, including fallback resolution before the runtime plugin registry is active, so cron and heartbeat Claude CLI runs do not regress to interactive approval mode. (#61114) Thanks @cathrynlavery and @thewilloftheshadow.</li>
<li>Agents/Claude CLI: persist explicit <code>openclaw agent --session-id</code> runs under a stable session key so follow-ups can reuse the stored CLI binding and resume the same underlying Claude session.</li>
<li>Agents/Claude CLI: persist routed Claude session bindings, rotate them on <code>/new</code> and <code>/reset</code>, and keep live Claude CLI model switches moving across the configured Claude family so resumed sessions follow the real active thread and model. Thanks @vincentkoc.</li>
<li>Agents/CLI backends: invalidate stored CLI session reuse when local CLI login state or the selected auth profile credential changes, so relogin and token rotation stop resuming stale sessions.</li>
<li>Agents/Claude CLI/images: reuse stable hydrated image file paths and preserve shared media extensions like HEIC when passing image refs to local CLI runs, so Claude CLI image prompts stop thrashing KV cache prefixes and oddball image formats do not fall back to <code>.bin</code>. Thanks @vincentkoc.</li>
<li>Agents/compaction: keep assistant tool calls and displaced tool results in the same compaction chunk so strict summarization providers stop rejecting orphaned tool pairs. (#58849) Thanks @openperf.</li>
<li>Agents/failover: scope Anthropic <code>An unknown error occurred</code> failover matching by provider so generic internal unknown-error text no longer triggers retryable timeout fallback. (#59325) Thanks @aaron-he-zhu.</li>
<li>Agents/subagents: honor allowlist validation, auth-profile handoff, and session override state when a subagent retries after <code>LiveSessionModelSwitchError</code>. (#58178) Thanks @openperf.</li>
<li>Agents/runtime: make default subagent allowlists, inherited skills/workspaces, and duplicate session-id resolution behave more predictably, and include value-shape hints in missing-parameter tool errors. (#59944, #59992, #59858, #55317) Thanks @hclsys, @gumadeiras, @joelnishanth, and @priyansh19.</li>
<li>Agents/pairing: merge completion announce delivery context with the requester session fallback so missing <code>to</code> still reaches the original channel, and include <code>operator.talk.secrets</code> in CLI default operator scopes for node-role device pairing approvals. (#56481) Thanks @maxpetrusenko.</li>
<li>Agents/scheduling: steer background-now work toward automatic completion wake and treat <code>process</code> polling as on-demand inspection or intervention instead of default completion handling. (#60877) Thanks @vincentkoc.</li>
<li>Agents/skills: skip <code>.git</code> and <code>node_modules</code> when mirroring skills into sandbox workspaces so read-only sandboxes do not copy repo history or dependency trees. (#61090) Thanks @joelnishanth.</li>
<li>ACP/agents: inherit the target agent workspace for cross-agent ACP spawns and fall back safely when the inherited workspace no longer exists. (#58438) Thanks @zssggle-rgb.</li>
<li>ACPX/Windows: preserve backslashes and absolute <code>.exe</code> paths in Claude CLI parsing, and fail fast on wrapper-script targets with guidance to use <code>cmd.exe /c</code>, <code>powershell.exe -File</code>, or <code>node <script></code>. (#60689) Thanks @steipete.</li>
<li>Auth/failover: persist selected fallback overrides before retrying, shorten <code>auth_permanent</code> lockouts, and refresh websocket/shared-auth sessions only when real auth changes occur so retries and secret rotations behave predictably. (#60404, #60323, #60387) Thanks @extrasmall0 and @mappel-nv.</li>
<li>Gateway/channels: pin the initial startup channel registry before later plugin-registry churn so configured channels stay visible and <code>channels.status</code> stops falling back to empty <code>channelOrder</code> / <code>channels</code> payloads after runtime plugin loads.</li>
<li>Prompt caching: order stable workspace project-context files before <code>HEARTBEAT.md</code> and keep <code>HEARTBEAT.md</code> below the system-prompt cache boundary so heartbeat churn does not invalidate the stable project-context prefix. (#58979) Thanks @yozu and @vincentkoc.</li>
<li>Prompt caching: route Codex Responses and Anthropic Vertex through boundary-aware cache shaping, and report the actual outbound system prompt in cache traces so cache reuse and misses line up with what providers really receive. Thanks @vincentkoc.</li>
<li>Agents/cache: preserve the full 3-turn prompt-cache image window across tool loops, keep colliding bundled MCP tool definitions deterministic, and reapply Anthropic Vertex cache shaping after payload hook replacements so KV/cache reuse stays stable. Thanks @vincentkoc.</li>
<li>Status/cache: restore <code>cacheRead</code> and <code>cacheWrite</code> in transcript fallback so <code>/status</code> keeps showing cache hit percentages when session logs are the only complete usage source. (#59247) Thanks @stuartsy.</li>
<li>Status/usage: let <code>/status</code> and <code>session_status</code> fall back to transcript token totals when the session meta store stayed at zero, so LM Studio, Ollama, DashScope, and similar OpenAI-compatible providers stop showing <code>Context: 0/...</code>. (#55041) Thanks @jjjojoj.</li>
<li>Mattermost/config schema: accept <code>groups.*.requireMention</code> again so existing Mattermost configs no longer fail strict validation after upgrade. (#58271) Thanks @MoerAI.</li>
<li>Doctor/config: compare normalized <code>talk</code> configs by deep structural equality instead of key-order-sensitive serialization so <code>openclaw doctor --fix</code> stops repeatedly reporting/applying no-op <code>talk.provider/providers</code> normalization. (#59911) Thanks @ejames-dev.</li>
<li>Anthropic CLI onboarding: rewrite migrated fallback model refs during non-interactive Claude CLI setup too, so onboarding and scripted setup no longer keep stale <code>anthropic/*</code> fallbacks after switching the primary model to <code>claude-cli/*</code>. Thanks @vincentkoc.</li>
<li>Models/Anthropic CLI auth: replace migrated <code>agents.defaults.models</code> allowlists when <code>openclaw models auth login --provider anthropic --method cli --set-default</code> switches to <code>claude-cli/*</code>, so stale <code>anthropic/*</code> entries do not linger beside the migrated Claude CLI defaults. Thanks @vincentkoc.</li>
<li>Doctor/Claude CLI: add dedicated Claude CLI health checks so <code>openclaw doctor</code> can spot missing local installs or broken auth before agent runs fail. Thanks @vincentkoc.</li>
<li>Plugins/auth-choice: apply provider-owned auth config patches without recursively preserving replaced default-model maps, so Anthropic Claude CLI and similar migrations can intentionally swap model allowlists during onboarding and setup instead of accumulating stale entries. Thanks @vincentkoc.</li>
<li>Plugins/onboarding: write dotted plugin uiHint paths like Brave <code>webSearch.mode</code> as nested plugin config so <code>llm-context</code> setup stops failing validation. (#61159) Thanks @obviyus.</li>
<li>Plugins/install: preserve unsafe override flags across linked plugin and hook-pack probes so local <code>--link</code> installs honor the documented override behavior. (#60624) Thanks @JerrettDavis.</li>
<li>Plugins/cache: inherit the active gateway workspace for provider, web-search, and web-fetch snapshot loads when callers omit <code>workspaceDir</code>, so compatible plugin registries and snapshot caches stop missing on gateway-owned runtime paths. (#61138) Thanks @jzakirov.</li>
<li>Plugin SDK/context engines: export the missing context-engine result and subagent lifecycle types from <code>openclaw/plugin-sdk</code> so context engine plugins can type <code>ContextEngine</code> implementations without local workarounds. (#61251) Thanks @DaevMithran.</li>
<li>Tasks/maintenance: reconcile stale cron and chat-backed CLI task rows against live cron-job and agent-run ownership instead of treating any persisted session key as proof that the task is still running. (#60310) Thanks @lml2468.</li>
<li>Plugins: suppress trust-warning noise during non-activating snapshot and CLI metadata loads. (#61427) Thanks @gumadeiras.</li>
<li>Agents/video generation: accept <code>agents.defaults.videoGenerationModel</code> in strict config validation and <code>openclaw config set/get</code>, so gateways using <code>video_generate</code> no longer fail to boot after enabling a video model.</li>
<li>Matrix/streaming: add a quiet preview mode for streamed Matrix replies, keep legacy <code>partial</code> preview-first behavior, and finalize quiet media captions correctly so previews stop notifying early without dropping final text semantics. (#61450) Thanks @gumadeiras.</li>
<li>Gateway/shutdown: bound websocket-server shutdown even when no tracked clients remain, so gateway restarts stop hanging until the watchdog kills the process. (#61565) Thanks @mbelinky.</li>
<li>Control UI/multilingual: localize the remaining shared channel, instances, nodes, and gateway-confirmation strings so the dashboard stops mixing translated UI with hardcoded English labels. Thanks @vincentkoc.</li>
<li>Discord/media: raise the default inbound and outbound media cap to <code>100MB</code> so Discord matches Telegram more closely and larger attachments stop failing on the old low default.</li>
<li>Matrix: keep direct transport requests on the pinned dispatcher by routing them through undici runtime fetch, so Matrix clients resume syncing on newer runtimes without dropping the validated address binding. (#61595) Thanks @gumadeiras.</li>
<li>Plugins/facades: resolve globally installed bundled-plugin runtime facades from registry roots so bundled channels like LINE still boot when the winning plugin install lives under the global extensions directory with an encoded scoped folder name. (#61297) Thanks @openperf.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.5/OpenClaw-2026.4.5.zip" length="25050620" type="application/octet-stream" sparkle:edSignature="gVbB/73byllY0utwGIi3P5t0FyvLldeR0Uq2pAa6LTBr8VyZlwNCZ2xPlt2zDFshSUBFKxicYzohOmfJ28ACBg=="/>
</item>
<item>
<title>2026.4.2</title>
<pubDate>Thu, 02 Apr 2026 18:57:54 +0000</pubDate>
@@ -187,121 +435,5 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.1/OpenClaw-2026.4.1.zip" length="25841903" type="application/octet-stream" sparkle:edSignature="0TPiyshScmwDbgs626JU08NOUUFJmIsVFa5g0xmizfl64Fr+IoT4l/dkXarFqbZAJidtj5WN7Bff7fG8ye/7AA=="/>
</item>
<item>
<title>2026.3.31</title>
<pubDate>Tue, 31 Mar 2026 21:47:15 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026033190</sparkle:version>
<sparkle:shortVersionString>2026.3.31</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.3.31</h2>
<h3>Breaking</h3>
<ul>
<li>Nodes/exec: remove the duplicated <code>nodes.run</code> shell wrapper from the CLI and agent <code>nodes</code> tool so node shell execution always goes through <code>exec host=node</code>, keeping node-specific capabilities on <code>nodes invoke</code> and the dedicated media/location/notify actions.</li>
<li>Plugin SDK: deprecate the legacy provider compat subpaths plus the older bundled provider setup and channel-runtime compatibility shims, emit migration warnings, and keep the current documented <code>openclaw/plugin-sdk/*</code> entrypoints plus local <code>api.ts</code> / <code>runtime-api.ts</code> barrels as the forward path ahead of a future major-release removal.</li>
<li>Skills/install and Plugins/install: built-in dangerous-code <code>critical</code> findings and install-time scan failures now fail closed by default, so plugin installs and gateway-backed skill dependency installs that previously succeeded may now require an explicit dangerous override such as <code>--dangerously-force-unsafe-install</code> to proceed.</li>
<li>Gateway/auth: <code>trusted-proxy</code> now rejects mixed shared-token configs, and local-direct fallback requires the configured token instead of implicitly authenticating same-host callers. Thanks @zhangning-agent, @jacobtomlinson, and @vincentkoc.</li>
<li>Gateway/node commands: node commands now stay disabled until node pairing is approved, so device pairing alone is no longer enough to expose declared node commands. (#57777) Thanks @jacobtomlinson.</li>
<li>Gateway/node events: node-originated runs now stay on a reduced trusted surface, so notification-driven or node-triggered flows that previously relied on broader host/session tool access may need adjustment. (#57691) Thanks @jacobtomlinson.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>ACP/plugins: add an explicit default-off ACPX plugin-tools MCP bridge config, document the trust boundary, and harden the built-in bridge packaging/logging path so global installs and stdio MCP sessions work reliably. (#56867) Thanks @joe2643.</li>
<li>Agents/LLM: add a configurable idle-stream timeout for embedded runner requests so stalled model streams abort cleanly instead of hanging until the broader run timeout fires. (#55072) Thanks @liuy.</li>
<li>Agents/MCP: materialize bundle MCP tools with provider-safe names (<code>serverName__toolName</code>), support optional <code>streamable-http</code> transport selection plus per-server connection timeouts, and preserve real tool results from aborted/error turns unless truncation explicitly drops them. (#49505) Thanks @ziomancer.</li>
<li>Android/notifications: add notification-forwarding controls with package filtering, quiet hours, rate limiting, and safer picker behavior for forwarded notification events. (#40175) Thanks @nimbleenigma.</li>
<li>Background tasks: turn tasks into a real shared background-run control plane instead of ACP-only bookkeeping by unifying ACP, subagent, cron, and background CLI execution under one SQLite-backed ledger, routing detached lifecycle updates through the executor seam, adding audit/maintenance/status visibility, tightening auto-cleanup and lost-run recovery, improving task awareness in internal status/tool surfaces, and clarifying the split between heartbeat/main-session automation and detached scheduled runs. Thanks @mbelinky and @vincentkoc.</li>
<li>Background tasks: add the first linear task flow control surface with <code>openclaw flows list|show|cancel</code>, keep manual multi-task flows separate from one-task auto-sync flows, and surface doctor recovery hints for obviously orphaned or broken flow/task linkage. Thanks @mbelinky and @vincentkoc.</li>
<li>Channels/QQ Bot: add QQ Bot as a bundled channel plugin with multi-account setup, SecretRef-aware credentials, slash commands, reminders, and media send/receive support. (#52986) Thanks @sliverp.</li>
<li>Diffs: skip unused viewer-versus-file SSR preload work so <code>diffs</code> view-only and file-only runs do less render work while keeping mode outputs aligned. (#57909) thanks @gumadeiras.</li>
<li>Tasks: add a minimal SQLite-backed task flow registry plus task-to-flow linkage scaffolding, so orchestrated work can start gaining a first-class parent record without changing current task delivery behavior. Thanks @mbelinky and @vincentkoc.</li>
<li>Tasks: persist blocked state on one-task task flows and let the same flow reopen cleanly on retry, so blocked detached work can carry a parent-level reason and continue without fragmenting into a new job. Thanks @mbelinky and @vincentkoc.</li>
<li>Tasks: route one-task ACP and subagent updates through a parent task-flow owner context, so detached work can emerge back through the intended parent thread/session instead of speaking only as a raw child task. Thanks @mbelinky and @vincentkoc.</li>
<li>LINE/outbound media: add LINE image, video, and audio outbound sends on the LINE-specific delivery path, including explicit preview/tracking handling for videos while keeping generic media sends on the existing image-only route. (#45826) Thanks @masatohoshino.</li>
<li>Matrix/history: add optional room history context for Matrix group triggers via <code>channels.matrix.historyLimit</code>, with per-agent watermarks and retry-safe snapshots so failed trigger retries do not drift into newer room messages. (#57022) thanks @chain710.</li>
<li>Matrix/network: add explicit <code>channels.matrix.proxy</code> config for routing Matrix traffic through an HTTP(S) proxy, including account-level overrides and matching probe/runtime behavior. (#56931) thanks @patrick-yingxi-pan.</li>
<li>Matrix/streaming: add draft streaming so partial Matrix replies update the same message in place instead of sending a new message for each chunk. (#56387) Thanks @jrusz.</li>
<li>Matrix/threads: add per-DM <code>threadReplies</code> overrides and keep thread session isolation aligned with the effective room or DM thread policy from the triggering message onward. (#57995) thanks @teconomix.</li>
<li>MCP: add remote HTTP/SSE server support for <code>mcp.servers</code> URL configs, including auth headers and safer config redaction for MCP credentials. (#50396) Thanks @dhananjai1729.</li>
<li>Memory/QMD: add per-agent <code>memorySearch.qmd.extraCollections</code> so agents can opt into cross-agent session search without flattening every transcript collection into one shared QMD namespace. Thanks @vincentkoc.</li>
<li>Microsoft Teams/member info: add a Graph-backed member info action so Teams automations and tools can resolve channel member details directly from Microsoft Graph. (#57528) Thanks @sudie-codes.</li>
<li>Nostr/inbound DMs: verify inbound event signatures before pairing or sender-authorization side effects, so forged DM events no longer create pairing requests or trigger reply attempts. Thanks @smaeljaish771 and @vincentkoc.</li>
<li>OpenAI/Responses: forward configured <code>text.verbosity</code> across Responses HTTP and WebSocket transports, surface it in <code>/status</code>, and keep per-agent verbosity precedence aligned with runtime behavior. (#47106) Thanks @merc1305 and @vincentkoc.</li>
<li>Pi/Codex: add native Codex web search support for embedded Pi runs, including config/docs/wizard coverage and managed-tool suppression when native Codex search is active. (#46579) Thanks @Evizero.</li>
<li>Slack/exec approvals: add native Slack approval routing and approver authorization so exec approval prompts can stay in Slack instead of falling back to the Web UI or terminal. Thanks @vincentkoc.</li>
<li>TTS: Add structured provider diagnostics and fallback attempt analytics. (#57954) Thanks @joshavant.</li>
<li>WhatsApp/reactions: agents can now react with emoji on incoming WhatsApp messages, enabling more natural conversational interactions like acknowledging a photo with ❤️ instead of typing a reply. Thanks @mcaxtr.</li>
<li>Agents/BTW: force <code>/btw</code> side questions to disable provider reasoning so Anthropic adaptive-thinking sessions stop failing with <code>No BTW response generated</code>. Fixes #55376. Thanks @Catteres and @vincentkoc.</li>
<li>CLI/onboarding: reset the remote gateway URL prompt to the safe loopback default after declining a discovered endpoint, so onboarding does not keep a previously rejected remote URL. (#57828)</li>
<li>Agents/exec defaults: honor per-agent <code>tools.exec</code> defaults when no inline directive or session override is present, so configured exec host, security, ask, and node settings actually apply. (#57689)</li>
<li>Sandbox/networking: sanitize SSH subprocess env vars through the shared sandbox policy and route marketplace archive downloads plus Ollama discovery, auth, and pull requests through the guarded fetch path so sandboxed execution and remote fetches follow the repo's trust boundaries. (#57848, #57850)</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Slack: stop retry-driven duplicate replies when draft-finalization edits fail ambiguously, and log configured allowlisted users/channels by readable name instead of raw IDs.</li>
<li>Agents/OpenAI Responses: normalize raw bundled MCP tool schemas on the WebSocket/Responses path so bare-object, object-ish, and top-level union MCP tools no longer get rejected by OpenAI during tool registration. (#58299) Thanks @yelog.</li>
<li>ACP/security: replace ACP's dangerous-tool name override with semantic approval classes, so only narrow readonly reads/searches can auto-approve while indirect exec-capable and control-plane tools always require explicit prompt approval. Thanks @vincentkoc.</li>
<li>ACP/sessions_spawn: register ACP child runs for completion tracking and lifecycle cleanup, and make registration-failure cleanup explicitly best-effort so callers do not assume an already-started ACP turn was fully aborted. (#40885) Thanks @xaeon2026 and @vincentkoc.</li>
<li>ACP/tasks: mark cleanly exited ACP runs as blocked when they end on deterministic write or authorization blockers, and wake the parent session with a follow-up instead of falsely reporting success.</li>
<li>ACPX/runtime: derive the bundled ACPX expected version from the extension package metadata instead of hardcoding a separate literal, so plugin-local ACPX installs stop drifting out of health-check parity after version bumps. (#49089) Thanks @jiejiesks and @vincentkoc.</li>
<li>Agents/Anthropic failover: treat Anthropic <code>api_error</code> payloads with <code>An unexpected error occurred while processing the response</code> as transient so retry/fallback can engage instead of surfacing a terminal failure. (#57441) Thanks @zijiess and @vincentkoc.</li>
<li>Agents/compaction: keep late compaction-retry completions from double-resolving finished compaction futures, so interrupted or timed-out compactions stop surfacing spurious second-completion races. (#57796) Thanks @joshavant.</li>
<li>Agents/disabled providers: make disabled providers disappear from default model selection and embedded provider fallback, while letting explicitly pinned disabled providers fail with a clear config error instead of silently taking traffic. (#57735) Thanks @rileybrown-dev and @vincentkoc.</li>
<li>Agents/OAuth output: force exec-host OAuth output readers through the gateway fs policy so embedded gateway runs stop crashing when provider auth writes land outside the current sandbox workspace. (#58249) Thanks @joshavant.</li>
<li>Agents/system prompt: fix <code>agent.name</code> interpolation in the embedded runtime system prompt and make provider/model fallback text reflect the effective runtime selection after start. (#57625) Thanks @StllrSvr and @vincentkoc.</li>
<li>Android/device info: read the app's version metadata from the package manager instead of hidden APIs so Android 15+ onboarding and device info no longer fail to compile or report placeholder values. (#58126) Thanks @L3ER0Y.</li>
<li>Android/pairing: stop appending duplicate push receiver entries to <code>gateway-service.conf</code> on repeated QR pairing and keep push registration bounded to the current successful pairing, so Android push delivery stays healthy across re-pair and token rotation. (#58256) Thanks @surrealroad.</li>
<li>App install smoke: pin the latest-release lookup to <code>latest</code>, cache the first stable install version across the rerun, and relax prerelease package assertions so the Parallels smoke lane can validate stable-to-main upgrades even when <code>beta</code> moves ahead or the guest starts from an older stable. (#58177) Thanks @vincentkoc.</li>
<li>Auth/profiles: keep the last successful config load in memory for the running process and refresh that snapshot on successful writes/reloads, so hot paths stop reparsing <code>openclaw.json</code> between watcher-driven swaps.</li>
<li>Config/SecretRef + Control UI: harden SecretRef redaction round-trip restore, block unsafe raw fallback (force Form mode when raw is unavailable), and preflight submitted-config SecretRefs before config write RPC persistence. (#58044) Thanks @joshavant.</li>
<li>Config/Telegram: migrate removed <code>channels.telegram.groupMentionsOnly</code> into <code>channels.telegram.groups[\"*\"].requireMention</code> on load so legacy configs no longer crash at startup. (#55336) thanks @jameslcowan.</li>
<li>Config/update: stop <code>openclaw doctor</code> write-backs from persisting plugin-injected channel defaults, so <code>openclaw update</code> no longer seeds config keys that later break service refresh validation. (#56834) Thanks @openperf.</li>
<li>Control UI/agents: auto-load agent workspace files on initial Files panel open, and populate overview model/workspace/fallbacks from effective runtime agent metadata so defaulted models no longer show as <code>Not set</code>. (#56637) Thanks @dxsx84.</li>
<li>Control UI/slash commands: make <code>/steer</code> and <code>/redirect</code> work from the chat command palette with visible pending state for active-run <code>/steer</code>, correct redirected-run tracking, and a single canonical <code>/steer</code> entry in the command menu. (#54625) Thanks @fuller-stack-dev.</li>
<li>Cron/announce: preserve all deliverable text payloads for announce mode instead of collapsing to the last chunk, so multi-line cron reports deliver in full to Telegram forum topics.</li>
<li>Cron/isolated sessions: carry the full live-session provider, model, and auth-profile selection across retry restarts so cron jobs with model overrides no longer fail or loop on mid-run model-switch requests. (#57972) Thanks @issaba1.</li>
<li>Diffs/config: preserve schema-shaped plugin config parsing from <code>diffsPluginConfigSchema.safeParse()</code>, so direct callers keep <code>defaults</code> and <code>security</code> sections instead of receiving flattened tool defaults. (#57904) Thanks @gumadeiras.</li>
<li>Diffs: fall back to plain text when <code>lang</code> hints are invalid during diff render and viewer hydration, so bad or stale language values no longer break the diff viewer. (#57902) Thanks @gumadeiras.</li>
<li>Discord/voice: enforce the same guild channel and member allowlist checks on spoken voice ingress before transcription, so joined voice channels no longer accept speech from users outside the configured Discord access policy. Thanks @cyjhhh and @vincentkoc.</li>
<li>Docker/setup: force BuildKit for local image builds (including sandbox image builds) so <code>./docker-setup.sh</code> no longer fails on <code>RUN --mount=...</code> when hosts default to Docker's legacy builder. (#56681) Thanks @zhanghui-china.</li>
<li>Docs/anchors: fix broken English docs links and make Mint anchor audits run against the English-source docs tree. (#57039) thanks @velvet-shark.</li>
<li>Doctor/plugins: skip false Matrix legacy-helper warnings when no migration plans exist, and keep bundled <code>enabledByDefault</code> plugins in the gateway startup set. (#57931) Thanks @dinakars777.</li>
<li>Exec approvals/macOS: unwrap <code>arch</code> and <code>xcrun</code> before deriving shell payloads and allow-always patterns, so wrapper approvals stay bound to the carried command instead of the outer carrier. Thanks @tdjackey and @vincentkoc.</li>
<li>Exec approvals: unwrap <code>caffeinate</code> and <code>sandbox-exec</code> before persisting allow-always trust so later shell payload changes still require a fresh approval. Thanks @tdjackey and @vincentkoc.</li>
<li>Exec/approvals: infer Discord and Telegram exec approvers from existing owner config when <code>execApprovals.approvers</code> is unset, extend the default approval window to 30 minutes, and clarify approval-unavailable guidance so approvals do not appear to silently disappear.</li>
<li>Pi/TUI: flush message-boundary replies at <code>message_end</code> so turns stop looking stuck until the next nudge when the final reply was already ready. Thanks @vincentkoc.</li>
<li>Exec/approvals: keep <code>awk</code> and <code>sed</code> family binaries out of the low-risk <code>safeBins</code> fast path, and stop doctor profile scaffolding from treating them like ordinary custom filters. Thanks @vincentkoc.</li>
<li>Exec/env: block proxy, TLS, and Docker endpoint env overrides in host execution so request-scoped commands cannot silently reroute outbound traffic or trust attacker-supplied certificate settings. Thanks @AntAISecurityLab.</li>
<li>Exec/env: block Python package index override variables from request-scoped host exec environment sanitization so package fetches cannot be redirected through a caller-supplied index. Thanks @nexrin and @vincentkoc.</li>
<li>Exec/node: stop gateway-side workdir fallback from rewriting explicit <code>host=node</code> cwd values to the gateway filesystem, so remote node exec approval and runs keep using the intended node-local directory. (#50961) Thanks @openperf.</li>
<li>Exec/runtime: default implicit exec to <code>host=auto</code>, resolve that target to sandbox only when a sandbox runtime exists, keep explicit <code>host=sandbox</code> fail-closed without sandbox, and show <code>/exec</code> effective host state in runtime status/docs.</li>
<li>Exec: fail closed when the implicit sandbox host has no sandbox runtime, and stop denied async approval followups from reusing prior command output from the same session. (#56800) Thanks @scoootscooob.</li>
<li>Feishu/groups: keep quoted replies and topic bootstrap context aligned with group sender allowlists so only allowlisted thread messages seed agent context. Thanks @AntAISecurityLab and @vincentkoc.</li>
<li>Gateway/attachments: offload large inbound images without leaking <code>media://</code> markers into text-only runs, preserve mixed attachment order for model input/transcripts, and fail closed when model image capability cannot be resolved. (#55513) Thanks @Syysean.</li>
<li>Gateway/auth: keep shared-auth rate limiting active during WebSocket handshake attempts even when callers also send device-token candidates, so bogus device-token fields no longer suppress shared-secret brute-force tracking. Thanks @kexinoh and @vincentkoc.</li>
<li>Gateway/auth: reject mismatched browser <code>Origin</code> headers on trusted-proxy HTTP operator requests while keeping origin-less headless proxy clients working. Thanks @AntAISecurityLab and @vincentkoc.</li>
<li>Gateway/device tokens: disconnect active device sessions after token rotation so newly rotated credentials revoke existing live connections immediately instead of waiting for those sockets to close naturally. Thanks @zsxsoft and @vincentkoc.</li>
<li>Gateway/health: carry webhook-vs-polling account mode from channel descriptors into runtime snapshots so passive channels like LINE and BlueBubbles skip false stale-socket health failures. (#47488) Thanks @karesansui-u.</li>
<li>Gateway/pairing: restore QR bootstrap onboarding handoff so fresh <code>/pair qr</code> iPhone setup can auto-approve the initial node pairing, receive a reusable node device token, and stop retrying with spent bootstrap auth. (#58382) Thanks @ngutman.</li>
<li>Gateway/OpenAI compatibility: accept flat Responses API function tool definitions on <code>/v1/responses</code> and preserve <code>strict</code> when normalizing hosted tools into the embedded runner, so spec-compliant clients like Codex no longer fail validation or silently lose strict tool enforcement. Thanks @malaiwah and @vincentkoc.</li>
<li>Gateway/OpenAI HTTP: restore default operator scopes for bearer-authenticated requests that omit <code>x-openclaw-scopes</code>, so headless <code>/v1/chat/completions</code> and session-history callers work again after the recent method-scope hardening. (#57596) Thanks @openperf.</li>
<li>Gateway/plugins: scope plugin-auth HTTP route runtime clients to read-only access and keep gateway-authenticated plugin routes on write scope, so plugin-owned webhook handlers do not inherit write-capable runtime access by default. Thanks @davidluzsilva and @vincentkoc.</li>
<li>Gateway/SecretRef: resolve restart token drift checks with merged service/runtime env sources and hard-fail unsupported mutable SecretRef plus OAuth-profile combinations so restart warnings and policy enforcement match runtime behavior. (#58141) Thanks @joshavant.</li>
<li>Gateway/tools HTTP: tighten HTTP tool-invoke authorization so owner-only tools stay off HTTP invoke paths. (#57773) Thanks @jacobtomlinson.</li>
<li>Harden async approval followup delivery in webchat-only sessions (#57359) Thanks @joshavant.</li>
<li>Heartbeat/auth: prevent exec-event heartbeat runs from inheriting owner-only tool access from the session delivery target, so node exec output stays on the non-owner tool surface even when the target session belongs to the owner. Thanks @AntAISecurityLab and @vincentkoc.</li>
<li>Hooks/config: accept runtime channel plugin ids in <code>hooks.mappings[].channel</code> (for example <code>feishu</code>) instead of rejecting non-core channels during config validation. (#56226) Thanks @AiKrai001.</li>
<li>Hooks/session routing: rebind hook-triggered <code>agent:</code> session keys to the actual target agent before isolated dispatch so dedicated hook agents keep their own session-scoped tool and plugin identity. Thanks @kexinoh and @vincentkoc.</li>
<li>Host exec/env: block additional request-scoped env overrides that can redirect Docker endpoints, trust roots, compiler include paths, package resolution, or Python environment roots during approved host runs. Thanks @tdjackey and @vincentkoc.</li>
<li>Image generation/build: write stable runtime alias files into <code>dist/</code> and route provider-auth runtime lookups through those aliases so image-generation providers keep resolving auth/runtime modules after rebuilds instead of crashing on missing hashed chunk files.</li>
<li>iOS/Live Activities: mark the <code>ActivityKit</code> import in <code>LiveActivityManager.swift</code> as <code>@preconcurrency</code> so Xcode 26.4 / Swift 6 builds stop failing on strict concurrency checks. (#57180) Thanks @ngutman.</li>
<li>LINE/ACP: add current-conversation binding and inbound binding-routing parity so <code>/acp spawn ... --thread here</code>, configured ACP bindings, and active conversation-bound ACP sessions work on LINE like the other conversation channels.</li>
<li>LINE/markdown: preserve underscores inside Latin, Cyrillic, and CJK words when stripping markdown, while still removing standalone <code>_italic_</code> markers on the shared text-runtime path used by LINE and TTS. (#47465) Thanks @jackjin1997.</li>
<li>Agents/failover: make overloaded same-provider retry count and retry delay configurable via <code>auth.cooldowns</code>, default to one retry with no delay, and document the model-fallback behavior.</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.3.31/OpenClaw-2026.3.31.zip" length="25820093" type="application/octet-stream" sparkle:edSignature="NjpuH/j7OaNASEatBTpQ4uQy6+oUNq/lIwjrY69rJfkgGSk3/kU8vgxo9osjSgx034m7TpuZvWyulu57OBsQCg=="/>
</item>
</channel>
</rss>

View File

@@ -65,8 +65,8 @@ android {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
versionCode = 2026040301
versionName = "2026.4.3"
versionCode = 2026040601
versionName = "2026.4.6"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
@@ -239,44 +239,52 @@ tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
val stripReleaseDnsjavaServiceDescriptor =
tasks.register("stripReleaseDnsjavaServiceDescriptor") {
androidComponents {
onVariants(selector().withBuildType("release")) { variant ->
val variantName = variant.name
val variantNameCapitalized = variantName.replaceFirstChar(Char::titlecase)
val stripTaskName = "strip${variantNameCapitalized}DnsjavaServiceDescriptor"
val mergeTaskName = "merge${variantNameCapitalized}JavaResource"
val minifyTaskName = "minify${variantNameCapitalized}WithR8"
val mergedJar =
layout.buildDirectory.file(
"intermediates/merged_java_res/release/mergeReleaseJavaResource/base.jar",
"intermediates/merged_java_res/$variantName/$mergeTaskName/base.jar",
)
inputs.file(mergedJar)
outputs.file(mergedJar)
val stripTask =
tasks.register(stripTaskName) {
inputs.file(mergedJar)
outputs.file(mergedJar)
doLast {
val jarFile = mergedJar.get().asFile
if (!jarFile.exists()) {
return@doLast
doLast {
val jarFile = mergedJar.get().asFile
if (!jarFile.exists()) {
return@doLast
}
val unpackDir = temporaryDir.resolve("merged-java-res")
delete(unpackDir)
copy {
from(zipTree(jarFile))
into(unpackDir)
exclude(dnsjavaInetAddressResolverService)
}
delete(jarFile)
ant.invokeMethod(
"zip",
mapOf(
"destfile" to jarFile.absolutePath,
"basedir" to unpackDir.absolutePath,
),
)
}
}
val unpackDir = temporaryDir.resolve("merged-java-res")
delete(unpackDir)
copy {
from(zipTree(jarFile))
into(unpackDir)
exclude(dnsjavaInetAddressResolverService)
}
delete(jarFile)
ant.invokeMethod(
"zip",
mapOf(
"destfile" to jarFile.absolutePath,
"basedir" to unpackDir.absolutePath,
),
)
tasks.matching { it.name == mergeTaskName }.configureEach {
finalizedBy(stripTask)
}
tasks.matching { it.name == minifyTaskName }.configureEach {
dependsOn(stripTask)
}
}
tasks.matching { it.name == "stripReleaseDnsjavaServiceDescriptor" }.configureEach {
dependsOn("mergeReleaseJavaResource")
}
tasks.matching { it.name == "minifyReleaseWithR8" }.configureEach {
dependsOn(stripReleaseDnsjavaServiceDescriptor)
}

View File

@@ -71,6 +71,7 @@ class NodeRuntime(
private val identityStore = DeviceIdentityStore(appContext)
private var connectedEndpoint: GatewayEndpoint? = null
private var activeGatewayAuth: GatewayConnectAuth? = null
private val cameraHandler: CameraHandler = CameraHandler(
appContext = appContext,
@@ -299,6 +300,11 @@ class NodeRuntime(
_canvasRehydrateErrorText.value = null
updateStatus()
showLocalCanvasOnConnect()
val endpoint = connectedEndpoint
val auth = activeGatewayAuth
if (endpoint != null && auth != null) {
maybeStartOperatorSessionAfterNodeConnect(endpoint, auth)
}
},
onDisconnected = { message ->
_nodeConnected.value = false
@@ -345,6 +351,8 @@ class NodeRuntime(
session = operatorSession,
supportsChatSubscribe = false,
isConnected = { operatorConnected },
onBeforeSpeak = { micCapture.pauseForTts() },
onAfterSpeak = { micCapture.resumeAfterTts() },
).also { speaker ->
speaker.setPlaybackEnabled(prefs.speakerEnabled.value)
}
@@ -373,11 +381,10 @@ class NodeRuntime(
parseChatSendRunId(response) ?: idempotencyKey
},
speakAssistantReply = { text ->
// Skip if TalkModeManager is handling TTS (ttsOnAllResponses) to avoid
// double-speaking the same assistant reply from both pipelines.
if (!talkMode.ttsOnAllResponses) {
voiceReplySpeaker.speakAssistantReply(text)
}
// Voice-tab replies should speak through the dedicated reply speaker.
// Relying on talkMode.ttsOnAllResponses here can drop playback if the
// chat-event path misses the terminal event for this turn.
voiceReplySpeaker.speakAssistantReply(text)
},
)
}
@@ -416,14 +423,19 @@ class NodeRuntime(
session = operatorSession,
supportsChatSubscribe = true,
isConnected = { operatorConnected },
onBeforeSpeak = { micCapture.pauseForTts() },
onAfterSpeak = { micCapture.resumeAfterTts() },
)
}
private fun syncMainSessionKey(agentId: String?) {
val resolvedKey = resolveNodeMainSessionKey(agentId)
// Always push the resolved session key into TalkMode, even when the
// state flow value is unchanged, so lazy TalkMode instances do not
// stay on the default "main" session key.
talkMode.setMainSessionKey(resolvedKey)
if (_mainSessionKey.value == resolvedKey) return
_mainSessionKey.value = resolvedKey
talkMode.setMainSessionKey(resolvedKey)
chat.applyMainSessionKey(resolvedKey)
updateHomeCanvasState()
}
@@ -583,12 +595,11 @@ class NodeRuntime(
scope.launch {
prefs.talkEnabled.collect { enabled ->
// MicCaptureManager handles STT + send to gateway.
// TalkModeManager plays TTS on assistant responses.
// MicCaptureManager handles STT + send to gateway, while the dedicated
// reply speaker handles TTS for assistant replies in the voice tab.
micCapture.setMicEnabled(enabled)
if (enabled) {
// Mic on = user is on voice screen and wants TTS responses.
talkMode.ttsOnAllResponses = true
talkMode.ttsOnAllResponses = false
scope.launch { talkMode.ensureChatSubscribed() }
}
externalAudioCaptureActive.value = enabled
@@ -749,8 +760,8 @@ class NodeRuntime(
prefs.setTalkEnabled(value)
if (value) {
// Tapping mic on interrupts any active TTS (barge-in)
talkMode.stopTts()
talkMode.ttsOnAllResponses = true
stopVoicePlayback()
talkMode.ttsOnAllResponses = false
scope.launch { talkMode.ensureChatSubscribed() }
}
micCapture.setMicEnabled(value)
@@ -765,18 +776,25 @@ class NodeRuntime(
if (voiceReplySpeakerLazy.isInitialized()) {
voiceReplySpeaker.setPlaybackEnabled(value)
}
// Keep TalkMode in sync so speaker mute works when ttsOnAllResponses is active.
// Keep TalkMode in sync so any active Talk playback also respects speaker mute.
talkMode.setPlaybackEnabled(value)
}
private fun stopActiveVoiceSession() {
talkMode.ttsOnAllResponses = false
talkMode.stopTts()
stopVoicePlayback()
micCapture.setMicEnabled(false)
prefs.setTalkEnabled(false)
externalAudioCaptureActive.value = false
}
private fun stopVoicePlayback() {
talkMode.stopTts()
if (voiceReplySpeakerLazy.isInitialized()) {
voiceReplySpeaker.stopTts()
}
}
fun refreshGatewayConnection() {
val endpoint =
connectedEndpoint ?: run {
@@ -793,15 +811,14 @@ class NodeRuntime(
auth: GatewayConnectAuth,
reconnect: Boolean = false,
) {
activeGatewayAuth = auth
val tls = connectionManager.resolveTlsParams(endpoint)
val connectOperator =
shouldConnectOperatorSession(
auth.token,
auth.bootstrapToken,
auth.password,
loadStoredRoleDeviceToken("operator"),
val operatorAuth =
resolveOperatorSessionConnectAuth(
auth = auth,
storedOperatorToken = loadStoredRoleDeviceToken("operator"),
)
if (!connectOperator) {
if (operatorAuth == null) {
operatorConnected = false
operatorStatusText = "Offline"
operatorSession.disconnect()
@@ -809,9 +826,9 @@ class NodeRuntime(
} else {
operatorSession.connect(
endpoint,
auth.token,
auth.bootstrapToken,
auth.password,
operatorAuth.token,
operatorAuth.bootstrapToken,
operatorAuth.password,
connectionManager.buildOperatorConnectOptions(),
tls,
)
@@ -824,7 +841,7 @@ class NodeRuntime(
connectionManager.buildNodeConnectOptions(),
tls,
)
if (reconnect && connectOperator) {
if (reconnect && operatorAuth != null) {
operatorSession.reconnect()
}
if (reconnect) {
@@ -922,8 +939,33 @@ class NodeRuntime(
return deviceAuthStore.loadToken(deviceId, role)
}
private fun maybeStartOperatorSessionAfterNodeConnect(
endpoint: GatewayEndpoint,
auth: GatewayConnectAuth,
) {
if (operatorConnected || operatorStatusText == "Connecting…") {
return
}
val operatorAuth =
resolveOperatorSessionConnectAuth(
auth = auth,
storedOperatorToken = loadStoredRoleDeviceToken("operator"),
) ?: return
operatorStatusText = "Connecting…"
updateStatus()
operatorSession.connect(
endpoint,
operatorAuth.token,
operatorAuth.bootstrapToken,
operatorAuth.password,
connectionManager.buildOperatorConnectOptions(),
connectionManager.resolveTlsParams(endpoint),
)
}
fun disconnect() {
connectedEndpoint = null
activeGatewayAuth = null
_pendingGatewayTrust.value = null
operatorSession.disconnect()
nodeSession.disconnect()
@@ -1259,18 +1301,47 @@ class NodeRuntime(
}
internal fun resolveOperatorSessionConnectAuth(
auth: NodeRuntime.GatewayConnectAuth,
storedOperatorToken: String?,
): NodeRuntime.GatewayConnectAuth? {
val explicitToken = auth.token?.trim()?.takeIf { it.isNotEmpty() }
if (explicitToken != null) {
return NodeRuntime.GatewayConnectAuth(
token = explicitToken,
bootstrapToken = null,
password = null,
)
}
val explicitPassword = auth.password?.trim()?.takeIf { it.isNotEmpty() }
if (explicitPassword != null) {
return NodeRuntime.GatewayConnectAuth(
token = null,
bootstrapToken = null,
password = explicitPassword,
)
}
val storedToken = storedOperatorToken?.trim()?.takeIf { it.isNotEmpty() }
if (storedToken != null) {
// Bootstrap can seed the operator token, but operator should reconnect
// through the stored device-token path rather than bootstrap auth itself.
return NodeRuntime.GatewayConnectAuth(
token = null,
bootstrapToken = null,
password = null,
)
}
return null
}
internal fun shouldConnectOperatorSession(
token: String?,
bootstrapToken: String?,
password: String?,
auth: NodeRuntime.GatewayConnectAuth,
storedOperatorToken: String?,
): Boolean {
return (
!token.isNullOrBlank() ||
!bootstrapToken.isNullOrBlank() ||
!password.isNullOrBlank() ||
!storedOperatorToken.isNullOrBlank()
)
return resolveOperatorSessionConnectAuth(auth, storedOperatorToken) != null
}
private enum class HomeCanvasGatewayState {

View File

@@ -1,32 +1,92 @@
package ai.openclaw.app.gateway
import ai.openclaw.app.SecurePrefs
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
data class DeviceAuthEntry(
val token: String,
val role: String,
val scopes: List<String>,
val updatedAtMs: Long,
)
@Serializable
private data class PersistedDeviceAuthMetadata(
val scopes: List<String> = emptyList(),
val updatedAtMs: Long = 0L,
)
interface DeviceAuthTokenStore {
fun loadToken(deviceId: String, role: String): String?
fun saveToken(deviceId: String, role: String, token: String)
fun loadEntry(deviceId: String, role: String): DeviceAuthEntry?
fun loadToken(deviceId: String, role: String): String? = loadEntry(deviceId, role)?.token
fun saveToken(deviceId: String, role: String, token: String, scopes: List<String> = emptyList())
fun clearToken(deviceId: String, role: String)
}
class DeviceAuthStore(private val prefs: SecurePrefs) : DeviceAuthTokenStore {
override fun loadToken(deviceId: String, role: String): String? {
private val json = Json { ignoreUnknownKeys = true }
override fun loadEntry(deviceId: String, role: String): DeviceAuthEntry? {
val key = tokenKey(deviceId, role)
return prefs.getString(key)?.trim()?.takeIf { it.isNotEmpty() }
val token = prefs.getString(key)?.trim()?.takeIf { it.isNotEmpty() } ?: return null
val normalizedRole = normalizeRole(role)
val metadata =
prefs.getString(metadataKey(deviceId, role))
?.let { raw ->
runCatching { json.decodeFromString<PersistedDeviceAuthMetadata>(raw) }.getOrNull()
}
return DeviceAuthEntry(
token = token,
role = normalizedRole,
scopes = metadata?.scopes ?: emptyList(),
updatedAtMs = metadata?.updatedAtMs ?: 0L,
)
}
override fun saveToken(deviceId: String, role: String, token: String) {
override fun saveToken(deviceId: String, role: String, token: String, scopes: List<String>) {
val normalizedScopes = normalizeScopes(scopes)
val key = tokenKey(deviceId, role)
prefs.putString(key, token.trim())
prefs.putString(
metadataKey(deviceId, role),
json.encodeToString(
PersistedDeviceAuthMetadata(
scopes = normalizedScopes,
updatedAtMs = System.currentTimeMillis(),
),
),
)
}
override fun clearToken(deviceId: String, role: String) {
val key = tokenKey(deviceId, role)
prefs.remove(key)
prefs.remove(metadataKey(deviceId, role))
}
private fun tokenKey(deviceId: String, role: String): String {
val normalizedDevice = deviceId.trim().lowercase()
val normalizedRole = role.trim().lowercase()
val normalizedDevice = normalizeDeviceId(deviceId)
val normalizedRole = normalizeRole(role)
return "gateway.deviceToken.$normalizedDevice.$normalizedRole"
}
private fun metadataKey(deviceId: String, role: String): String {
val normalizedDevice = normalizeDeviceId(deviceId)
val normalizedRole = normalizeRole(role)
return "gateway.deviceTokenMeta.$normalizedDevice.$normalizedRole"
}
private fun normalizeDeviceId(deviceId: String): String = deviceId.trim().lowercase()
private fun normalizeRole(role: String): String = role.trim().lowercase()
private fun normalizeScopes(scopes: List<String>): List<String> {
return scopes
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
.sorted()
}
}

View File

@@ -64,6 +64,7 @@ data class GatewayConnectErrorDetails(
val code: String?,
val canRetryWithDeviceToken: Boolean,
val recommendedNextStep: String?,
val reason: String? = null,
)
private data class SelectedConnectAuth(
@@ -116,6 +117,8 @@ class GatewaySession(
val details: GatewayConnectErrorDetails? = null,
)
data class RpcResult(val ok: Boolean, val payloadJson: String?, val error: ErrorShape?)
private val json = Json { ignoreUnknownKeys = true }
private val writeLock = Mutex()
private val pending = ConcurrentHashMap<String, CompletableDeferred<RpcResponse>>()
@@ -196,6 +199,13 @@ class GatewaySession(
}
suspend fun request(method: String, paramsJson: String?, timeoutMs: Long = 15_000): String {
val res = requestDetailed(method = method, paramsJson = paramsJson, timeoutMs = timeoutMs)
if (res.ok) return res.payloadJson ?: ""
val err = res.error
throw IllegalStateException("${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}")
}
suspend fun requestDetailed(method: String, paramsJson: String?, timeoutMs: Long = 15_000): RpcResult {
val conn = currentConnection ?: throw IllegalStateException("not connected")
val params =
if (paramsJson.isNullOrBlank()) {
@@ -204,9 +214,7 @@ class GatewaySession(
json.parseToJsonElement(paramsJson)
}
val res = conn.request(method, params, timeoutMs)
if (res.ok) return res.payloadJson ?: ""
val err = res.error
throw IllegalStateException("${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}")
return RpcResult(ok = res.ok, payloadJson = res.payloadJson, error = res.error)
}
suspend fun refreshNodeCanvasCapability(timeoutMs: Long = 8_000): Boolean {
@@ -418,11 +426,63 @@ class GatewaySession(
}
throw GatewayConnectFailure(error)
}
handleConnectSuccess(res, identity.deviceId)
handleConnectSuccess(res, identity.deviceId, selectedAuth.authSource)
connectDeferred.complete(Unit)
}
private fun handleConnectSuccess(res: RpcResponse, deviceId: String) {
private fun shouldPersistBootstrapHandoffTokens(authSource: GatewayConnectAuthSource): Boolean {
if (authSource != GatewayConnectAuthSource.BOOTSTRAP_TOKEN) return false
if (isLoopbackGatewayHost(endpoint.host)) return true
return tls != null
}
private fun filteredBootstrapHandoffScopes(role: String, scopes: List<String>): List<String>? {
return when (role.trim()) {
"node" -> emptyList()
"operator" -> {
val allowedOperatorScopes =
setOf(
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
)
scopes.filter { allowedOperatorScopes.contains(it) }.distinct().sorted()
}
else -> null
}
}
private fun persistBootstrapHandoffToken(
deviceId: String,
role: String,
token: String,
scopes: List<String>,
) {
val filteredScopes = filteredBootstrapHandoffScopes(role, scopes) ?: return
deviceAuthStore.saveToken(deviceId, role, token, filteredScopes)
}
private fun persistIssuedDeviceToken(
authSource: GatewayConnectAuthSource,
deviceId: String,
role: String,
token: String,
scopes: List<String>,
) {
if (authSource == GatewayConnectAuthSource.BOOTSTRAP_TOKEN) {
if (!shouldPersistBootstrapHandoffTokens(authSource)) return
persistBootstrapHandoffToken(deviceId, role, token, scopes)
return
}
deviceAuthStore.saveToken(deviceId, role, token, scopes)
}
private fun handleConnectSuccess(
res: RpcResponse,
deviceId: String,
authSource: GatewayConnectAuthSource,
) {
val payloadJson = res.payloadJson ?: throw IllegalStateException("connect failed: missing payload")
val obj = json.parseToJsonElement(payloadJson).asObjectOrNull() ?: throw IllegalStateException("connect failed")
pendingDeviceTokenRetry = false
@@ -432,8 +492,27 @@ class GatewaySession(
val authObj = obj["auth"].asObjectOrNull()
val deviceToken = authObj?.get("deviceToken").asStringOrNull()
val authRole = authObj?.get("role").asStringOrNull() ?: options.role
val authScopes =
authObj?.get("scopes").asArrayOrNull()
?.mapNotNull { it.asStringOrNull() }
?: emptyList()
if (!deviceToken.isNullOrBlank()) {
deviceAuthStore.saveToken(deviceId, authRole, deviceToken)
persistIssuedDeviceToken(authSource, deviceId, authRole, deviceToken, authScopes)
}
if (shouldPersistBootstrapHandoffTokens(authSource)) {
authObj?.get("deviceTokens").asArrayOrNull()
?.mapNotNull { it.asObjectOrNull() }
?.forEach { tokenEntry ->
val handoffToken = tokenEntry["deviceToken"].asStringOrNull()
val handoffRole = tokenEntry["role"].asStringOrNull()
val handoffScopes =
tokenEntry["scopes"].asArrayOrNull()
?.mapNotNull { it.asStringOrNull() }
?: emptyList()
if (!handoffToken.isNullOrBlank() && !handoffRole.isNullOrBlank()) {
persistBootstrapHandoffToken(deviceId, handoffRole, handoffToken, handoffScopes)
}
}
}
val rawCanvas = obj["canvasHostUrl"].asStringOrNull()
canvasHostUrl = normalizeCanvasHostUrl(rawCanvas, endpoint, isTlsConnection = tls != null)
@@ -560,6 +639,7 @@ class GatewaySession(
code = it["code"].asStringOrNull(),
canRetryWithDeviceToken = it["canRetryWithDeviceToken"].asBooleanOrNull() == true,
recommendedNextStep = it["recommendedNextStep"].asStringOrNull(),
reason = it["reason"].asStringOrNull(),
)
}
ErrorShape(code, msg, details)
@@ -899,6 +979,8 @@ private fun formatGatewayAuthorityHost(host: String): String {
private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject
private fun JsonElement?.asArrayOrNull(): JsonArray? = this as? JsonArray
private fun JsonElement?.asStringOrNull(): String? =
when (this) {
is JsonNull -> null

View File

@@ -14,30 +14,31 @@ object CanvasActionTrust {
if (candidateUri.scheme.equals("file", ignoreCase = true)) {
return false
}
val normalizedCandidate = normalizeTrustedRemoteA2uiUri(candidateUri) ?: return false
return trustedA2uiUrls.any { trusted ->
isTrustedA2uiPage(candidateUri, trusted)
matchesTrustedRemoteA2uiUrlExact(normalizedCandidate, trusted)
}
}
private fun isTrustedA2uiPage(candidateUri: URI, trustedUrl: String): Boolean {
private fun matchesTrustedRemoteA2uiUrlExact(candidateUri: URI, trustedUrl: String): Boolean {
val trustedUri = parseUri(trustedUrl) ?: return false
if (!candidateUri.scheme.equals(trustedUri.scheme, ignoreCase = true)) return false
if (candidateUri.host?.equals(trustedUri.host, ignoreCase = true) != true) return false
if (effectivePort(candidateUri) != effectivePort(trustedUri)) return false
val trustedPath = trustedUri.rawPath?.takeIf { it.isNotBlank() } ?: return false
val candidatePath = candidateUri.rawPath?.takeIf { it.isNotBlank() } ?: return false
val trustedPrefix = if (trustedPath.endsWith("/")) trustedPath else "$trustedPath/"
return candidatePath == trustedPath || candidatePath.startsWith(trustedPrefix)
val normalizedTrusted = normalizeTrustedRemoteA2uiUri(trustedUri) ?: return false
return candidateUri == normalizedTrusted
}
private fun effectivePort(uri: URI): Int {
if (uri.port >= 0) return uri.port
return when (uri.scheme?.lowercase()) {
"https" -> 443
"http" -> 80
else -> -1
private fun normalizeTrustedRemoteA2uiUri(uri: URI): URI? {
// Keep Android trust normalization aligned with iOS ScreenController:
// exact remote URL match, scheme/host normalized, fragment ignored.
val scheme = uri.scheme?.lowercase() ?: return null
if (scheme != "http" && scheme != "https") return null
val host = uri.host?.trim()?.takeIf { it.isNotEmpty() }?.lowercase() ?: return null
return try {
URI(scheme, uri.userInfo, host, uri.port, uri.rawPath, uri.rawQuery, null)
} catch (_: Throwable) {
null
}
}

View File

@@ -7,7 +7,7 @@ import ai.openclaw.app.gateway.GatewayClientInfo
import ai.openclaw.app.gateway.GatewayConnectOptions
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.GatewayTlsParams
import ai.openclaw.app.gateway.isLoopbackGatewayHost
import ai.openclaw.app.gateway.isPrivateLanGatewayHost
import ai.openclaw.app.LocationMode
import ai.openclaw.app.VoiceWakeMode
@@ -34,7 +34,7 @@ class ConnectionManager(
val stableId = endpoint.stableId
val stored = storedFingerprint?.trim().takeIf { !it.isNullOrEmpty() }
val isManual = stableId.startsWith("manual|")
val cleartextAllowedHost = isLoopbackGatewayHost(endpoint.host)
val cleartextAllowedHost = isPrivateLanGatewayHost(endpoint.host)
if (isManual) {
if (!manualTlsEnabled && cleartextAllowedHost) return null

View File

@@ -163,7 +163,7 @@ private fun disableForceDarkIfSupported(settings: WebSettings) {
WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_OFF)
}
private class CanvasA2UIActionBridge(
internal class CanvasA2UIActionBridge(
private val isTrustedPage: () -> Boolean,
private val onMessage: (String) -> Unit,
) {

View File

@@ -1,6 +1,6 @@
package ai.openclaw.app.ui
import ai.openclaw.app.gateway.isLoopbackGatewayHost
import ai.openclaw.app.gateway.isPrivateLanGatewayHost
import java.util.Base64
import java.util.Locale
import java.net.URI
@@ -56,7 +56,7 @@ internal data class GatewayScannedSetupCodeResult(
private val gatewaySetupJson = Json { ignoreUnknownKeys = true }
private const val remoteGatewaySecurityRule =
"Non-loopback mobile nodes require wss:// or Tailscale Serve. ws:// is allowed only for localhost and the Android emulator."
"Tailscale and public mobile nodes require wss:// or Tailscale Serve. ws:// is allowed for private LAN, localhost, and the Android emulator."
private const val remoteGatewaySecurityFix =
"Use a private LAN host/address, or enable Tailscale Serve / expose a wss:// gateway URL."
@@ -143,7 +143,7 @@ internal fun parseGatewayEndpoint(rawInput: String): GatewayEndpointConfig? {
"wss", "https" -> true
else -> true
}
if (!tls && !isLoopbackGatewayHost(host)) {
if (!tls && !isPrivateLanGatewayHost(host)) {
return GatewayEndpointParseResult(error = GatewayEndpointValidationError.INSECURE_REMOTE_URL)
}
val defaultPort =

View File

@@ -14,11 +14,13 @@ import android.speech.SpeechRecognizer
import androidx.core.content.ContextCompat
import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
@@ -88,6 +90,7 @@ class MicCaptureManager(
val isSending: StateFlow<Boolean> = _isSending
private val messageQueue = ArrayDeque<String>()
private val messageQueueLock = Any()
private var flushedPartialTranscript: String? = null
private var pendingRunId: String? = null
private var pendingAssistantEntryId: String? = null
@@ -99,11 +102,63 @@ class MicCaptureManager(
private var transcriptFlushJob: Job? = null
private var pendingRunTimeoutJob: Job? = null
private var stopRequested = false
private val ttsPauseLock = Any()
private var ttsPauseDepth = 0
private var resumeMicAfterTts = false
private fun enqueueMessage(message: String) {
synchronized(messageQueueLock) {
messageQueue.addLast(message)
}
}
private fun snapshotMessageQueue(): List<String> {
return synchronized(messageQueueLock) {
messageQueue.toList()
}
}
private fun hasQueuedMessages(): Boolean {
return synchronized(messageQueueLock) {
messageQueue.isNotEmpty()
}
}
private fun firstQueuedMessage(): String? {
return synchronized(messageQueueLock) {
messageQueue.firstOrNull()
}
}
private fun removeFirstQueuedMessage(): String? {
return synchronized(messageQueueLock) {
if (messageQueue.isEmpty()) null else messageQueue.removeFirst()
}
}
private fun queuedMessageCount(): Int {
return synchronized(messageQueueLock) {
messageQueue.size
}
}
fun setMicEnabled(enabled: Boolean) {
if (_micEnabled.value == enabled) return
_micEnabled.value = enabled
if (enabled) {
val pausedForTts =
synchronized(ttsPauseLock) {
if (ttsPauseDepth > 0) {
resumeMicAfterTts = true
true
} else {
false
}
}
if (pausedForTts) {
_statusText.value = if (_isSending.value) "Speaking · waiting for reply" else "Speaking…"
return
}
start()
sendQueuedIfIdle()
} else {
@@ -126,6 +181,58 @@ class MicCaptureManager(
}
}
suspend fun pauseForTts() {
val shouldPause =
synchronized(ttsPauseLock) {
ttsPauseDepth += 1
if (ttsPauseDepth > 1) return@synchronized false
resumeMicAfterTts = _micEnabled.value
val active = resumeMicAfterTts || recognizer != null || _isListening.value
if (!active) return@synchronized false
stopRequested = true
restartJob?.cancel()
restartJob = null
transcriptFlushJob?.cancel()
transcriptFlushJob = null
_isListening.value = false
_inputLevel.value = 0f
_liveTranscript.value = null
_statusText.value = if (_isSending.value) "Speaking · waiting for reply" else "Speaking…"
true
}
if (!shouldPause) return
withContext(Dispatchers.Main) {
recognizer?.cancel()
recognizer?.destroy()
recognizer = null
}
}
suspend fun resumeAfterTts() {
val shouldResume =
synchronized(ttsPauseLock) {
if (ttsPauseDepth == 0) return@synchronized false
ttsPauseDepth -= 1
if (ttsPauseDepth > 0) return@synchronized false
val resume = resumeMicAfterTts && _micEnabled.value
resumeMicAfterTts = false
if (!resume) {
_statusText.value =
when {
_micEnabled.value && _isSending.value -> "Listening · sending queued voice"
_micEnabled.value -> "Listening"
_isSending.value -> "Mic off · sending…"
else -> "Mic off"
}
}
resume
}
if (!shouldResume) return
stopRequested = false
start()
sendQueuedIfIdle()
}
fun onGatewayConnectionChanged(connected: Boolean) {
gatewayConnected = connected
if (connected) {
@@ -137,7 +244,7 @@ class MicCaptureManager(
pendingRunId = null
pendingAssistantEntryId = null
_isSending.value = false
if (messageQueue.isNotEmpty()) {
if (hasQueuedMessages()) {
_statusText.value = queuedWaitingStatus()
}
}
@@ -245,7 +352,7 @@ class MicCaptureManager(
_statusText.value =
when {
_isSending.value -> "Listening · sending queued voice"
messageQueue.isNotEmpty() -> "Listening · ${messageQueue.size} queued"
hasQueuedMessages() -> "Listening · ${queuedMessageCount()} queued"
else -> "Listening"
}
_isListening.value = true
@@ -278,7 +385,7 @@ class MicCaptureManager(
role = VoiceConversationRole.User,
text = message,
)
messageQueue.addLast(message)
enqueueMessage(message)
publishQueue()
}
@@ -297,12 +404,12 @@ class MicCaptureManager(
}
private fun publishQueue() {
_queuedMessages.value = messageQueue.toList()
_queuedMessages.value = snapshotMessageQueue()
}
private fun sendQueuedIfIdle() {
if (_isSending.value) return
if (messageQueue.isEmpty()) {
if (!hasQueuedMessages()) {
if (_micEnabled.value) {
_statusText.value = "Listening"
} else {
@@ -315,7 +422,7 @@ class MicCaptureManager(
return
}
val next = messageQueue.first()
val next = firstQueuedMessage() ?: return
_isSending.value = true
pendingRunTimeoutJob?.cancel()
pendingRunTimeoutJob = null
@@ -333,7 +440,7 @@ class MicCaptureManager(
if (runId == null) {
pendingRunTimeoutJob?.cancel()
pendingRunTimeoutJob = null
messageQueue.removeFirst()
removeFirstQueuedMessage()
publishQueue()
_isSending.value = false
pendingAssistantEntryId = null
@@ -379,8 +486,7 @@ class MicCaptureManager(
private fun completePendingTurn() {
pendingRunTimeoutJob?.cancel()
pendingRunTimeoutJob = null
if (messageQueue.isNotEmpty()) {
messageQueue.removeFirst()
if (removeFirstQueuedMessage() != null) {
publishQueue()
}
pendingRunId = null
@@ -390,7 +496,7 @@ class MicCaptureManager(
}
private fun queuedWaitingStatus(): String {
return "${messageQueue.size} queued · waiting for gateway"
return "${queuedMessageCount()} queued · waiting for gateway"
}
private fun appendConversation(

View File

@@ -0,0 +1,242 @@
package ai.openclaw.app.voice
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioTrack
import android.media.MediaPlayer
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File
internal class TalkAudioPlayer(
private val context: Context,
) {
private val lock = Any()
private var active: ActivePlayback? = null
suspend fun play(audio: TalkSpeakAudio) {
when (val mode = resolvePlaybackMode(audio)) {
is TalkPlaybackMode.Pcm -> playPcm(audio.bytes, mode.sampleRate)
is TalkPlaybackMode.Compressed -> playCompressed(audio.bytes, mode.fileExtension)
}
}
fun stop() {
synchronized(lock) {
active?.cancel()
active = null
}
}
internal fun resolvePlaybackMode(audio: TalkSpeakAudio): TalkPlaybackMode {
return resolvePlaybackMode(
outputFormat = audio.outputFormat,
mimeType = audio.mimeType,
fileExtension = audio.fileExtension,
)
}
companion object {
internal fun resolvePlaybackMode(
outputFormat: String?,
mimeType: String?,
fileExtension: String?,
): TalkPlaybackMode {
val normalizedOutputFormat = outputFormat?.trim()?.lowercase()
if (normalizedOutputFormat != null) {
val pcmSampleRate = parsePcmSampleRate(normalizedOutputFormat)
if (pcmSampleRate != null) {
return TalkPlaybackMode.Pcm(sampleRate = pcmSampleRate)
}
}
val normalizedMimeType = mimeType?.trim()?.lowercase()
val extension =
normalizeExtension(
fileExtension ?: inferExtension(outputFormat = normalizedOutputFormat, mimeType = normalizedMimeType),
)
if (extension != null) {
return TalkPlaybackMode.Compressed(fileExtension = extension)
}
throw IllegalStateException("Unsupported talk audio format")
}
private fun parsePcmSampleRate(outputFormat: String): Int? {
return when (outputFormat) {
"pcm_16000" -> 16_000
"pcm_22050" -> 22_050
"pcm_24000" -> 24_000
"pcm_44100" -> 44_100
else -> null
}
}
private fun inferExtension(outputFormat: String?, mimeType: String?): String? {
return when {
outputFormat == "mp3" || outputFormat?.startsWith("mp3_") == true || mimeType == "audio/mpeg" -> ".mp3"
outputFormat == "opus" || outputFormat?.startsWith("opus_") == true || mimeType == "audio/ogg" -> ".ogg"
outputFormat?.endsWith("-wav") == true || mimeType == "audio/wav" -> ".wav"
outputFormat?.endsWith("-webm") == true || mimeType == "audio/webm" -> ".webm"
else -> null
}
}
private fun normalizeExtension(value: String?): String? {
val trimmed = value?.trim()?.lowercase().orEmpty()
if (trimmed.isEmpty()) return null
return if (trimmed.startsWith(".")) trimmed else ".$trimmed"
}
}
private suspend fun playPcm(bytes: ByteArray, sampleRate: Int) {
withContext(Dispatchers.IO) {
val minBufferSize =
AudioTrack.getMinBufferSize(
sampleRate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
)
if (minBufferSize <= 0) {
throw IllegalStateException("AudioTrack buffer unavailable")
}
val track =
AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRate)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build(),
)
.setTransferMode(AudioTrack.MODE_STATIC)
.setBufferSizeInBytes(maxOf(minBufferSize, bytes.size))
.build()
val finished = CompletableDeferred<Unit>()
val playback =
ActivePlayback(
cancel = {
finished.completeExceptionally(CancellationException("assistant speech cancelled"))
runCatching { track.pause() }
runCatching { track.flush() }
runCatching { track.stop() }
},
)
register(playback)
try {
val written = track.write(bytes, 0, bytes.size)
if (written != bytes.size) {
throw IllegalStateException("AudioTrack write failed")
}
val totalFrames = bytes.size / 2
track.play()
while (track.playState == AudioTrack.PLAYSTATE_PLAYING) {
if (track.playbackHeadPosition >= totalFrames) {
finished.complete(Unit)
break
}
delay(20)
}
if (!finished.isCompleted) {
finished.complete(Unit)
}
finished.await()
} finally {
clear(playback)
runCatching { track.pause() }
runCatching { track.flush() }
runCatching { track.stop() }
track.release()
}
}
}
private suspend fun playCompressed(bytes: ByteArray, fileExtension: String) {
val tempFile = withContext(Dispatchers.IO) {
File.createTempFile("talk-audio-", fileExtension, context.cacheDir).apply {
writeBytes(bytes)
}
}
try {
val finished = CompletableDeferred<Unit>()
val player =
withContext(Dispatchers.Main) {
MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
setDataSource(tempFile.absolutePath)
setOnCompletionListener {
finished.complete(Unit)
}
setOnErrorListener { _, what, extra ->
finished.completeExceptionally(IllegalStateException("MediaPlayer error ($what/$extra)"))
true
}
prepare()
}
}
val playback =
ActivePlayback(
cancel = {
finished.completeExceptionally(CancellationException("assistant speech cancelled"))
runCatching { player.stop() }
},
)
register(playback)
try {
withContext(Dispatchers.Main) {
player.start()
}
finished.await()
} finally {
clear(playback)
withContext(Dispatchers.Main) {
runCatching { player.stop() }
player.release()
}
}
} finally {
withContext(Dispatchers.IO) {
tempFile.delete()
}
}
}
private fun register(playback: ActivePlayback) {
synchronized(lock) {
active?.cancel()
active = playback
}
}
private fun clear(playback: ActivePlayback) {
synchronized(lock) {
if (active === playback) {
active = null
}
}
}
}
internal sealed interface TalkPlaybackMode {
data class Pcm(val sampleRate: Int) : TalkPlaybackMode
data class Compressed(val fileExtension: String) : TalkPlaybackMode
}
private class ActivePlayback(
val cancel: () -> Unit,
)

View File

@@ -14,19 +14,21 @@ import android.os.SystemClock
import android.speech.RecognitionListener
import android.speech.RecognizerIntent
import android.speech.SpeechRecognizer
import android.util.Log
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import android.util.Log
import androidx.core.content.ContextCompat
import ai.openclaw.app.gateway.GatewaySession
import java.util.Locale
import java.util.UUID
import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
@@ -46,6 +48,8 @@ class TalkModeManager(
private val session: GatewaySession,
private val supportsChatSubscribe: Boolean,
private val isConnected: () -> Boolean,
private val onBeforeSpeak: suspend () -> Unit = {},
private val onAfterSpeak: suspend () -> Unit = {},
) {
companion object {
private const val tag = "TalkMode"
@@ -57,6 +61,8 @@ class TalkModeManager(
private val mainHandler = Handler(Looper.getMainLooper())
private val json = Json { ignoreUnknownKeys = true }
private val talkSpeakClient = TalkSpeakClient(session = session, json = json)
private val talkAudioPlayer = TalkAudioPlayer(context)
private val _isEnabled = MutableStateFlow(false)
val isEnabled: StateFlow<Boolean> = _isEnabled
@@ -101,6 +107,7 @@ class TalkModeManager(
private val playbackGeneration = AtomicLong(0L)
private var ttsJob: Job? = null
private val ttsJobLock = Any()
private val ttsLock = Any()
private var textToSpeech: TextToSpeech? = null
private var textToSpeechInit: CompletableDeferred<TextToSpeech>? = null
@@ -163,8 +170,11 @@ class TalkModeManager(
?: waitForAssistantText(session, startedAt, if (ok) 12_000 else 25_000)
if (!assistant.isNullOrBlank()) {
val playbackToken = playbackGeneration.incrementAndGet()
cancelActivePlayback()
_statusText.value = "Speaking…"
playAssistant(assistant, playbackToken)
runPlaybackSession(playbackToken) {
playAssistant(assistant, playbackToken)
}
} else {
_statusText.value = "No reply"
}
@@ -180,14 +190,12 @@ class TalkModeManager(
fun playTtsForText(text: String) {
val playbackToken = playbackGeneration.incrementAndGet()
ttsJob?.cancel()
ttsJob = scope.launch {
cancelActivePlayback()
scope.launch {
reloadConfig()
ensurePlaybackActive(playbackToken)
_isSpeaking.value = true
_statusText.value = "Speaking…"
playAssistant(text, playbackToken)
ttsJob = null
runPlaybackSession(playbackToken) {
playAssistant(text, playbackToken)
}
}
}
@@ -258,7 +266,6 @@ class TalkModeManager(
if (playbackEnabled == enabled) return
playbackEnabled = enabled
if (!enabled) {
playbackGeneration.incrementAndGet()
stopSpeaking()
}
}
@@ -270,10 +277,11 @@ class TalkModeManager(
suspend fun speakAssistantReply(text: String) {
if (!playbackEnabled) return
val playbackToken = playbackGeneration.incrementAndGet()
stopSpeaking(resetInterrupt = false)
cancelActivePlayback()
ensureConfigLoaded()
ensurePlaybackActive(playbackToken)
playAssistant(text, playbackToken)
runPlaybackSession(playbackToken) {
playAssistant(text, playbackToken)
}
}
private fun start() {
@@ -483,9 +491,10 @@ class TalkModeManager(
}
Log.d(tag, "assistant text ok chars=${assistant.length}")
val playbackToken = playbackGeneration.incrementAndGet()
stopSpeaking(resetInterrupt = false)
ensurePlaybackActive(playbackToken)
playAssistant(assistant, playbackToken)
cancelActivePlayback()
runPlaybackSession(playbackToken) {
playAssistant(assistant, playbackToken)
}
} catch (err: Throwable) {
if (err is CancellationException) {
Log.d(tag, "finalize speech cancelled")
@@ -655,22 +664,87 @@ class TalkModeManager(
requestAudioFocusForTts()
try {
val ttsStarted = SystemClock.elapsedRealtime()
speakWithSystemTts(cleaned, directive, playbackToken)
Log.d(tag, "system tts ok durMs=${SystemClock.elapsedRealtime() - ttsStarted}")
val started = SystemClock.elapsedRealtime()
when (val result = talkSpeakClient.synthesize(text = cleaned, directive = directive)) {
is TalkSpeakResult.Success -> {
ensurePlaybackActive(playbackToken)
talkAudioPlayer.play(result.audio)
ensurePlaybackActive(playbackToken)
Log.d(tag, "talk.speak ok durMs=${SystemClock.elapsedRealtime() - started}")
}
is TalkSpeakResult.FallbackToLocal -> {
Log.d(tag, "talk.speak unavailable; using local TTS: ${result.message}")
speakWithSystemTts(cleaned, directive, playbackToken)
Log.d(tag, "system tts ok durMs=${SystemClock.elapsedRealtime() - started}")
}
is TalkSpeakResult.Failure -> {
throw IllegalStateException(result.message)
}
}
} catch (err: Throwable) {
if (isPlaybackCancelled(err, playbackToken)) {
Log.d(tag, "assistant speech cancelled")
return
}
_statusText.value = "Speak failed: ${err.message ?: err::class.simpleName}"
Log.w(tag, "system tts failed: ${err.message ?: err::class.simpleName}")
Log.w(tag, "talk playback failed: ${err.message ?: err::class.simpleName}")
} finally {
_isSpeaking.value = false
}
}
private suspend fun runPlaybackSession(
playbackToken: Long,
block: suspend () -> Unit,
) {
val currentJob = coroutineContext[Job]
var shouldResumeAfterSpeak = false
try {
val claimedPlayback =
synchronized(ttsJobLock) {
if (!playbackEnabled || playbackToken != playbackGeneration.get()) {
false
} else {
ttsJob = currentJob
true
}
}
if (!claimedPlayback) {
ensurePlaybackActive(playbackToken)
return
}
ensurePlaybackActive(playbackToken)
shouldResumeAfterSpeak = true
onBeforeSpeak()
ensurePlaybackActive(playbackToken)
_isSpeaking.value = true
_statusText.value = "Speaking…"
block()
} finally {
synchronized(ttsJobLock) {
if (ttsJob === currentJob) {
ttsJob = null
}
}
_isSpeaking.value = false
if (shouldResumeAfterSpeak) {
withContext(NonCancellable) {
onAfterSpeak()
}
}
}
}
private fun cancelActivePlayback() {
val activeJob =
synchronized(ttsJobLock) {
ttsJob
}
activeJob?.cancel()
talkAudioPlayer.stop()
stopTextToSpeechPlayback()
}
private suspend fun speakWithSystemTts(text: String, directive: TalkDirective?, playbackToken: Long) {
ensurePlaybackActive(playbackToken)
val engine = ensureTextToSpeech()
@@ -755,15 +829,16 @@ class TalkModeManager(
}
private fun stopSpeaking(resetInterrupt: Boolean = true) {
playbackGeneration.incrementAndGet()
if (!_isSpeaking.value) {
stopTextToSpeechPlayback()
cancelActivePlayback()
abandonAudioFocus()
return
}
if (resetInterrupt) {
lastInterruptedAtSeconds = null
}
stopTextToSpeechPlayback()
cancelActivePlayback()
_isSpeaking.value = false
abandonAudioFocus()
}

View File

@@ -0,0 +1,143 @@
package ai.openclaw.app.voice
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
internal data class TalkSpeakAudio(
val bytes: ByteArray,
val provider: String,
val outputFormat: String?,
val voiceCompatible: Boolean?,
val mimeType: String?,
val fileExtension: String?,
)
internal sealed interface TalkSpeakResult {
data class Success(val audio: TalkSpeakAudio) : TalkSpeakResult
data class FallbackToLocal(val message: String) : TalkSpeakResult
data class Failure(val message: String) : TalkSpeakResult
}
internal class TalkSpeakClient(
private val session: GatewaySession? = null,
private val json: Json = Json { ignoreUnknownKeys = true },
private val requestDetailed: (suspend (String, String, Long) -> GatewaySession.RpcResult)? = null,
) {
suspend fun synthesize(text: String, directive: TalkDirective?): TalkSpeakResult {
val response =
try {
performRequest(
method = "talk.speak",
paramsJson = json.encodeToString(TalkSpeakRequest.from(text = text, directive = directive)),
timeoutMs = 45_000,
)
} catch (err: Throwable) {
return TalkSpeakResult.Failure(err.message ?: "talk.speak request failed")
}
if (!response.ok) {
val error = response.error
val message = error?.message ?: "talk.speak request failed"
return if (isFallbackEligible(error)) {
TalkSpeakResult.FallbackToLocal(message)
} else {
TalkSpeakResult.Failure(message)
}
}
val payload =
try {
json.decodeFromString<TalkSpeakResponse>(response.payloadJson ?: "")
} catch (err: Throwable) {
return TalkSpeakResult.Failure(err.message ?: "talk.speak payload invalid")
}
val bytes =
try {
android.util.Base64.decode(payload.audioBase64, android.util.Base64.DEFAULT)
} catch (err: Throwable) {
return TalkSpeakResult.Failure(err.message ?: "talk.speak audio decode failed")
}
if (bytes.isEmpty()) {
return TalkSpeakResult.Failure("talk.speak returned empty audio")
}
return TalkSpeakResult.Success(
TalkSpeakAudio(
bytes = bytes,
provider = payload.provider,
outputFormat = payload.outputFormat,
voiceCompatible = payload.voiceCompatible,
mimeType = payload.mimeType,
fileExtension = payload.fileExtension,
),
)
}
private fun isFallbackEligible(error: GatewaySession.ErrorShape?): Boolean {
val reason = error?.details?.reason
if (reason == null) return true
return reason == "talk_unconfigured" ||
reason == "talk_provider_unsupported" ||
reason == "method_unavailable"
}
private suspend fun performRequest(
method: String,
paramsJson: String,
timeoutMs: Long,
): GatewaySession.RpcResult {
requestDetailed?.let { return it(method, paramsJson, timeoutMs) }
val activeSession = session ?: throw IllegalStateException("session missing")
return activeSession.requestDetailed(method = method, paramsJson = paramsJson, timeoutMs = timeoutMs)
}
}
@Serializable
internal data class TalkSpeakRequest(
val text: String,
val voiceId: String? = null,
val modelId: String? = null,
val outputFormat: String? = null,
val speed: Double? = null,
val rateWpm: Int? = null,
val stability: Double? = null,
val similarity: Double? = null,
val style: Double? = null,
val speakerBoost: Boolean? = null,
val seed: Long? = null,
val normalize: String? = null,
val language: String? = null,
val latencyTier: Int? = null,
) {
companion object {
fun from(text: String, directive: TalkDirective?): TalkSpeakRequest {
return TalkSpeakRequest(
text = text,
voiceId = directive?.voiceId,
modelId = directive?.modelId,
outputFormat = directive?.outputFormat,
speed = directive?.speed,
rateWpm = directive?.rateWpm,
stability = directive?.stability,
similarity = directive?.similarity,
style = directive?.style,
speakerBoost = directive?.speakerBoost,
seed = directive?.seed,
normalize = directive?.normalize,
language = directive?.language,
latencyTier = directive?.latencyTier,
)
}
}
}
@Serializable
private data class TalkSpeakResponse(
val audioBase64: String,
val provider: String,
val outputFormat: String? = null,
val voiceCompatible: Boolean? = null,
val mimeType: String? = null,
val fileExtension: String? = null,
)

View File

@@ -10,7 +10,7 @@
<parameter
android:name="prompt"
android:key="prompt"
android:mimeType="text/*"
android:mimeType="https://schema.org/Text"
android:required="true" />
</intent>
</capability>

View File

@@ -21,17 +21,72 @@ import java.util.UUID
@Config(sdk = [34])
class GatewayBootstrapAuthTest {
@Test
fun connectsOperatorSessionWhenBootstrapAuthExists() {
assertTrue(shouldConnectOperatorSession(token = "", bootstrapToken = "bootstrap-1", password = "", storedOperatorToken = ""))
assertTrue(shouldConnectOperatorSession(token = null, bootstrapToken = "bootstrap-1", password = null, storedOperatorToken = null))
fun skipsOperatorSessionWhenOnlyBootstrapAuthExists() {
assertFalse(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = "", bootstrapToken = "bootstrap-1", password = ""),
storedOperatorToken = "",
),
)
assertFalse(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
storedOperatorToken = null,
),
)
}
@Test
fun skipsOperatorSessionOnlyWhenNoSharedBootstrapOrStoredAuthExists() {
assertTrue(shouldConnectOperatorSession(token = "shared-token", bootstrapToken = "bootstrap-1", password = null, storedOperatorToken = null))
assertTrue(shouldConnectOperatorSession(token = null, bootstrapToken = "bootstrap-1", password = "shared-password", storedOperatorToken = null))
assertTrue(shouldConnectOperatorSession(token = null, bootstrapToken = null, password = null, storedOperatorToken = "stored-token"))
assertFalse(shouldConnectOperatorSession(token = null, bootstrapToken = "", password = null, storedOperatorToken = null))
fun connectsOperatorSessionWhenSharedPasswordOrStoredAuthExists() {
assertTrue(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = "bootstrap-1", password = null),
storedOperatorToken = null,
),
)
assertTrue(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = "shared-password"),
storedOperatorToken = null,
),
)
assertTrue(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
storedOperatorToken = "stored-token",
),
)
assertFalse(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "", password = null),
storedOperatorToken = null,
),
)
}
@Test
fun resolveOperatorSessionConnectAuthUsesStoredTokenPathAfterBootstrapHandoff() {
val resolved =
resolveOperatorSessionConnectAuth(
auth = NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
storedOperatorToken = "stored-token",
)
assertEquals(NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = null, password = null), resolved)
}
@Test
fun resolveOperatorSessionConnectAuthPrefersExplicitSharedAuth() {
val resolved =
resolveOperatorSessionConnectAuth(
auth = NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = "bootstrap-1", password = "shared-password"),
storedOperatorToken = "stored-token",
)
assertEquals(
NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = null, password = null),
resolved,
)
}
@Test
@@ -97,7 +152,7 @@ class GatewayBootstrapAuthTest {
assertEquals("fp-1", prefs.loadGatewayTlsFingerprint(endpoint.stableId))
assertEquals("setup-bootstrap-token", desiredBootstrapToken(runtime, "nodeSession"))
assertEquals("setup-bootstrap-token", desiredBootstrapToken(runtime, "operatorSession"))
assertNull(desiredBootstrapToken(runtime, "operatorSession"))
}
@Test

View File

@@ -0,0 +1,63 @@
package ai.openclaw.app.gateway
import ai.openclaw.app.SecurePrefs
import android.content.Context
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import java.util.UUID
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class DeviceAuthStoreTest {
@Test
fun saveTokenPersistsNormalizedScopesMetadata() {
val app = RuntimeEnvironment.getApplication()
val securePrefs =
app.getSharedPreferences(
"openclaw.node.secure.test.${UUID.randomUUID()}",
Context.MODE_PRIVATE,
)
val prefs = SecurePrefs(app, securePrefsOverride = securePrefs)
val store = DeviceAuthStore(prefs)
store.saveToken(
deviceId = " Device-1 ",
role = " Operator ",
token = " operator-token ",
scopes = listOf("operator.write", "operator.read", "operator.write", " "),
)
val entry = store.loadEntry("device-1", "operator")
assertNotNull(entry)
assertEquals("operator-token", entry?.token)
assertEquals("operator", entry?.role)
assertEquals(listOf("operator.read", "operator.write"), entry?.scopes)
assertTrue((entry?.updatedAtMs ?: 0L) > 0L)
}
@Test
fun loadEntryReadsLegacyTokenWithoutMetadata() {
val app = RuntimeEnvironment.getApplication()
val securePrefs =
app.getSharedPreferences(
"openclaw.node.secure.test.${UUID.randomUUID()}",
Context.MODE_PRIVATE,
)
val prefs = SecurePrefs(app, securePrefsOverride = securePrefs)
prefs.putString("gateway.deviceToken.device-1.operator", "legacy-token")
val store = DeviceAuthStore(prefs)
val entry = store.loadEntry("device-1", "operator")
assertNotNull(entry)
assertEquals("legacy-token", entry?.token)
assertEquals("operator", entry?.role)
assertEquals(emptyList<String>(), entry?.scopes)
assertEquals(0L, entry?.updatedAtMs)
}
}

View File

@@ -35,12 +35,18 @@ private const val CONNECT_CHALLENGE_FRAME =
"""{"type":"event","event":"connect.challenge","payload":{"nonce":"android-test-nonce"}}"""
private class InMemoryDeviceAuthStore : DeviceAuthTokenStore {
private val tokens = mutableMapOf<String, String>()
private val tokens = mutableMapOf<String, DeviceAuthEntry>()
override fun loadToken(deviceId: String, role: String): String? = tokens["${deviceId.trim()}|${role.trim()}"]?.trim()?.takeIf { it.isNotEmpty() }
override fun loadEntry(deviceId: String, role: String): DeviceAuthEntry? = tokens["${deviceId.trim()}|${role.trim()}"]
override fun saveToken(deviceId: String, role: String, token: String) {
tokens["${deviceId.trim()}|${role.trim()}"] = token.trim()
override fun saveToken(deviceId: String, role: String, token: String, scopes: List<String>) {
tokens["${deviceId.trim()}|${role.trim()}"] =
DeviceAuthEntry(
token = token.trim(),
role = role.trim(),
scopes = scopes,
updatedAtMs = System.currentTimeMillis(),
)
}
override fun clearToken(deviceId: String, role: String) {
@@ -213,6 +219,144 @@ class GatewaySessionInvokeTest {
}
}
@Test
fun connect_storesPrimaryDeviceTokenFromSuccessfulSharedTokenConnect() = runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, _ ->
when (method) {
"connect" -> {
webSocket.send(
connectResponseFrame(
id,
authJson = """{"deviceToken":"shared-node-token","role":"node","scopes":[]}""",
),
)
webSocket.close(1000, "done")
}
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
connectNodeSession(
session = harness.session,
port = server.port,
token = "shared-auth-token",
bootstrapToken = null,
)
awaitConnectedOrThrow(connected, lastDisconnect, server)
val deviceId = DeviceIdentityStore(RuntimeEnvironment.getApplication()).loadOrCreate().deviceId
assertEquals("shared-node-token", harness.deviceAuthStore.loadToken(deviceId, "node"))
assertNull(harness.deviceAuthStore.loadToken(deviceId, "operator"))
} finally {
shutdownHarness(harness, server)
}
}
@Test
fun bootstrapConnect_storesAdditionalBoundedDeviceTokensOnTrustedTransport() = runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, _ ->
when (method) {
"connect" -> {
webSocket.send(
connectResponseFrame(
id,
authJson =
"""{"deviceToken":"bootstrap-node-token","role":"node","scopes":[],"deviceTokens":[{"deviceToken":"bootstrap-operator-token","role":"operator","scopes":["operator.admin","operator.approvals","operator.read","operator.talk.secrets","operator.write"]}]}""",
),
)
webSocket.close(1000, "done")
}
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
connectNodeSession(
session = harness.session,
port = server.port,
token = null,
bootstrapToken = "bootstrap-token",
)
awaitConnectedOrThrow(connected, lastDisconnect, server)
val deviceId = DeviceIdentityStore(RuntimeEnvironment.getApplication()).loadOrCreate().deviceId
val nodeEntry = harness.deviceAuthStore.loadEntry(deviceId, "node")
val operatorEntry = harness.deviceAuthStore.loadEntry(deviceId, "operator")
assertEquals("bootstrap-node-token", nodeEntry?.token)
assertEquals(emptyList<String>(), nodeEntry?.scopes)
assertEquals("bootstrap-operator-token", operatorEntry?.token)
assertEquals(
listOf("operator.approvals", "operator.read", "operator.talk.secrets", "operator.write"),
operatorEntry?.scopes,
)
} finally {
shutdownHarness(harness, server)
}
}
@Test
fun nonBootstrapConnect_ignoresAdditionalBootstrapDeviceTokens() = runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, _ ->
when (method) {
"connect" -> {
webSocket.send(
connectResponseFrame(
id,
authJson =
"""{"deviceToken":"shared-node-token","role":"node","scopes":[],"deviceTokens":[{"deviceToken":"shared-operator-token","role":"operator","scopes":["operator.approvals","operator.read"]}]}""",
),
)
webSocket.close(1000, "done")
}
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
connectNodeSession(
session = harness.session,
port = server.port,
token = "shared-auth-token",
bootstrapToken = null,
)
awaitConnectedOrThrow(connected, lastDisconnect, server)
val deviceId = DeviceIdentityStore(RuntimeEnvironment.getApplication()).loadOrCreate().deviceId
assertEquals("shared-node-token", harness.deviceAuthStore.loadToken(deviceId, "node"))
assertNull(harness.deviceAuthStore.loadToken(deviceId, "operator"))
} finally {
shutdownHarness(harness, server)
}
}
@Test
fun nodeInvokeRequest_roundTripsInvokeResult() = runBlocking {
val handshakeOrigin = AtomicReference<String?>(null)
@@ -470,9 +614,14 @@ class GatewaySessionInvokeTest {
}
}
private fun connectResponseFrame(id: String, canvasHostUrl: String? = null): String {
private fun connectResponseFrame(
id: String,
canvasHostUrl: String? = null,
authJson: String? = null,
): String {
val canvas = canvasHostUrl?.let { "\"canvasHostUrl\":\"$it\"," } ?: ""
return """{"type":"res","id":"$id","ok":true,"payload":{$canvas"snapshot":{"sessionDefaults":{"mainSessionKey":"main"}}}}"""
val auth = authJson?.let { "\"auth\":$it," } ?: ""
return """{"type":"res","id":"$id","ok":true,"payload":{$canvas$auth"snapshot":{"sessionDefaults":{"mainSessionKey":"main"}}}}"""
}
private fun startGatewayServer(

View File

@@ -39,4 +39,34 @@ class CanvasActionTrustTest {
),
)
}
@Test
fun acceptsFragmentOnlyDifferenceForTrustedA2uiPage() {
assertTrue(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android#step2",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
}
@Test
fun rejectsQueryMismatchOnTrustedOriginAndPath() {
assertFalse(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=ios",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
}
@Test
fun rejectsDescendantPathUnderTrustedA2uiRoot() {
assertFalse(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/child/index.html?platform=android",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
}
}

View File

@@ -108,7 +108,7 @@ class ConnectionManagerTest {
}
@Test
fun resolveTlsParamsForEndpoint_manualPrivateLanRequiresTlsWhenToggleIsOff() {
fun resolveTlsParamsForEndpoint_manualPrivateLanCanStayCleartextWhenToggleIsOff() {
val endpoint = GatewayEndpoint.manual(host = "192.168.1.20", port = 18789)
val params =
@@ -118,9 +118,7 @@ class ConnectionManagerTest {
manualTlsEnabled = false,
)
assertEquals(true, params?.required)
assertNull(params?.expectedFingerprint)
assertEquals(false, params?.allowTOFU)
assertNull(params)
}
@Test
@@ -148,7 +146,7 @@ class ConnectionManagerTest {
}
@Test
fun resolveTlsParamsForEndpoint_discoveryPrivateLanWithoutHintsRequiresTls() {
fun resolveTlsParamsForEndpoint_discoveryPrivateLanWithoutHintsCanStayCleartext() {
val endpoint =
GatewayEndpoint(
stableId = "_openclaw-gw._tcp.|local.|Test",
@@ -166,9 +164,7 @@ class ConnectionManagerTest {
manualTlsEnabled = false,
)
assertEquals(true, params?.required)
assertNull(params?.expectedFingerprint)
assertEquals(false, params?.allowTOFU)
assertNull(params)
}
@Test

View File

@@ -0,0 +1,50 @@
package ai.openclaw.app.ui
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class CanvasA2UIActionBridgeTest {
@Test
fun forwardsTrimmedPayloadFromTrustedPage() {
val forwarded = mutableListOf<String>()
val bridge =
CanvasA2UIActionBridge(
isTrustedPage = { true },
onMessage = { forwarded += it },
)
bridge.postMessage(" {\"ok\":true} ")
assertEquals(listOf("{\"ok\":true}"), forwarded)
}
@Test
fun rejectsPayloadFromUntrustedPage() {
val forwarded = mutableListOf<String>()
val bridge =
CanvasA2UIActionBridge(
isTrustedPage = { false },
onMessage = { forwarded += it },
)
bridge.postMessage("{\"ok\":true}")
assertTrue(forwarded.isEmpty())
}
@Test
fun rejectsBlankPayloadBeforeForwarding() {
val forwarded = mutableListOf<String>()
val bridge =
CanvasA2UIActionBridge(
isTrustedPage = { true },
onMessage = { forwarded += it },
)
bridge.postMessage(" ")
bridge.postMessage(null)
assertTrue(forwarded.isEmpty())
}
}

View File

@@ -290,11 +290,19 @@ class GatewayConfigResolverTest {
}
@Test
fun parseGatewayEndpointResultFlagsInsecureLanCleartextGateway() {
fun parseGatewayEndpointResultAcceptsLanCleartextGateway() {
val parsed = parseGatewayEndpointResult("ws://192.168.1.20:18789")
assertNull(parsed.config)
assertEquals(GatewayEndpointValidationError.INSECURE_REMOTE_URL, parsed.error)
assertEquals(
GatewayEndpointConfig(
host = "192.168.1.20",
port = 18789,
tls = false,
displayUrl = "http://192.168.1.20:18789",
),
parsed.config,
)
assertNull(parsed.error)
}
@Test

View File

@@ -0,0 +1,44 @@
package ai.openclaw.app.voice
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class TalkAudioPlayerTest {
@Test
fun resolvesPcmPlaybackFromOutputFormat() {
val mode =
TalkAudioPlayer.resolvePlaybackMode(
outputFormat = "pcm_24000",
mimeType = null,
fileExtension = null,
)
assertEquals(TalkPlaybackMode.Pcm(sampleRate = 24_000), mode)
}
@Test
fun resolvesCompressedPlaybackFromMimeType() {
val mode =
TalkAudioPlayer.resolvePlaybackMode(
outputFormat = null,
mimeType = "audio/mpeg",
fileExtension = null,
)
assertEquals(TalkPlaybackMode.Compressed(fileExtension = ".mp3"), mode)
}
@Test
fun preservesProvidedExtensionForCompressedPlayback() {
val mode =
TalkAudioPlayer.resolvePlaybackMode(
outputFormat = null,
mimeType = "audio/webm",
fileExtension = "webm",
)
assertTrue(mode is TalkPlaybackMode.Compressed)
assertEquals(".webm", (mode as TalkPlaybackMode.Compressed).fileExtension)
}
}

View File

@@ -0,0 +1,97 @@
package ai.openclaw.app.voice
import ai.openclaw.app.gateway.DeviceAuthEntry
import ai.openclaw.app.gateway.DeviceAuthTokenStore
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewaySession
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class TalkModeManagerTest {
@Test
fun stopTtsCancelsTrackedPlaybackJob() {
val manager = createManager()
val playbackJob = Job()
setPrivateField(manager, "ttsJob", playbackJob)
playbackGeneration(manager).set(7L)
manager.stopTts()
assertTrue(playbackJob.isCancelled)
assertEquals(8L, playbackGeneration(manager).get())
}
@Test
fun disablingPlaybackCancelsTrackedJobOnce() {
val manager = createManager()
val playbackJob = Job()
setPrivateField(manager, "ttsJob", playbackJob)
playbackGeneration(manager).set(11L)
manager.setPlaybackEnabled(false)
manager.setPlaybackEnabled(false)
assertTrue(playbackJob.isCancelled)
assertEquals(12L, playbackGeneration(manager).get())
}
private fun createManager(): TalkModeManager {
val app = RuntimeEnvironment.getApplication()
val sessionJob = SupervisorJob()
val session =
GatewaySession(
scope = CoroutineScope(sessionJob + Dispatchers.Default),
identityStore = DeviceIdentityStore(app),
deviceAuthStore = InMemoryDeviceAuthStore(),
onConnected = { _, _, _ -> },
onDisconnected = {},
onEvent = { _, _ -> },
)
return TalkModeManager(
context = app,
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
session = session,
supportsChatSubscribe = false,
isConnected = { true },
)
}
@Suppress("UNCHECKED_CAST")
private fun playbackGeneration(manager: TalkModeManager): AtomicLong {
return readPrivateField(manager, "playbackGeneration") as AtomicLong
}
private fun setPrivateField(target: Any, name: String, value: Any?) {
val field = target.javaClass.getDeclaredField(name)
field.isAccessible = true
field.set(target, value)
}
private fun readPrivateField(target: Any, name: String): Any? {
val field = target.javaClass.getDeclaredField(name)
field.isAccessible = true
return field.get(target)
}
}
private class InMemoryDeviceAuthStore : DeviceAuthTokenStore {
override fun loadEntry(deviceId: String, role: String): DeviceAuthEntry? = null
override fun saveToken(deviceId: String, role: String, token: String, scopes: List<String>) = Unit
override fun clearToken(deviceId: String, role: String) = Unit
}

View File

@@ -0,0 +1,128 @@
package ai.openclaw.app.voice
import ai.openclaw.app.gateway.GatewayConnectErrorDetails
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class TalkSpeakClientTest {
@Test
fun buildsRequestFromDirective() {
val request =
TalkSpeakRequest.from(
text = "Hello from talk mode.",
directive =
TalkDirective(
voiceId = "voice-123",
modelId = "model-abc",
speed = 1.1,
rateWpm = 190,
stability = 0.5,
similarity = 0.7,
style = 0.2,
speakerBoost = true,
seed = 42,
normalize = "auto",
language = "en",
outputFormat = "pcm_24000",
latencyTier = 3,
once = true,
),
)
assertEquals("Hello from talk mode.", request.text)
assertEquals("voice-123", request.voiceId)
assertEquals("model-abc", request.modelId)
assertEquals(1.1, request.speed)
assertEquals(190, request.rateWpm)
assertEquals(0.5, request.stability)
assertEquals(0.7, request.similarity)
assertEquals(0.2, request.style)
assertEquals(true, request.speakerBoost)
assertEquals(42L, request.seed)
assertEquals("auto", request.normalize)
assertEquals("en", request.language)
assertEquals("pcm_24000", request.outputFormat)
assertEquals(3, request.latencyTier)
}
@Test
fun fallsBackOnlyForUnavailableReasons() = runTest {
val client =
TalkSpeakClient(
requestDetailed = { _, _, _ ->
GatewaySession.RpcResult(
ok = false,
payloadJson = null,
error =
GatewaySession.ErrorShape(
code = "UNAVAILABLE",
message = "talk unavailable",
details =
GatewayConnectErrorDetails(
code = null,
canRetryWithDeviceToken = false,
recommendedNextStep = null,
reason = "talk_unconfigured",
),
),
)
},
)
val result = client.synthesize(text = "Hello", directive = null)
assertTrue(result is TalkSpeakResult.FallbackToLocal)
}
@Test
fun doesNotFallBackForSynthesisFailure() = runTest {
val client =
TalkSpeakClient(
requestDetailed = { _, _, _ ->
GatewaySession.RpcResult(
ok = false,
payloadJson = null,
error =
GatewaySession.ErrorShape(
code = "UNAVAILABLE",
message = "provider failed",
details =
GatewayConnectErrorDetails(
code = null,
canRetryWithDeviceToken = false,
recommendedNextStep = null,
reason = "synthesis_failed",
),
),
)
},
)
val result = client.synthesize(text = "Hello", directive = null)
assertTrue(result is TalkSpeakResult.Failure)
}
@Test
fun fallsBackWhenGatewayOmitsReason() = runTest {
val client =
TalkSpeakClient(
requestDetailed = { _, _, _ ->
GatewaySession.RpcResult(
ok = false,
payloadJson = null,
error =
GatewaySession.ErrorShape(
code = "INVALID_REQUEST",
message = "unknown method: talk.speak",
details = null,
),
)
},
)
val result = client.synthesize(text = "Hello", directive = null)
assertTrue(result is TalkSpeakResult.FallbackToLocal)
}
}

View File

@@ -3,19 +3,23 @@
OPENCLAW_IOS_DEFAULT_TEAM = Y5PE65HELJ
OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM)
OPENCLAW_DEVELOPMENT_TEAM = $(OPENCLAW_IOS_SELECTED_TEAM)
OPENCLAW_CODE_SIGN_STYLE = Automatic
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.client
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.client.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.client.watchkitapp.extension
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.client.activitywidget
OPENCLAW_WATCH_APP_PROFILE =
OPENCLAW_WATCH_EXTENSION_PROFILE =
// Local contributors can override this by running scripts/ios-configure-signing.sh.
// Keep include after defaults: xcconfig is evaluated top-to-bottom.
#include? "../.local-signing.xcconfig"
#include? "../LocalSigning.xcconfig"
CODE_SIGN_STYLE = Automatic
CODE_SIGN_STYLE = $(OPENCLAW_CODE_SIGN_STYLE)
CODE_SIGN_IDENTITY = Apple Development
DEVELOPMENT_TEAM = $(OPENCLAW_IOS_SELECTED_TEAM)
DEVELOPMENT_TEAM = $(OPENCLAW_DEVELOPMENT_TEAM)
// Let Xcode manage provisioning for the selected local team.
// Let Xcode manage provisioning for the selected local team unless a local override pins one.
PROVISIONING_PROFILE_SPECIFIER =

View File

@@ -1,8 +1,8 @@
// Shared iOS version defaults.
// Generated overrides live in build/Version.xcconfig (git-ignored).
OPENCLAW_GATEWAY_VERSION = 2026.4.3
OPENCLAW_MARKETING_VERSION = 2026.4.3
OPENCLAW_BUILD_VERSION = 2026040301
OPENCLAW_GATEWAY_VERSION = 2026.4.6
OPENCLAW_MARKETING_VERSION = 2026.4.6
OPENCLAW_BUILD_VERSION = 2026040601
#include? "../build/Version.xcconfig"

View File

@@ -13,3 +13,5 @@ OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.client.watchkitapp.extension
// Leave empty with automatic signing.
OPENCLAW_APP_PROFILE =
OPENCLAW_SHARE_PROFILE =
OPENCLAW_WATCH_APP_PROFILE =
OPENCLAW_WATCH_EXTENSION_PROFILE =

View File

@@ -92,6 +92,54 @@ If you need to force a specific build number:
pnpm ios:beta -- --build-number 7
```
### Maintainer Quick Release Checklist
Use this when a clone is missing local iOS release setup and you want the shortest path to a TestFlight upload.
1. Confirm Fastlane auth is set up:
```bash
cd apps/ios
fastlane ios auth_check
```
2. If auth is missing, bootstrap it once on this Mac:
```bash
scripts/ios-asc-keychain-setup.sh \
--key-path /absolute/path/to/AuthKey_XXXXXXXXXX.p8 \
--issuer-id YOUR_ISSUER_ID \
--write-env
```
This should create `apps/ios/fastlane/.env` with the non-secret ASC variables while the private key stays in Keychain.
3. Set the official/TestFlight relay URL for the build:
```bash
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
```
4. Upload the beta:
```bash
pnpm ios:beta
```
5. Expected behavior:
- Fastlane reads `package.json.version`
- resolves the next TestFlight build number for that short version
- generates `apps/ios/build/BetaRelease.xcconfig`
- archives `OpenClaw`
- uploads the IPA to TestFlight
6. Expected outputs after a successful run:
- `apps/ios/build/beta/OpenClaw-<version>.ipa`
- `apps/ios/build/beta/OpenClaw-<version>.app.dSYM.zip`
- Fastlane log line like `Uploaded iOS beta: version=<version> short=<short> build=<build>`
7. If this is a fresh clone on a maintainer machine that already works elsewhere, it is OK to copy the non-secret `apps/ios/fastlane/.env` from another trusted local clone on the same Mac. The Keychain-backed private key remains machine-local and is not stored in the repo.
## APNs Expectations For Local/Manual Builds
- The app calls `registerForRemoteNotifications()` at launch.
@@ -100,6 +148,9 @@ pnpm ios:beta -- --build-number 7
- Local/manual builds default to `OpenClawPushTransport=direct` and `OpenClawPushDistribution=local`.
- Your selected team/profile must support Push Notifications for the app bundle ID you are signing.
- If push capability or provisioning is wrong, APNs registration fails at runtime (check Xcode logs for `APNs registration failed`).
- The gateway host also needs direct APNs auth configured separately with `OPENCLAW_APNS_TEAM_ID`, `OPENCLAW_APNS_KEY_ID`, and either `OPENCLAW_APNS_PRIVATE_KEY_P8` or `OPENCLAW_APNS_PRIVATE_KEY_PATH`.
- Recommended gateway-host storage for the APNs `.p8` file is `~/.openclaw/credentials/apns/AuthKey_<KEYID>.p8` with restrictive permissions, then point `OPENCLAW_APNS_PRIVATE_KEY_PATH` at that file.
- `apps/ios/fastlane/.env` only covers App Store Connect / Fastlane auth; it does not provide gateway APNs credentials for local direct-push testing.
- Debug builds default to `OpenClawPushAPNsEnvironment=sandbox`; Release builds default to `production`.
## APNs Expectations For Official Builds

View File

@@ -0,0 +1,196 @@
import SwiftUI
private struct ExecApprovalPromptDialogModifier: ViewModifier {
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
@Environment(\.colorScheme) private var colorScheme
func body(content: Content) -> some View {
content
.overlay {
if let prompt = self.appModel.pendingExecApprovalPrompt {
ZStack {
Color.black.opacity(0.38)
.ignoresSafeArea()
ExecApprovalPromptCard(
prompt: prompt,
isResolving: self.appModel.pendingExecApprovalPromptResolving,
errorText: self.appModel.pendingExecApprovalPromptErrorText,
brighten: self.colorScheme == .light,
onAllowOnce: {
Task {
await self.appModel.resolvePendingExecApprovalPrompt(decision: "allow-once")
}
},
onAllowAlways: {
Task {
await self.appModel.resolvePendingExecApprovalPrompt(decision: "allow-always")
}
},
onDeny: {
Task {
await self.appModel.resolvePendingExecApprovalPrompt(decision: "deny")
}
},
onCancel: {
self.appModel.dismissPendingExecApprovalPrompt()
})
.padding(.horizontal, 20)
.frame(maxWidth: 460)
.transition(.scale(scale: 0.98).combined(with: .opacity))
}
.zIndex(1)
}
}
.animation(.easeInOut(duration: 0.18), value: self.appModel.pendingExecApprovalPrompt?.id)
}
}
private struct ExecApprovalPromptCard: View {
let prompt: NodeAppModel.ExecApprovalPrompt
let isResolving: Bool
let errorText: String?
let brighten: Bool
let onAllowOnce: () -> Void
let onAllowAlways: () -> Void
let onDeny: () -> Void
let onCancel: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 14) {
VStack(alignment: .leading, spacing: 6) {
Text("Exec approval required")
.font(.headline)
Text("OpenClaw opened from a notification. Review this exec request before continuing.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Text(self.prompt.commandText)
.font(.system(size: 15, weight: .regular, design: .monospaced))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(10)
.background(.black.opacity(0.14), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
VStack(alignment: .leading, spacing: 8) {
if let host = self.normalized(self.prompt.host) {
ExecApprovalPromptMetadataRow(label: "Host", value: host)
}
if let nodeId = self.normalized(self.prompt.nodeId) {
ExecApprovalPromptMetadataRow(label: "Node", value: nodeId)
}
if let agentId = self.normalized(self.prompt.agentId) {
ExecApprovalPromptMetadataRow(label: "Agent", value: agentId)
}
if let expiresText = self.expiresText(self.prompt.expiresAtMs) {
ExecApprovalPromptMetadataRow(label: "Expires", value: expiresText)
}
}
if let errorText = self.normalized(self.errorText) {
Text(errorText)
.font(.footnote)
.foregroundStyle(.red)
}
if self.isResolving {
HStack(spacing: 8) {
ProgressView()
.progressViewStyle(.circular)
Text("Resolving…")
.font(.footnote)
.foregroundStyle(.secondary)
}
}
VStack(spacing: 10) {
Button {
self.onAllowOnce()
} label: {
Text("Allow Once")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.disabled(self.isResolving)
if self.prompt.allowsAllowAlways {
Button {
self.onAllowAlways()
} label: {
Text("Allow Always")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.disabled(self.isResolving)
}
HStack(spacing: 10) {
Button(role: .destructive) {
self.onDeny()
} label: {
Text("Deny")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.disabled(self.isResolving)
Button(role: .cancel) {
self.onCancel()
} label: {
Text("Cancel")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.disabled(self.isResolving)
}
}
.controlSize(.large)
.frame(maxWidth: .infinity)
}
.statusGlassCard(brighten: self.brighten, verticalPadding: 18, horizontalPadding: 18)
}
private func normalized(_ value: String?) -> String? {
let trimmed = (value ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
private func expiresText(_ expiresAtMs: Int?) -> String? {
guard let expiresAtMs else { return nil }
let remainingSeconds = Int((Double(expiresAtMs) / 1000.0) - Date().timeIntervalSince1970)
if remainingSeconds <= 0 {
return "expired"
}
if remainingSeconds < 60 {
return "under a minute"
}
if remainingSeconds < 3600 {
let minutes = Int(ceil(Double(remainingSeconds) / 60.0))
return minutes == 1 ? "about 1 minute" : "about \(minutes) minutes"
}
let hours = Int(ceil(Double(remainingSeconds) / 3600.0))
return hours == 1 ? "about 1 hour" : "about \(hours) hours"
}
}
private struct ExecApprovalPromptMetadataRow: View {
let label: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(self.label)
.font(.caption)
.foregroundStyle(.secondary)
Text(self.value)
.font(.footnote)
.textSelection(.enabled)
}
}
}
extension View {
func execApprovalPromptDialog() -> some View {
self.modifier(ExecApprovalPromptDialogModifier())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,13 @@ private struct PendingWatchPromptAction {
var sessionKey: String?
}
private typealias PendingExecApprovalPrompt = ExecApprovalNotificationPrompt
@MainActor
enum OpenClawAppModelRegistry {
static var appModel: NodeAppModel?
}
@MainActor
final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrency UNUserNotificationCenterDelegate {
private let logger = Logger(subsystem: "ai.openclaw.ios", category: "Push")
@@ -21,10 +28,13 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
private var backgroundWakeTask: Task<Bool, Never>?
private var pendingAPNsDeviceToken: Data?
private var pendingWatchPromptActions: [PendingWatchPromptAction] = []
private var pendingExecApprovalPrompts: [PendingExecApprovalPrompt] = []
private var pendingExecApprovalRequestedPushIDs: [String] = []
private var pendingExecApprovalResolvedPushIDs: [String] = []
weak var appModel: NodeAppModel? {
didSet {
guard let model = self.appModel else { return }
guard let model = self.resolvedAppModel() else { return }
if let token = self.pendingAPNsDeviceToken {
self.pendingAPNsDeviceToken = nil
Task { @MainActor in
@@ -44,22 +54,65 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
}
}
}
if !self.pendingExecApprovalPrompts.isEmpty {
let pending = self.pendingExecApprovalPrompts
self.pendingExecApprovalPrompts.removeAll()
Task { @MainActor in
for prompt in pending {
await model.presentExecApprovalNotificationPrompt(prompt)
}
}
}
if !self.pendingExecApprovalRequestedPushIDs.isEmpty {
let pending = self.pendingExecApprovalRequestedPushIDs
self.pendingExecApprovalRequestedPushIDs.removeAll()
Task { @MainActor in
for approvalId in pending {
_ = await model.handleExecApprovalRequestedRemotePush(approvalId: approvalId)
}
}
}
if !self.pendingExecApprovalResolvedPushIDs.isEmpty {
let pending = self.pendingExecApprovalResolvedPushIDs
self.pendingExecApprovalResolvedPushIDs.removeAll()
Task { @MainActor in
for approvalId in pending {
await model.handleExecApprovalResolvedRemotePush(approvalId: approvalId)
}
}
}
}
}
private func resolvedAppModel() -> NodeAppModel? {
self.appModel ?? OpenClawAppModelRegistry.appModel
}
#if DEBUG
func _test_resolvedAppModel() -> NodeAppModel? {
self.resolvedAppModel()
}
#endif
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool
{
GatewayDiagnostics.log("app delegate: didFinishLaunching")
if self.appModel == nil {
self.appModel = OpenClawAppModelRegistry.appModel
}
self.registerBackgroundWakeRefreshTask()
UNUserNotificationCenter.current().delegate = self
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
ExecApprovalNotificationBridge.registerCategory(center: notificationCenter)
application.registerForRemoteNotifications()
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
if let appModel = self.appModel {
if let appModel = self.resolvedAppModel() {
Task { @MainActor in
appModel.updateAPNsDeviceToken(deviceToken)
}
@@ -80,7 +133,28 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
{
self.logger.info("APNs remote notification received keys=\(userInfo.keys.count, privacy: .public)")
Task { @MainActor in
guard let appModel = self.appModel else {
let notificationCenter = LiveNotificationCenter()
if await ExecApprovalNotificationBridge.handleResolvedPushIfNeeded(
userInfo: userInfo,
notificationCenter: notificationCenter)
{
if let approvalId = ExecApprovalNotificationBridge.approvalID(from: userInfo) {
if let appModel = self.resolvedAppModel() {
await appModel.handleExecApprovalResolvedRemotePush(approvalId: approvalId)
} else {
self.pendingExecApprovalResolvedPushIDs.append(approvalId)
}
}
completionHandler(.newData)
return
}
guard let appModel = self.resolvedAppModel() else {
if ExecApprovalNotificationBridge.payloadKind(userInfo: userInfo)
== ExecApprovalNotificationBridge.requestedKind,
let approvalId = ExecApprovalNotificationBridge.approvalID(from: userInfo)
{
self.pendingExecApprovalRequestedPushIDs.append(approvalId)
}
self.logger.info("APNs wake skipped: appModel unavailable")
self.scheduleBackgroundWakeRefresh(afterSeconds: 90, reason: "silent_push_no_model")
completionHandler(.noData)
@@ -96,6 +170,7 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
}
func scenePhaseChanged(_ phase: ScenePhase) {
GatewayDiagnostics.log("app delegate: scene phase changed=\(String(describing: phase))")
if phase == .background {
self.scheduleBackgroundWakeRefresh(afterSeconds: 120, reason: "scene_background")
}
@@ -140,7 +215,7 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
self.backgroundWakeTask?.cancel()
let wakeTask = Task { @MainActor [weak self] in
guard let self, let appModel = self.appModel else { return false }
guard let self, let appModel = self.resolvedAppModel() else { return false }
return await appModel.handleBackgroundRefreshWake(trigger: "bg_app_refresh")
}
self.backgroundWakeTask = wakeTask
@@ -216,8 +291,16 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
sessionKey: sessionKey)
}
private static func parseExecApprovalPrompt(
from response: UNNotificationResponse) -> PendingExecApprovalPrompt?
{
ExecApprovalNotificationBridge.parsePrompt(
actionIdentifier: response.actionIdentifier,
userInfo: response.notification.request.content.userInfo)
}
private func routeWatchPromptAction(_ action: PendingWatchPromptAction) async {
guard let appModel = self.appModel else {
guard let appModel = self.resolvedAppModel() else {
self.pendingWatchPromptActions.append(action)
return
}
@@ -229,13 +312,25 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
_ = await appModel.handleBackgroundRefreshWake(trigger: "watch_prompt_action")
}
private func routeExecApprovalPrompt(_ prompt: PendingExecApprovalPrompt) {
guard let appModel = self.resolvedAppModel() else {
self.pendingExecApprovalPrompts.append(prompt)
return
}
Task { @MainActor in
await appModel.presentExecApprovalNotificationPrompt(prompt)
}
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)
{
let userInfo = notification.request.content.userInfo
if Self.isWatchPromptNotification(userInfo) {
if Self.isWatchPromptNotification(userInfo)
|| ExecApprovalNotificationBridge.shouldPresentNotification(userInfo: userInfo)
{
completionHandler([.banner, .list, .sound])
return
}
@@ -247,18 +342,29 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void)
{
guard let action = Self.parseWatchPromptAction(from: response) else {
completionHandler()
if let action = Self.parseWatchPromptAction(from: response) {
Task { @MainActor [weak self] in
guard let self else {
completionHandler()
return
}
await self.routeWatchPromptAction(action)
completionHandler()
}
return
}
Task { @MainActor [weak self] in
guard let self else {
if let prompt = Self.parseExecApprovalPrompt(from: response) {
Task { @MainActor [weak self] in
guard let self else {
completionHandler()
return
}
self.routeExecApprovalPrompt(prompt)
completionHandler()
return
}
await self.routeWatchPromptAction(action)
completionHandler()
return
}
completionHandler()
}
}
@@ -507,6 +613,7 @@ struct OpenClawApp: App {
Self.installUncaughtExceptionLogger()
GatewaySettingsStore.bootstrapPersistence()
let appModel = NodeAppModel()
OpenClawAppModelRegistry.appModel = appModel
_appModel = State(initialValue: appModel)
_gatewayController = State(initialValue: GatewayConnectionController(appModel: appModel))
}

View File

@@ -0,0 +1,117 @@
import Foundation
import UserNotifications
struct ExecApprovalNotificationPrompt: Sendable, Equatable {
let approvalId: String
}
enum ExecApprovalNotificationBridge {
static let requestedKind = "exec.approval.requested"
static let resolvedKind = "exec.approval.resolved"
static let categoryIdentifier = "openclaw.exec-approval"
static let reviewActionIdentifier = "openclaw.exec-approval.review"
private static let localRequestPrefix = "exec.approval."
static func registerCategory(center: UNUserNotificationCenter = .current()) {
let category = UNNotificationCategory(
identifier: self.categoryIdentifier,
actions: [
UNNotificationAction(
identifier: self.reviewActionIdentifier,
title: "Review",
options: [.foreground]),
],
intentIdentifiers: [],
options: [])
center.getNotificationCategories { categories in
var updated = categories
updated.update(with: category)
center.setNotificationCategories(updated)
}
}
static func shouldPresentNotification(userInfo: [AnyHashable: Any]) -> Bool {
self.payloadKind(userInfo: userInfo) == self.requestedKind
}
static func parsePrompt(
actionIdentifier: String,
userInfo: [AnyHashable: Any]
) -> ExecApprovalNotificationPrompt?
{
guard actionIdentifier == UNNotificationDefaultActionIdentifier
|| actionIdentifier == self.reviewActionIdentifier
else {
return nil
}
guard self.payloadKind(userInfo: userInfo) == self.requestedKind else { return nil }
guard let approvalId = self.approvalID(from: userInfo) else { return nil }
return ExecApprovalNotificationPrompt(approvalId: approvalId)
}
@MainActor
static func handleResolvedPushIfNeeded(
userInfo: [AnyHashable: Any],
notificationCenter: NotificationCentering
) async -> Bool
{
guard self.payloadKind(userInfo: userInfo) == self.resolvedKind,
let approvalId = self.approvalID(from: userInfo)
else {
return false
}
await self.removeNotifications(forApprovalID: approvalId, notificationCenter: notificationCenter)
return true
}
@MainActor
static func removeNotifications(
forApprovalID approvalId: String,
notificationCenter: NotificationCentering
) async {
let normalizedID = approvalId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalizedID.isEmpty else { return }
await notificationCenter.removePendingNotificationRequests(
withIdentifiers: [self.localRequestIdentifier(for: normalizedID)])
let delivered = await notificationCenter.deliveredNotifications()
let identifiers = delivered.compactMap { snapshot -> String? in
guard self.approvalID(from: snapshot.userInfo) == normalizedID else { return nil }
return snapshot.identifier
}
await notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers)
}
static func approvalID(from userInfo: [AnyHashable: Any]) -> String? {
let raw = self.openClawPayload(userInfo: userInfo)?["approvalId"] as? String
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}
private static func localRequestIdentifier(for approvalId: String) -> String {
"\(self.localRequestPrefix)\(approvalId)"
}
static func payloadKind(userInfo: [AnyHashable: Any]) -> String {
let raw = self.openClawPayload(userInfo: userInfo)?["kind"] as? String
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? "unknown" : trimmed
}
private static func openClawPayload(userInfo: [AnyHashable: Any]) -> [String: Any]? {
if let payload = userInfo["openclaw"] as? [String: Any] {
return payload
}
if let payload = userInfo["openclaw"] as? [AnyHashable: Any] {
return payload.reduce(into: [String: Any]()) { partialResult, pair in
guard let key = pair.key as? String else { return }
partialResult[key] = pair.value
}
}
return nil
}
}

View File

@@ -107,6 +107,7 @@ struct RootCanvas: View {
}
.gatewayTrustPromptAlert()
.deepLinkAgentPromptAlert()
.execApprovalPromptDialog()
.sheet(item: self.$presentedSheet) { sheet in
switch sheet {
case .settings:

View File

@@ -88,6 +88,20 @@ struct WatchQuickReplyEvent: Sendable, Equatable {
var transport: String
}
struct WatchExecApprovalResolveEvent: Sendable, Equatable {
var replyId: String
var approvalId: String
var decision: OpenClawWatchExecApprovalDecision
var sentAtMs: Int?
var transport: String
}
struct WatchExecApprovalSnapshotRequestEvent: Sendable, Equatable {
var requestId: String
var sentAtMs: Int?
var transport: String
}
struct WatchNotificationSendResult: Sendable, Equatable {
var deliveredImmediately: Bool
var queuedForDelivery: Bool
@@ -96,10 +110,22 @@ struct WatchNotificationSendResult: Sendable, Equatable {
protocol WatchMessagingServicing: AnyObject, Sendable {
func status() async -> WatchMessagingStatus
func setStatusHandler(_ handler: (@Sendable (WatchMessagingStatus) -> Void)?)
func setReplyHandler(_ handler: (@Sendable (WatchQuickReplyEvent) -> Void)?)
func setExecApprovalResolveHandler(_ handler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?)
func setExecApprovalSnapshotRequestHandler(
_ handler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?)
func sendNotification(
id: String,
params: OpenClawWatchNotifyParams) async throws -> WatchNotificationSendResult
func sendExecApprovalPrompt(
_ message: OpenClawWatchExecApprovalPromptMessage) async throws -> WatchNotificationSendResult
func sendExecApprovalResolved(
_ message: OpenClawWatchExecApprovalResolvedMessage) async throws -> WatchNotificationSendResult
func sendExecApprovalExpired(
_ message: OpenClawWatchExecApprovalExpiredMessage) async throws -> WatchNotificationSendResult
func syncExecApprovalSnapshot(
_ message: OpenClawWatchExecApprovalSnapshotMessage) async throws -> WatchNotificationSendResult
}
extension CameraController: CameraServicing {}

View File

@@ -1,6 +1,11 @@
import Foundation
import UserNotifications
struct NotificationSnapshot: @unchecked Sendable {
let identifier: String
let userInfo: [AnyHashable: Any]
}
enum NotificationAuthorizationStatus: Sendable {
case notDetermined
case denied
@@ -13,6 +18,9 @@ protocol NotificationCentering: Sendable {
func authorizationStatus() async -> NotificationAuthorizationStatus
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool
func add(_ request: UNNotificationRequest) async throws
func removePendingNotificationRequests(withIdentifiers identifiers: [String]) async
func removeDeliveredNotifications(withIdentifiers identifiers: [String]) async
func deliveredNotifications() async -> [NotificationSnapshot]
}
struct LiveNotificationCenter: NotificationCentering, @unchecked Sendable {
@@ -55,4 +63,27 @@ struct LiveNotificationCenter: NotificationCentering, @unchecked Sendable {
}
}
}
func removePendingNotificationRequests(withIdentifiers identifiers: [String]) async {
guard !identifiers.isEmpty else { return }
self.center.removePendingNotificationRequests(withIdentifiers: identifiers)
}
func removeDeliveredNotifications(withIdentifiers identifiers: [String]) async {
guard !identifiers.isEmpty else { return }
self.center.removeDeliveredNotifications(withIdentifiers: identifiers)
}
func deliveredNotifications() async -> [NotificationSnapshot] {
await withCheckedContinuation { continuation in
self.center.getDeliveredNotifications { notifications in
continuation.resume(
returning: notifications.map { notification in
NotificationSnapshot(
identifier: notification.request.identifier,
userInfo: notification.request.content.userInfo)
})
}
}
}
}

View File

@@ -0,0 +1,363 @@
import Foundation
import OSLog
@preconcurrency import WatchConnectivity
private struct WatchConnectivityTransportCallbacks {
var statusUpdateHandler: (@Sendable (WatchMessagingStatus) -> Void)?
var replyHandler: (@Sendable (WatchQuickReplyEvent) -> Void)?
var execApprovalResolveHandler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?
var execApprovalSnapshotRequestHandler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?
}
private func sendReachableWatchMessage(_ payload: [String: Any], with session: WCSession) async throws {
// WatchConnectivity replies arrive on its own queue. Keep this continuation explicitly
// nonisolated so Swift 6 does not inherit a caller actor (for example MainActor) into the
// Objective-C callback boundary and trap on the reply callback executor check.
try await withCheckedThrowingContinuation(isolation: nil) {
(continuation: CheckedContinuation<Void, Error>) in
session.sendMessage(
payload,
replyHandler: { _ in
continuation.resume(returning: ())
},
errorHandler: { error in
continuation.resume(throwing: error)
}
)
}
}
final class WatchConnectivityTransport: NSObject, @unchecked Sendable {
nonisolated private static let logger = Logger(subsystem: "ai.openclaw", category: "watch.messaging")
private let session: WCSession?
private let callbacksLock = NSLock()
private var callbacks = WatchConnectivityTransportCallbacks()
override init() {
if WCSession.isSupported() {
self.session = WCSession.default
} else {
self.session = nil
}
super.init()
if let session = self.session {
session.delegate = self
session.activate()
}
}
nonisolated static func isSupportedOnDevice() -> Bool {
WCSession.isSupported()
}
nonisolated static func currentStatusSnapshot() -> WatchMessagingStatus {
guard WCSession.isSupported() else {
return WatchMessagingStatus(
supported: false,
paired: false,
appInstalled: false,
reachable: false,
activationState: "unsupported")
}
return self.status(for: WCSession.default)
}
func status() async -> WatchMessagingStatus {
await self.ensureActivated()
return self.currentStatusSnapshot()
}
func currentStatusSnapshot() -> WatchMessagingStatus {
guard let session = self.session else {
return WatchMessagingStatus(
supported: false,
paired: false,
appInstalled: false,
reachable: false,
activationState: "unsupported")
}
return Self.status(for: session)
}
func setStatusUpdateHandler(_ handler: (@Sendable (WatchMessagingStatus) -> Void)?) {
self.updateCallbacks { $0.statusUpdateHandler = handler }
}
func setReplyHandler(_ handler: (@Sendable (WatchQuickReplyEvent) -> Void)?) {
self.updateCallbacks { $0.replyHandler = handler }
}
func setExecApprovalResolveHandler(_ handler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?) {
self.updateCallbacks { $0.execApprovalResolveHandler = handler }
}
func setExecApprovalSnapshotRequestHandler(
_ handler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?)
{
self.updateCallbacks { $0.execApprovalSnapshotRequestHandler = handler }
}
func sendPayload(_ payload: [String: Any]) async throws -> WatchNotificationSendResult {
await self.ensureActivated()
let session = try self.requireReadySession()
if session.isReachable {
do {
try await sendReachableWatchMessage(payload, with: session)
return WatchNotificationSendResult(
deliveredImmediately: true,
queuedForDelivery: false,
transport: "sendMessage")
} catch {
Self.logger.error("watch sendMessage failed: \(error.localizedDescription, privacy: .public)")
}
}
_ = session.transferUserInfo(payload)
return WatchNotificationSendResult(
deliveredImmediately: false,
queuedForDelivery: true,
transport: "transferUserInfo")
}
func sendSnapshotPayload(_ payload: [String: Any]) async throws -> WatchNotificationSendResult {
await self.ensureActivated()
let session = try self.requireReadySession()
if session.isReachable {
do {
try await sendReachableWatchMessage(payload, with: session)
return WatchNotificationSendResult(
deliveredImmediately: true,
queuedForDelivery: false,
transport: "sendMessage")
} catch {
Self.logger.error(
"watch snapshot sendMessage failed: \(error.localizedDescription, privacy: .public)")
}
}
do {
try session.updateApplicationContext(payload)
return WatchNotificationSendResult(
deliveredImmediately: false,
queuedForDelivery: true,
transport: "applicationContext")
} catch {
Self.logger.error(
"watch updateApplicationContext failed: \(error.localizedDescription, privacy: .public)")
_ = session.transferUserInfo(payload)
return WatchNotificationSendResult(
deliveredImmediately: false,
queuedForDelivery: true,
transport: "transferUserInfo")
}
}
private func updateCallbacks(_ update: (inout WatchConnectivityTransportCallbacks) -> Void) {
self.callbacksLock.lock()
defer { self.callbacksLock.unlock() }
update(&self.callbacks)
}
private func callbacksSnapshot() -> WatchConnectivityTransportCallbacks {
self.callbacksLock.lock()
defer { self.callbacksLock.unlock() }
return self.callbacks
}
private func requireReadySession() throws -> WCSession {
guard let session = self.session else {
throw WatchMessagingError.unsupported
}
let snapshot = Self.status(for: session)
guard snapshot.paired else {
throw WatchMessagingError.notPaired
}
guard snapshot.appInstalled else {
throw WatchMessagingError.watchAppNotInstalled
}
return session
}
private func ensureActivated() async {
guard let session = self.session else { return }
if session.activationState == .activated {
return
}
session.activate()
for _ in 0..<8 {
if session.activationState == .activated {
return
}
try? await Task.sleep(nanoseconds: 100_000_000)
}
}
private func emitStatusUpdate(_ snapshot: WatchMessagingStatus) {
guard let handler = self.callbacksSnapshot().statusUpdateHandler else {
return
}
Task { @MainActor in
handler(snapshot)
}
}
private func emitReply(_ event: WatchQuickReplyEvent) {
guard let handler = self.callbacksSnapshot().replyHandler else {
return
}
Task { @MainActor in
handler(event)
}
}
private func emitExecApprovalResolve(_ event: WatchExecApprovalResolveEvent) {
guard let handler = self.callbacksSnapshot().execApprovalResolveHandler else {
return
}
Task { @MainActor in
handler(event)
}
}
private func emitExecApprovalSnapshotRequest(_ event: WatchExecApprovalSnapshotRequestEvent) {
guard let handler = self.callbacksSnapshot().execApprovalSnapshotRequestHandler else {
return
}
Task { @MainActor in
handler(event)
}
}
nonisolated private static func status(for session: WCSession) -> WatchMessagingStatus {
WatchMessagingStatus(
supported: true,
paired: session.isPaired,
appInstalled: session.isWatchAppInstalled,
reachable: session.isReachable,
activationState: self.activationStateLabel(session.activationState))
}
nonisolated private static func activationStateLabel(_ state: WCSessionActivationState) -> String {
switch state {
case .notActivated:
"notActivated"
case .inactive:
"inactive"
case .activated:
"activated"
@unknown default:
"unknown"
}
}
}
extension WatchConnectivityTransport: WCSessionDelegate {
func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: (any Error)?)
{
GatewayDiagnostics.log(
"watch messaging: activation complete state=\(Self.activationStateLabel(activationState)) error=\(error?.localizedDescription ?? "none")")
if let error {
Self.logger.error("watch activation failed: \(error.localizedDescription, privacy: .public)")
} else {
Self.logger.debug(
"watch activation state=\(Self.activationStateLabel(activationState), privacy: .public)")
}
self.emitStatusUpdate(Self.status(for: session))
}
func sessionDidBecomeInactive(_: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) {
GatewayDiagnostics.log("watch messaging: session did deactivate; reactivating")
session.activate()
self.emitStatusUpdate(Self.status(for: session))
}
func session(_: WCSession, didReceiveMessage message: [String: Any]) {
let type = (message["type"] as? String) ?? "unknown"
GatewayDiagnostics.log("watch messaging: didReceiveMessage type=\(type)")
if let event = WatchMessagingPayloadCodec.parseQuickReplyPayload(message, transport: "sendMessage") {
self.emitReply(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalResolvePayload(
message,
transport: "sendMessage")
{
self.emitExecApprovalResolve(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalSnapshotRequestPayload(
message,
transport: "sendMessage")
{
self.emitExecApprovalSnapshotRequest(event)
}
}
func session(
_: WCSession,
didReceiveMessage message: [String: Any],
replyHandler: @escaping ([String: Any]) -> Void)
{
let type = (message["type"] as? String) ?? "unknown"
GatewayDiagnostics.log("watch messaging: didReceiveMessageWithReply type=\(type)")
if let event = WatchMessagingPayloadCodec.parseQuickReplyPayload(message, transport: "sendMessage") {
replyHandler(["ok": true])
self.emitReply(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalResolvePayload(
message,
transport: "sendMessage")
{
replyHandler(["ok": true])
self.emitExecApprovalResolve(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalSnapshotRequestPayload(
message,
transport: "sendMessage")
{
replyHandler(["ok": true])
self.emitExecApprovalSnapshotRequest(event)
return
}
replyHandler(["ok": false, "error": "unsupported_payload"])
}
func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
let type = (userInfo["type"] as? String) ?? "unknown"
GatewayDiagnostics.log("watch messaging: didReceiveUserInfo type=\(type)")
if let event = WatchMessagingPayloadCodec.parseQuickReplyPayload(
userInfo,
transport: "transferUserInfo")
{
self.emitReply(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalResolvePayload(
userInfo,
transport: "transferUserInfo")
{
self.emitExecApprovalResolve(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalSnapshotRequestPayload(
userInfo,
transport: "transferUserInfo")
{
self.emitExecApprovalSnapshotRequest(event)
}
}
func sessionReachabilityDidChange(_ session: WCSession) {
GatewayDiagnostics.log(
"watch messaging: reachability changed reachable=\(session.isReachable) paired=\(session.isPaired) installed=\(session.isWatchAppInstalled)")
self.emitStatusUpdate(Self.status(for: session))
}
}

View File

@@ -0,0 +1,219 @@
import Foundation
import OpenClawKit
enum WatchMessagingPayloadCodec {
static func nowMs() -> Int {
Int(Date().timeIntervalSince1970 * 1000)
}
static func nonEmpty(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}
static func encodeNotificationPayload(
id: String,
params: OpenClawWatchNotifyParams) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.notify.rawValue,
"id": id,
"title": params.title,
"body": params.body,
"priority": params.priority?.rawValue ?? OpenClawNotificationPriority.active.rawValue,
"sentAtMs": nowMs(),
]
if let promptId = nonEmpty(params.promptId) {
payload["promptId"] = promptId
}
if let sessionKey = nonEmpty(params.sessionKey) {
payload["sessionKey"] = sessionKey
}
if let kind = nonEmpty(params.kind) {
payload["kind"] = kind
}
if let details = nonEmpty(params.details) {
payload["details"] = details
}
if let expiresAtMs = params.expiresAtMs {
payload["expiresAtMs"] = expiresAtMs
}
if let risk = params.risk {
payload["risk"] = risk.rawValue
}
if let actions = params.actions, !actions.isEmpty {
payload["actions"] = actions.map { action in
var encoded: [String: Any] = [
"id": action.id,
"label": action.label,
]
if let style = nonEmpty(action.style) {
encoded["style"] = style
}
return encoded
}
}
return payload
}
static func encodeExecApprovalItem(_ item: OpenClawWatchExecApprovalItem) -> [String: Any] {
var payload: [String: Any] = [
"id": item.id,
"commandText": item.commandText,
"allowedDecisions": item.allowedDecisions.map(\.rawValue),
]
if let commandPreview = nonEmpty(item.commandPreview) {
payload["commandPreview"] = commandPreview
}
if let host = nonEmpty(item.host) {
payload["host"] = host
}
if let nodeId = nonEmpty(item.nodeId) {
payload["nodeId"] = nodeId
}
if let agentId = nonEmpty(item.agentId) {
payload["agentId"] = agentId
}
if let expiresAtMs = item.expiresAtMs {
payload["expiresAtMs"] = expiresAtMs
}
if let risk = item.risk {
payload["risk"] = risk.rawValue
}
return payload
}
static func encodeExecApprovalPromptPayload(
_ message: OpenClawWatchExecApprovalPromptMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.execApprovalPrompt.rawValue,
"approval": encodeExecApprovalItem(message.approval),
]
if let sentAtMs = message.sentAtMs {
payload["sentAtMs"] = sentAtMs
}
if let deliveryId = nonEmpty(message.deliveryId) {
payload["deliveryId"] = deliveryId
}
if message.resetResolvingState == true {
payload["resetResolvingState"] = true
}
return payload
}
static func encodeExecApprovalResolvedPayload(
_ message: OpenClawWatchExecApprovalResolvedMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.execApprovalResolved.rawValue,
"approvalId": message.approvalId,
]
if let decision = message.decision {
payload["decision"] = decision.rawValue
}
if let resolvedAtMs = message.resolvedAtMs {
payload["resolvedAtMs"] = resolvedAtMs
}
if let source = nonEmpty(message.source) {
payload["source"] = source
}
return payload
}
static func encodeExecApprovalExpiredPayload(
_ message: OpenClawWatchExecApprovalExpiredMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.execApprovalExpired.rawValue,
"approvalId": message.approvalId,
"reason": message.reason.rawValue,
]
if let expiredAtMs = message.expiredAtMs {
payload["expiredAtMs"] = expiredAtMs
}
return payload
}
static func encodeExecApprovalSnapshotPayload(
_ message: OpenClawWatchExecApprovalSnapshotMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.execApprovalSnapshot.rawValue,
"approvals": message.approvals.map(encodeExecApprovalItem),
]
if let sentAtMs = message.sentAtMs {
payload["sentAtMs"] = sentAtMs
}
if let snapshotId = nonEmpty(message.snapshotId) {
payload["snapshotId"] = snapshotId
}
return payload
}
static func parseQuickReplyPayload(
_ payload: [String: Any],
transport: String) -> WatchQuickReplyEvent?
{
guard (payload["type"] as? String) == OpenClawWatchPayloadType.reply.rawValue else {
return nil
}
guard let actionId = nonEmpty(payload["actionId"] as? String) else {
return nil
}
let promptId = nonEmpty(payload["promptId"] as? String) ?? "unknown"
let replyId = nonEmpty(payload["replyId"] as? String) ?? UUID().uuidString
let actionLabel = nonEmpty(payload["actionLabel"] as? String)
let sessionKey = nonEmpty(payload["sessionKey"] as? String)
let note = nonEmpty(payload["note"] as? String)
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchQuickReplyEvent(
replyId: replyId,
promptId: promptId,
actionId: actionId,
actionLabel: actionLabel,
sessionKey: sessionKey,
note: note,
sentAtMs: sentAtMs,
transport: transport)
}
static func parseExecApprovalResolvePayload(
_ payload: [String: Any],
transport: String) -> WatchExecApprovalResolveEvent?
{
guard (payload["type"] as? String) == OpenClawWatchPayloadType.execApprovalResolve.rawValue else {
return nil
}
guard let approvalId = nonEmpty(payload["approvalId"] as? String),
let rawDecision = nonEmpty(payload["decision"] as? String),
let decision = OpenClawWatchExecApprovalDecision(rawValue: rawDecision)
else {
return nil
}
let replyId = nonEmpty(payload["replyId"] as? String) ?? UUID().uuidString
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchExecApprovalResolveEvent(
replyId: replyId,
approvalId: approvalId,
decision: decision,
sentAtMs: sentAtMs,
transport: transport)
}
static func parseExecApprovalSnapshotRequestPayload(
_ payload: [String: Any],
transport: String) -> WatchExecApprovalSnapshotRequestEvent?
{
guard (payload["type"] as? String) == OpenClawWatchPayloadType.execApprovalSnapshotRequest.rawValue else {
return nil
}
let requestId = nonEmpty(payload["requestId"] as? String) ?? UUID().uuidString
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchExecApprovalSnapshotRequestEvent(
requestId: requestId,
sentAtMs: sentAtMs,
transport: transport)
}
}

View File

@@ -1,7 +1,5 @@
import Foundation
import OpenClawKit
import OSLog
@preconcurrency import WatchConnectivity
enum WatchMessagingError: LocalizedError {
case unsupported
@@ -21,272 +19,136 @@ enum WatchMessagingError: LocalizedError {
}
@MainActor
final class WatchMessagingService: NSObject, @preconcurrency WatchMessagingServicing {
nonisolated private static let logger = Logger(subsystem: "ai.openclaw", category: "watch.messaging")
private let session: WCSession?
private var pendingActivationContinuations: [CheckedContinuation<Void, Never>] = []
final class WatchMessagingService: @preconcurrency WatchMessagingServicing {
private let transport: WatchConnectivityTransport
private var statusHandler: (@Sendable (WatchMessagingStatus) -> Void)?
private var lastEmittedStatus: WatchMessagingStatus?
private var replyHandler: (@Sendable (WatchQuickReplyEvent) -> Void)?
private var execApprovalResolveHandler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?
private var execApprovalSnapshotRequestHandler: (
@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?
override init() {
if WCSession.isSupported() {
self.session = WCSession.default
} else {
self.session = nil
init(transport: WatchConnectivityTransport = WatchConnectivityTransport()) {
self.transport = transport
self.transport.setStatusUpdateHandler { [weak self] snapshot in
Task { @MainActor [weak self] in
self?.emitStatusIfChanged(snapshot)
}
}
super.init()
if let session = self.session {
session.delegate = self
session.activate()
self.transport.setReplyHandler { [weak self] event in
Task { @MainActor [weak self] in
self?.emitReply(event)
}
}
self.transport.setExecApprovalResolveHandler { [weak self] event in
Task { @MainActor [weak self] in
self?.emitExecApprovalResolve(event)
}
}
self.transport.setExecApprovalSnapshotRequestHandler { [weak self] event in
Task { @MainActor [weak self] in
self?.emitExecApprovalSnapshotRequest(event)
}
}
}
nonisolated static func isSupportedOnDevice() -> Bool {
WCSession.isSupported()
WatchConnectivityTransport.isSupportedOnDevice()
}
nonisolated static func currentStatusSnapshot() -> WatchMessagingStatus {
guard WCSession.isSupported() else {
return WatchMessagingStatus(
supported: false,
paired: false,
appInstalled: false,
reachable: false,
activationState: "unsupported")
}
let session = WCSession.default
return status(for: session)
WatchConnectivityTransport.currentStatusSnapshot()
}
func status() async -> WatchMessagingStatus {
await self.ensureActivated()
guard let session = self.session else {
return WatchMessagingStatus(
supported: false,
paired: false,
appInstalled: false,
reachable: false,
activationState: "unsupported")
await self.transport.status()
}
func setStatusHandler(_ handler: (@Sendable (WatchMessagingStatus) -> Void)?) {
self.statusHandler = handler
guard let handler else {
self.lastEmittedStatus = nil
GatewayDiagnostics.log("watch messaging: cleared status handler")
return
}
return Self.status(for: session)
let snapshot = self.transport.currentStatusSnapshot()
self.lastEmittedStatus = snapshot
GatewayDiagnostics.log(
"watch messaging: set status handler supported=\(snapshot.supported) paired=\(snapshot.paired) appInstalled=\(snapshot.appInstalled) reachable=\(snapshot.reachable) activation=\(snapshot.activationState)")
handler(snapshot)
}
func setReplyHandler(_ handler: (@Sendable (WatchQuickReplyEvent) -> Void)?) {
self.replyHandler = handler
}
func setExecApprovalResolveHandler(_ handler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?) {
self.execApprovalResolveHandler = handler
}
func setExecApprovalSnapshotRequestHandler(
_ handler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?)
{
self.execApprovalSnapshotRequestHandler = handler
}
func sendNotification(
id: String,
params: OpenClawWatchNotifyParams) async throws -> WatchNotificationSendResult
{
await self.ensureActivated()
guard let session = self.session else {
throw WatchMessagingError.unsupported
}
let snapshot = Self.status(for: session)
guard snapshot.paired else { throw WatchMessagingError.notPaired }
guard snapshot.appInstalled else { throw WatchMessagingError.watchAppNotInstalled }
var payload: [String: Any] = [
"type": "watch.notify",
"id": id,
"title": params.title,
"body": params.body,
"priority": params.priority?.rawValue ?? OpenClawNotificationPriority.active.rawValue,
"sentAtMs": Int(Date().timeIntervalSince1970 * 1000),
]
if let promptId = Self.nonEmpty(params.promptId) {
payload["promptId"] = promptId
}
if let sessionKey = Self.nonEmpty(params.sessionKey) {
payload["sessionKey"] = sessionKey
}
if let kind = Self.nonEmpty(params.kind) {
payload["kind"] = kind
}
if let details = Self.nonEmpty(params.details) {
payload["details"] = details
}
if let expiresAtMs = params.expiresAtMs {
payload["expiresAtMs"] = expiresAtMs
}
if let risk = params.risk {
payload["risk"] = risk.rawValue
}
if let actions = params.actions, !actions.isEmpty {
payload["actions"] = actions.map { action in
var encoded: [String: Any] = [
"id": action.id,
"label": action.label,
]
if let style = Self.nonEmpty(action.style) {
encoded["style"] = style
}
return encoded
}
}
if snapshot.reachable {
do {
try await self.sendReachableMessage(payload, with: session)
return WatchNotificationSendResult(
deliveredImmediately: true,
queuedForDelivery: false,
transport: "sendMessage")
} catch {
Self.logger.error("watch sendMessage failed: \(error.localizedDescription, privacy: .public)")
}
}
_ = session.transferUserInfo(payload)
return WatchNotificationSendResult(
deliveredImmediately: false,
queuedForDelivery: true,
transport: "transferUserInfo")
let payload = WatchMessagingPayloadCodec.encodeNotificationPayload(id: id, params: params)
return try await self.transport.sendPayload(payload)
}
private func sendReachableMessage(_ payload: [String: Any], with session: WCSession) async throws {
try await withCheckedThrowingContinuation { continuation in
session.sendMessage(
payload,
replyHandler: { _ in
continuation.resume()
},
errorHandler: { error in
continuation.resume(throwing: error)
}
)
func sendExecApprovalPrompt(
_ message: OpenClawWatchExecApprovalPromptMessage) async throws -> WatchNotificationSendResult
{
try await self.transport.sendPayload(
WatchMessagingPayloadCodec.encodeExecApprovalPromptPayload(message))
}
func sendExecApprovalResolved(
_ message: OpenClawWatchExecApprovalResolvedMessage) async throws -> WatchNotificationSendResult
{
try await self.transport.sendPayload(
WatchMessagingPayloadCodec.encodeExecApprovalResolvedPayload(message))
}
func sendExecApprovalExpired(
_ message: OpenClawWatchExecApprovalExpiredMessage) async throws -> WatchNotificationSendResult
{
try await self.transport.sendPayload(
WatchMessagingPayloadCodec.encodeExecApprovalExpiredPayload(message))
}
func syncExecApprovalSnapshot(
_ message: OpenClawWatchExecApprovalSnapshotMessage) async throws -> WatchNotificationSendResult
{
try await self.transport.sendSnapshotPayload(
WatchMessagingPayloadCodec.encodeExecApprovalSnapshotPayload(message))
}
private func emitStatusIfChanged(_ snapshot: WatchMessagingStatus) {
guard snapshot != self.lastEmittedStatus else {
return
}
self.lastEmittedStatus = snapshot
GatewayDiagnostics.log(
"watch messaging: status supported=\(snapshot.supported) paired=\(snapshot.paired) appInstalled=\(snapshot.appInstalled) reachable=\(snapshot.reachable) activation=\(snapshot.activationState)")
self.statusHandler?(snapshot)
}
private func emitReply(_ event: WatchQuickReplyEvent) {
self.replyHandler?(event)
}
nonisolated private static func nonEmpty(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
private func emitExecApprovalResolve(_ event: WatchExecApprovalResolveEvent) {
self.execApprovalResolveHandler?(event)
}
nonisolated private static func parseQuickReplyPayload(
_ payload: [String: Any],
transport: String) -> WatchQuickReplyEvent?
{
guard (payload["type"] as? String) == "watch.reply" else {
return nil
}
guard let actionId = nonEmpty(payload["actionId"] as? String) else {
return nil
}
let promptId = nonEmpty(payload["promptId"] as? String) ?? "unknown"
let replyId = nonEmpty(payload["replyId"] as? String) ?? UUID().uuidString
let actionLabel = nonEmpty(payload["actionLabel"] as? String)
let sessionKey = nonEmpty(payload["sessionKey"] as? String)
let note = nonEmpty(payload["note"] as? String)
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchQuickReplyEvent(
replyId: replyId,
promptId: promptId,
actionId: actionId,
actionLabel: actionLabel,
sessionKey: sessionKey,
note: note,
sentAtMs: sentAtMs,
transport: transport)
}
private func ensureActivated() async {
guard let session = self.session else { return }
if session.activationState == .activated { return }
session.activate()
await withCheckedContinuation { continuation in
self.pendingActivationContinuations.append(continuation)
}
}
nonisolated private static func status(for session: WCSession) -> WatchMessagingStatus {
WatchMessagingStatus(
supported: true,
paired: session.isPaired,
appInstalled: session.isWatchAppInstalled,
reachable: session.isReachable,
activationState: activationStateLabel(session.activationState))
}
nonisolated private static func activationStateLabel(_ state: WCSessionActivationState) -> String {
switch state {
case .notActivated:
"notActivated"
case .inactive:
"inactive"
case .activated:
"activated"
@unknown default:
"unknown"
}
private func emitExecApprovalSnapshotRequest(_ event: WatchExecApprovalSnapshotRequestEvent) {
GatewayDiagnostics.log(
"watch messaging: snapshot request id=\(event.requestId) transport=\(event.transport) sentAtMs=\(event.sentAtMs ?? -1)")
self.execApprovalSnapshotRequestHandler?(event)
}
}
extension WatchMessagingService: WCSessionDelegate {
nonisolated func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: (any Error)?)
{
if let error {
Self.logger.error("watch activation failed: \(error.localizedDescription, privacy: .public)")
} else {
Self.logger.debug("watch activation state=\(Self.activationStateLabel(activationState), privacy: .public)")
}
// Always resume all waiters so callers never hang, even on error.
Task { @MainActor in
let waiters = self.pendingActivationContinuations
self.pendingActivationContinuations.removeAll()
for continuation in waiters {
continuation.resume()
}
}
}
nonisolated func sessionDidBecomeInactive(_ session: WCSession) {}
nonisolated func sessionDidDeactivate(_ session: WCSession) {
session.activate()
}
nonisolated func session(_: WCSession, didReceiveMessage message: [String: Any]) {
guard let event = Self.parseQuickReplyPayload(message, transport: "sendMessage") else {
return
}
Task { @MainActor in
self.emitReply(event)
}
}
nonisolated func session(
_: WCSession,
didReceiveMessage message: [String: Any],
replyHandler: @escaping ([String: Any]) -> Void)
{
guard let event = Self.parseQuickReplyPayload(message, transport: "sendMessage") else {
replyHandler(["ok": false, "error": "unsupported_payload"])
return
}
replyHandler(["ok": true])
Task { @MainActor in
self.emitReply(event)
}
}
nonisolated func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
guard let event = Self.parseQuickReplyPayload(userInfo, transport: "transferUserInfo") else {
return
}
Task { @MainActor in
self.emitReply(event)
}
}
nonisolated func sessionReachabilityDidChange(_ session: WCSession) {}
}

View File

@@ -0,0 +1,112 @@
import Foundation
import Testing
import UserNotifications
@testable import OpenClaw
private final class MockNotificationCenter: NotificationCentering, @unchecked Sendable {
var authorization: NotificationAuthorizationStatus = .authorized
var addedRequests: [UNNotificationRequest] = []
var pendingRemovedIdentifiers: [[String]] = []
var deliveredRemovedIdentifiers: [[String]] = []
var delivered: [NotificationSnapshot] = []
func authorizationStatus() async -> NotificationAuthorizationStatus {
self.authorization
}
func requestAuthorization(options _: UNAuthorizationOptions) async throws -> Bool {
true
}
func add(_ request: UNNotificationRequest) async throws {
self.addedRequests.append(request)
}
func removePendingNotificationRequests(withIdentifiers identifiers: [String]) async {
self.pendingRemovedIdentifiers.append(identifiers)
}
func removeDeliveredNotifications(withIdentifiers identifiers: [String]) async {
self.deliveredRemovedIdentifiers.append(identifiers)
}
func deliveredNotifications() async -> [NotificationSnapshot] {
self.delivered
}
}
@Suite(.serialized) struct ExecApprovalNotificationBridgeTests {
@Test func parsePromptMapsDefaultNotificationTap() {
let prompt = ExecApprovalNotificationBridge.parsePrompt(
actionIdentifier: UNNotificationDefaultActionIdentifier,
userInfo: [
"openclaw": [
"kind": ExecApprovalNotificationBridge.requestedKind,
"approvalId": "approval-123",
],
])
#expect(prompt == ExecApprovalNotificationPrompt(approvalId: "approval-123"))
}
@Test func parsePromptMapsReviewAction() {
let prompt = ExecApprovalNotificationBridge.parsePrompt(
actionIdentifier: ExecApprovalNotificationBridge.reviewActionIdentifier,
userInfo: [
"openclaw": [
"kind": ExecApprovalNotificationBridge.requestedKind,
"approvalId": "approval-456",
],
])
#expect(prompt == ExecApprovalNotificationPrompt(approvalId: "approval-456"))
}
@Test func parsePromptIgnoresUnexpectedActionIdentifiers() {
let prompt = ExecApprovalNotificationBridge.parsePrompt(
actionIdentifier: "openclaw.exec-approval.allow-once",
userInfo: [
"openclaw": [
"kind": ExecApprovalNotificationBridge.requestedKind,
"approvalId": "approval-789",
],
])
#expect(prompt == nil)
}
@Test @MainActor func handleResolvedPushRemovesMatchingNotifications() async {
let center = MockNotificationCenter()
center.delivered = [
NotificationSnapshot(
identifier: "remote-approval-1",
userInfo: [
"openclaw": [
"kind": ExecApprovalNotificationBridge.requestedKind,
"approvalId": "approval-123",
],
]),
NotificationSnapshot(
identifier: "remote-other",
userInfo: [
"openclaw": [
"kind": ExecApprovalNotificationBridge.requestedKind,
"approvalId": "approval-999",
],
]),
]
let handled = await ExecApprovalNotificationBridge.handleResolvedPushIfNeeded(
userInfo: [
"openclaw": [
"kind": ExecApprovalNotificationBridge.resolvedKind,
"approvalId": "approval-123",
],
],
notificationCenter: center)
#expect(handled)
#expect(center.pendingRemovedIdentifiers == [["exec.approval.approval-123"]])
#expect(center.deliveredRemovedIdentifiers == [["remote-approval-1"]])
}
}

View File

@@ -70,6 +70,52 @@ import UIKit
}
}
@Test @MainActor func operatorConnectOptionsOnlyRequestApprovalScopeWhenEnabled() {
let appModel = NodeAppModel()
let withoutApprovalScope = appModel._test_makeOperatorConnectOptions(
clientId: "openclaw-ios",
displayName: "OpenClaw iOS",
includeApprovalScope: false)
let withApprovalScope = appModel._test_makeOperatorConnectOptions(
clientId: "openclaw-ios",
displayName: "OpenClaw iOS",
includeApprovalScope: true)
#expect(withoutApprovalScope.role == "operator")
#expect(withoutApprovalScope.scopes.contains("operator.read"))
#expect(withoutApprovalScope.scopes.contains("operator.write"))
#expect(!withoutApprovalScope.scopes.contains("operator.approvals"))
#expect(withoutApprovalScope.scopes.contains("operator.talk.secrets"))
#expect(withApprovalScope.scopes.contains("operator.approvals"))
}
@Test func operatorApprovalScopeRequestsStayBackwardCompatible() {
#expect(
!NodeAppModel._test_shouldRequestOperatorApprovalScope(
token: nil,
password: nil,
storedOperatorScopes: ["operator.read", "operator.write", "operator.talk.secrets"])
)
#expect(
NodeAppModel._test_shouldRequestOperatorApprovalScope(
token: nil,
password: nil,
storedOperatorScopes: [
"operator.approvals",
"operator.read",
"operator.write",
"operator.talk.secrets",
])
)
#expect(
NodeAppModel._test_shouldRequestOperatorApprovalScope(
token: "shared-token",
password: nil,
storedOperatorScopes: [])
)
}
@Test @MainActor func loadLastConnectionReadsSavedValues() {
let prior = KeychainStore.loadString(service: "ai.openclaw.gateway", account: "lastConnection")
defer {

View File

@@ -2,6 +2,7 @@ import OpenClawKit
import Foundation
import Testing
import UIKit
import UserNotifications
@testable import OpenClaw
private func makeAgentDeepLinkURL(
@@ -45,16 +46,37 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
transport: "sendMessage")
var sendError: Error?
var lastSent: (id: String, params: OpenClawWatchNotifyParams)?
var lastSentExecApprovalPrompt: OpenClawWatchExecApprovalPromptMessage?
var lastSentExecApprovalResolved: OpenClawWatchExecApprovalResolvedMessage?
var lastSentExecApprovalExpired: OpenClawWatchExecApprovalExpiredMessage?
var lastSentExecApprovalSnapshot: OpenClawWatchExecApprovalSnapshotMessage?
private var statusHandler: (@Sendable (WatchMessagingStatus) -> Void)?
private var replyHandler: (@Sendable (WatchQuickReplyEvent) -> Void)?
private var execApprovalResolveHandler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?
private var execApprovalSnapshotRequestHandler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?
func status() async -> WatchMessagingStatus {
self.currentStatus
}
func setStatusHandler(_ handler: (@Sendable (WatchMessagingStatus) -> Void)?) {
self.statusHandler = handler
}
func setReplyHandler(_ handler: (@Sendable (WatchQuickReplyEvent) -> Void)?) {
self.replyHandler = handler
}
func setExecApprovalResolveHandler(_ handler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?) {
self.execApprovalResolveHandler = handler
}
func setExecApprovalSnapshotRequestHandler(
_ handler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?)
{
self.execApprovalSnapshotRequestHandler = handler
}
func sendNotification(id: String, params: OpenClawWatchNotifyParams) async throws -> WatchNotificationSendResult {
self.lastSent = (id: id, params: params)
if let sendError = self.sendError {
@@ -63,9 +85,87 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
return self.nextSendResult
}
func sendExecApprovalPrompt(
_ message: OpenClawWatchExecApprovalPromptMessage) async throws -> WatchNotificationSendResult
{
self.lastSentExecApprovalPrompt = message
if let sendError = self.sendError {
throw sendError
}
return self.nextSendResult
}
func sendExecApprovalResolved(
_ message: OpenClawWatchExecApprovalResolvedMessage) async throws -> WatchNotificationSendResult
{
self.lastSentExecApprovalResolved = message
if let sendError = self.sendError {
throw sendError
}
return self.nextSendResult
}
func sendExecApprovalExpired(
_ message: OpenClawWatchExecApprovalExpiredMessage) async throws -> WatchNotificationSendResult
{
self.lastSentExecApprovalExpired = message
if let sendError = self.sendError {
throw sendError
}
return self.nextSendResult
}
func syncExecApprovalSnapshot(
_ message: OpenClawWatchExecApprovalSnapshotMessage) async throws -> WatchNotificationSendResult
{
self.lastSentExecApprovalSnapshot = message
if let sendError = self.sendError {
throw sendError
}
return self.nextSendResult
}
func emitReply(_ event: WatchQuickReplyEvent) {
self.replyHandler?(event)
}
func emitExecApprovalResolve(_ event: WatchExecApprovalResolveEvent) {
self.execApprovalResolveHandler?(event)
}
func emitExecApprovalSnapshotRequest(_ event: WatchExecApprovalSnapshotRequestEvent) {
self.execApprovalSnapshotRequestHandler?(event)
}
}
private final class MockBootstrapNotificationCenter: NotificationCentering, @unchecked Sendable {
var status: NotificationAuthorizationStatus = .notDetermined
var requestAuthorizationResult = false
var requestAuthorizationCalls = 0
func authorizationStatus() async -> NotificationAuthorizationStatus {
self.status
}
func requestAuthorization(options _: UNAuthorizationOptions) async throws -> Bool {
self.requestAuthorizationCalls += 1
if self.requestAuthorizationResult {
self.status = .authorized
} else {
self.status = .denied
}
return self.requestAuthorizationResult
}
func add(_: UNNotificationRequest) async throws {}
func removePendingNotificationRequests(withIdentifiers _: [String]) async {}
func removeDeliveredNotifications(withIdentifiers _: [String]) async {}
func deliveredNotifications() async -> [NotificationSnapshot] {
[]
}
}
@Suite(.serialized) struct NodeAppModelInvokeTests {
@@ -96,6 +196,233 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
#expect(appModel.mainSessionKey == "agent:agent-123:main")
}
@Test @MainActor func execApprovalPromptPresentationTracksLatestNotificationTap() throws {
let appModel = NodeAppModel()
appModel._test_presentExecApprovalPrompt(
try #require(
NodeAppModel._test_makeExecApprovalPrompt(
id: "approval-1",
commandText: "echo first",
allowedDecisions: ["allow-once", "deny"],
host: "gateway",
nodeId: nil,
agentId: "main",
expiresAtMs: 1)))
let firstPrompt = try #require(appModel._test_pendingExecApprovalPrompt())
#expect(firstPrompt.id == "approval-1")
#expect(firstPrompt.commandText == "echo first")
#expect(firstPrompt.allowsAllowAlways == false)
appModel._test_presentExecApprovalPrompt(
try #require(
NodeAppModel._test_makeExecApprovalPrompt(
id: "approval-2",
commandText: "echo second",
allowedDecisions: ["allow-once", "allow-always", "deny"],
host: "gateway",
nodeId: "node-2",
agentId: nil,
expiresAtMs: 2)))
let secondPrompt = try #require(appModel._test_pendingExecApprovalPrompt())
#expect(secondPrompt.id == "approval-2")
#expect(secondPrompt.commandText == "echo second")
#expect(secondPrompt.allowsAllowAlways)
appModel._test_dismissPendingExecApprovalPrompt()
#expect(appModel._test_pendingExecApprovalPrompt() == nil)
}
@Test @MainActor func dismissPendingExecApprovalPromptByIdLeavesDifferentPromptVisible() throws {
let appModel = NodeAppModel()
appModel._test_presentExecApprovalPrompt(
try #require(
NodeAppModel._test_makeExecApprovalPrompt(
id: "approval-active",
commandText: "echo keep",
allowedDecisions: ["allow-once", "deny"],
host: "gateway",
nodeId: nil,
agentId: nil,
expiresAtMs: 1)))
appModel.dismissPendingExecApprovalPrompt(approvalId: "approval-stale")
let prompt = try #require(appModel._test_pendingExecApprovalPrompt())
#expect(prompt.id == "approval-active")
}
@Test @MainActor func presentingExecApprovalPromptSyncsWatchPrompt() async throws {
let watchService = MockWatchMessagingService()
let appModel = NodeAppModel(watchMessagingService: watchService)
let prompt = try #require(
NodeAppModel._test_makeExecApprovalPrompt(
id: "approval-watch-sync",
commandText: "npm publish",
allowedDecisions: ["allow-once", "deny"],
host: "gateway",
nodeId: "node-1",
agentId: "main",
expiresAtMs: 1234))
appModel._test_presentExecApprovalPrompt(prompt)
await Task.yield()
let sent = try #require(watchService.lastSentExecApprovalPrompt)
#expect(sent.approval.id == "approval-watch-sync")
#expect(sent.approval.allowedDecisions == [.allowOnce, .deny])
#expect(sent.approval.host == "gateway")
#expect(sent.approval.risk == nil)
#expect(sent.resetResolvingState != true)
}
@Test @MainActor func watchExecApprovalSnapshotRequestPublishesCachedApprovalsInBackground() async throws {
let watchService = MockWatchMessagingService()
let appModel = NodeAppModel(watchMessagingService: watchService)
let futureExpiryMs = Int(Date().timeIntervalSince1970 * 1000) + 60_000
appModel._test_presentExecApprovalPrompt(
try #require(
NodeAppModel._test_makeExecApprovalPrompt(
id: "approval-watch-snapshot",
commandText: "echo from watch",
allowedDecisions: ["allow-once", "deny"],
host: "gateway",
nodeId: nil,
agentId: nil,
expiresAtMs: futureExpiryMs)))
await Task.yield()
appModel.setScenePhase(.background)
watchService.emitExecApprovalSnapshotRequest(
WatchExecApprovalSnapshotRequestEvent(
requestId: "snapshot-1",
sentAtMs: 111,
transport: "sendMessage"))
await Task.yield()
let snapshot = try #require(watchService.lastSentExecApprovalSnapshot)
#expect(snapshot.approvals.map(\.id) == ["approval-watch-snapshot"])
}
@Test @MainActor func watchExecApprovalSnapshotRequestSkipsForegroundRecovery() async throws {
let watchService = MockWatchMessagingService()
let appModel = NodeAppModel(watchMessagingService: watchService)
let futureExpiryMs = Int(Date().timeIntervalSince1970 * 1000) + 60_000
appModel._test_presentExecApprovalPrompt(
try #require(
NodeAppModel._test_makeExecApprovalPrompt(
id: "approval-watch-foreground-skip",
commandText: "echo foreground",
allowedDecisions: ["allow-once", "deny"],
host: "gateway",
nodeId: nil,
agentId: nil,
expiresAtMs: futureExpiryMs)))
await Task.yield()
watchService.lastSentExecApprovalSnapshot = nil
watchService.emitExecApprovalSnapshotRequest(
WatchExecApprovalSnapshotRequestEvent(
requestId: "snapshot-foreground",
sentAtMs: 222,
transport: "sendMessage"))
await Task.yield()
#expect(watchService.lastSentExecApprovalSnapshot == nil)
}
@Test @MainActor func pendingWatchRecoveryIDsAreIncludedWithoutDeliveredNotifications() async {
NodeAppModel._test_resetPersistedWatchExecApprovalBridgeState()
defer { NodeAppModel._test_resetPersistedWatchExecApprovalBridgeState() }
let appModel = NodeAppModel(notificationCenter: MockBootstrapNotificationCenter())
appModel._test_recordPendingWatchExecApprovalRecoveryID("approval-watch-recovery")
let ids = await appModel._test_pendingExecApprovalIDsForWatchRecovery()
#expect(ids == ["approval-watch-recovery"])
}
@Test @MainActor func presentingExecApprovalPromptClearsPendingWatchRecoveryID() throws {
NodeAppModel._test_resetPersistedWatchExecApprovalBridgeState()
defer { NodeAppModel._test_resetPersistedWatchExecApprovalBridgeState() }
let appModel = NodeAppModel(notificationCenter: MockBootstrapNotificationCenter())
appModel._test_recordPendingWatchExecApprovalRecoveryID("approval-watch-clear")
#expect(appModel._test_pendingWatchExecApprovalRecoveryIDs() == ["approval-watch-clear"])
appModel._test_presentExecApprovalPrompt(
try #require(
NodeAppModel._test_makeExecApprovalPrompt(
id: "approval-watch-clear",
commandText: "echo clear",
allowedDecisions: ["allow-once", "deny"],
host: "gateway",
nodeId: nil,
agentId: nil,
expiresAtMs: Int(Date().timeIntervalSince1970 * 1000) + 60_000)))
#expect(appModel._test_pendingWatchExecApprovalRecoveryIDs().isEmpty)
}
@Test func approvalNotificationErrorClassificationPrefersStructuredDetails() {
let staleError = GatewayResponseError(
method: "exec.approval.get",
code: "INVALID_REQUEST",
message: "gateway error",
details: ["reason": AnyCodable("APPROVAL_NOT_FOUND")])
let unavailableError = GatewayResponseError(
method: "exec.approval.resolve",
code: "INVALID_REQUEST",
message: "gateway error",
details: ["reason": AnyCodable("APPROVAL_ALLOW_ALWAYS_UNAVAILABLE")])
#expect(NodeAppModel._test_isApprovalNotificationStaleError(staleError))
#expect(NodeAppModel._test_isApprovalNotificationUnavailableError(unavailableError))
}
@Test func backgroundAwareExecApprovalReconnectCoversWatchAndPushPaths() {
#expect(
NodeAppModel._test_shouldUseBackgroundAwareExecApprovalReconnect(
sourceReason: "watch_request",
isBackgrounded: true)
)
#expect(
NodeAppModel._test_shouldUseBackgroundAwareExecApprovalReconnect(
sourceReason: "push_request",
isBackgrounded: true)
)
#expect(
NodeAppModel._test_shouldUseBackgroundAwareExecApprovalReconnect(
sourceReason: "watch_resolve",
isBackgrounded: true)
)
#expect(
!NodeAppModel._test_shouldUseBackgroundAwareExecApprovalReconnect(
sourceReason: "direct",
isBackgrounded: true)
)
#expect(
!NodeAppModel._test_shouldUseBackgroundAwareExecApprovalReconnect(
sourceReason: "watch_request",
isBackgrounded: false)
)
}
@Test func watchExecApprovalHydrateFetchesOnlyMissingIDs() {
let idsToFetch = NodeAppModel._test_watchExecApprovalIDsNeedingFetch(
candidateIDs: ["cached", "pending", "cached", "other", "", " pending "],
cachedApprovalIDs: ["cached", "also-cached"])
#expect(idsToFetch == ["pending", "other"])
}
@Test func watchExecApprovalRetryPromptResetsResolvingStateOnlyForRetryReason() {
#expect(NodeAppModel._test_shouldResetWatchExecApprovalResolvingStateOnPrompt(reason: "resolve_retry"))
#expect(!NodeAppModel._test_shouldResetWatchExecApprovalResolvingStateOnPrompt(reason: "push_request"))
#expect(!NodeAppModel._test_shouldResetWatchExecApprovalResolvingStateOnPrompt(reason: "present_prompt"))
}
@Test func operatorLoopWaitsForBootstrapHandoffBeforeUsingStoredToken() {
#expect(
!NodeAppModel._test_shouldStartOperatorGatewayLoop(
@@ -127,6 +454,15 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
)
}
@Test @MainActor func successfulBootstrapOnboardingRequestsNotificationAuthorization() async {
let center = MockBootstrapNotificationCenter()
let appModel = NodeAppModel(notificationCenter: center)
await appModel._test_handleSuccessfulBootstrapGatewayOnboarding()
#expect(center.requestAuthorizationCalls == 1)
}
@Test func clearingBootstrapTokenStripsReconnectConfigEvenWithoutPersistence() {
let config = GatewayConnectConfig(
url: URL(string: "wss://gateway.example")!,
@@ -145,7 +481,7 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
clientMode: "node",
clientDisplayName: nil))
let cleared = NodeAppModel.clearingBootstrapToken(in: config)
let cleared = NodeAppModel._test_clearingBootstrapToken(in: config)
#expect(cleared?.bootstrapToken == nil)
#expect(cleared?.url == config.url)
#expect(cleared?.stableID == config.stableID)
@@ -477,6 +813,7 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
note: nil,
sentAtMs: 1234,
transport: "transferUserInfo"))
await Task.yield()
#expect(appModel._test_queuedWatchReplyCount() == 1)
}

View File

@@ -0,0 +1,26 @@
import Testing
@testable import OpenClaw
@Suite(.serialized) struct OpenClawAppDelegateTests {
@Test @MainActor func resolvesRegistryModelBeforeViewTaskAssignsDelegateModel() {
let registryModel = NodeAppModel()
OpenClawAppModelRegistry.appModel = registryModel
defer { OpenClawAppModelRegistry.appModel = nil }
let delegate = OpenClawAppDelegate()
#expect(delegate._test_resolvedAppModel() === registryModel)
}
@Test @MainActor func prefersExplicitDelegateModelOverRegistryFallback() {
let registryModel = NodeAppModel()
let explicitModel = NodeAppModel()
OpenClawAppModelRegistry.appModel = registryModel
defer { OpenClawAppModelRegistry.appModel = nil }
let delegate = OpenClawAppDelegate()
delegate.appModel = explicitModel
#expect(delegate._test_resolvedAppModel() === explicitModel)
}
}

View File

@@ -2,27 +2,79 @@ import SwiftUI
@main
struct OpenClawWatchApp: App {
@Environment(\.scenePhase) private var scenePhase
@State private var inboxStore = WatchInboxStore()
@State private var receiver: WatchConnectivityReceiver?
@State private var execApprovalRefreshTask: Task<Void, Never>?
var body: some Scene {
WindowGroup {
WatchInboxView(store: self.inboxStore) { action in
guard let receiver = self.receiver else { return }
let draft = self.inboxStore.makeReplyDraft(action: action)
self.inboxStore.markReplySending(actionLabel: action.label)
Task { @MainActor in
let result = await receiver.sendReply(draft)
self.inboxStore.markReplyResult(result, actionLabel: action.label)
}
}
WatchInboxView(
store: self.inboxStore,
onAction: { action in
guard let receiver = self.receiver else { return }
let draft = self.inboxStore.makeReplyDraft(action: action)
self.inboxStore.markReplySending(actionLabel: action.label)
Task { @MainActor in
let result = await receiver.sendReply(draft)
self.inboxStore.markReplyResult(result, actionLabel: action.label)
}
},
onExecApprovalDecision: { approvalId, decision in
guard let receiver = self.receiver else { return }
self.inboxStore.markExecApprovalSending(approvalId: approvalId, decision: decision)
Task { @MainActor in
let result = await receiver.sendExecApprovalResolve(
approvalId: approvalId,
decision: decision)
self.inboxStore.markExecApprovalSendResult(
approvalId: approvalId,
decision: decision,
result: result)
}
},
onRefreshExecApprovalReview: {
self.refreshExecApprovalReview(force: true)
})
.task {
if self.receiver == nil {
let receiver = WatchConnectivityReceiver(store: self.inboxStore)
receiver.activate()
self.receiver = receiver
}
self.refreshExecApprovalReview()
}
.onChange(of: self.scenePhase) { _, newPhase in
guard newPhase == .active else { return }
self.refreshExecApprovalReview()
}
}
}
private func refreshExecApprovalReview(force: Bool = false) {
guard let receiver = self.receiver else { return }
guard force || self.inboxStore.shouldAutoRequestExecApprovalSnapshot else { return }
self.execApprovalRefreshTask?.cancel()
self.execApprovalRefreshTask = Task { @MainActor in
self.inboxStore.beginExecApprovalReviewLoading()
for attempt in 0..<5 {
if Task.isCancelled { return }
await receiver.requestExecApprovalSnapshot()
if !self.inboxStore.execApprovals.isEmpty
|| self.inboxStore.hasCompletedExecApprovalSnapshotRefresh
{
self.inboxStore.markExecApprovalReviewLoaded()
return
}
if attempt < 4 {
try? await Task.sleep(nanoseconds: 700_000_000)
}
}
if self.inboxStore.execApprovals.isEmpty {
self.inboxStore.markExecApprovalReviewUnavailable(
"Couldn't load approval from your iPhone yet.")
}
}
}
}

View File

@@ -52,6 +52,31 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable {
}
}
func requestExecApprovalSnapshot() async {
await self.ensureActivated()
guard let session = self.session else { return }
let request = WatchExecApprovalSnapshotRequestMessage(
requestId: UUID().uuidString,
sentAtMs: Self.nowMs())
let payload = Self.encodeSnapshotRequestPayload(request)
if session.isReachable {
do {
try await withCheckedThrowingContinuation(isolation: nil) {
(continuation: CheckedContinuation<Void, Error>) in
session.sendMessage(payload, replyHandler: { _ in
continuation.resume(returning: ())
}, errorHandler: { error in
continuation.resume(throwing: error)
})
}
return
} catch {
// Fall through to queued delivery.
}
}
_ = session.transferUserInfo(payload)
}
func sendReply(_ draft: WatchReplyDraft) async -> WatchReplySendResult {
await self.ensureActivated()
guard let session = self.session else {
@@ -63,7 +88,7 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable {
}
var payload: [String: Any] = [
"type": "watch.reply",
"type": WatchPayloadType.reply.rawValue,
"replyId": draft.replyId,
"promptId": draft.promptId,
"actionId": draft.actionId,
@@ -83,11 +108,38 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable {
payload["note"] = note
}
return await self.sendPayload(payload, session: session)
}
func sendExecApprovalResolve(
approvalId: String,
decision: WatchExecApprovalDecision) async -> WatchReplySendResult
{
await self.ensureActivated()
guard let session = self.session else {
return WatchReplySendResult(
deliveredImmediately: false,
queuedForDelivery: false,
transport: "none",
errorMessage: "watch session unavailable")
}
let payload = Self.encodeExecApprovalResolvePayload(
WatchExecApprovalResolveMessage(
approvalId: approvalId,
decision: decision,
replyId: UUID().uuidString,
sentAtMs: Self.nowMs()))
return await self.sendPayload(payload, session: session)
}
private func sendPayload(_ payload: [String: Any], session: WCSession) async -> WatchReplySendResult {
if session.isReachable {
do {
try await withCheckedThrowingContinuation { continuation in
try await withCheckedThrowingContinuation(isolation: nil) {
(continuation: CheckedContinuation<Void, Error>) in
session.sendMessage(payload, replyHandler: { _ in
continuation.resume()
continuation.resume(returning: ())
}, errorHandler: { error in
continuation.resume(throwing: error)
})
@@ -110,6 +162,10 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable {
errorMessage: nil)
}
private static func nowMs() -> Int {
Int(Date().timeIntervalSince1970 * 1000)
}
private static func normalizeObject(_ value: Any) -> [String: Any]? {
if let object = value as? [String: Any] {
return object
@@ -147,7 +203,9 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable {
}
private static func parseNotificationPayload(_ payload: [String: Any]) -> WatchNotifyMessage? {
guard let type = payload["type"] as? String, type == "watch.notify" else {
guard let type = payload["type"] as? String,
type == WatchPayloadType.notify.rawValue
else {
return nil
}
@@ -189,6 +247,153 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable {
risk: risk,
actions: actions)
}
private static func parseExecApprovalDecision(_ value: Any?) -> WatchExecApprovalDecision? {
let raw = (value as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return WatchExecApprovalDecision(rawValue: raw)
}
private static func parseExecApprovalItem(_ value: Any?) -> WatchExecApprovalItem? {
guard let payload = value.flatMap(Self.normalizeObject) else {
return nil
}
let id = (payload["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let commandText = (payload["commandText"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !id.isEmpty, !commandText.isEmpty else {
return nil
}
let commandPreview = (payload["commandPreview"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let host = (payload["host"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
let nodeId = (payload["nodeId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
let agentId = (payload["agentId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
let expiresAtMs = (payload["expiresAtMs"] as? Int) ?? (payload["expiresAtMs"] as? NSNumber)?.intValue
let riskRaw = (payload["risk"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let risk = WatchRiskLevel(rawValue: riskRaw)
let allowedDecisions = (payload["allowedDecisions"] as? [Any] ?? []).compactMap {
Self.parseExecApprovalDecision($0)
}
return WatchExecApprovalItem(
id: id,
commandText: commandText,
commandPreview: commandPreview,
host: host,
nodeId: nodeId,
agentId: agentId,
expiresAtMs: expiresAtMs,
allowedDecisions: allowedDecisions,
risk: risk)
}
private static func parseExecApprovalPromptPayload(
_ payload: [String: Any]) -> WatchExecApprovalPromptMessage?
{
guard let type = payload["type"] as? String,
type == WatchPayloadType.execApprovalPrompt.rawValue,
let approval = Self.parseExecApprovalItem(payload["approval"])
else {
return nil
}
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
let deliveryId = (payload["deliveryId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
let resetResolvingState = payload["resetResolvingState"] as? Bool
return WatchExecApprovalPromptMessage(
approval: approval,
sentAtMs: sentAtMs,
deliveryId: deliveryId,
resetResolvingState: resetResolvingState)
}
private static func parseExecApprovalResolvedPayload(
_ payload: [String: Any]) -> WatchExecApprovalResolvedMessage?
{
guard let type = payload["type"] as? String,
type == WatchPayloadType.execApprovalResolved.rawValue
else {
return nil
}
let approvalId = (payload["approvalId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !approvalId.isEmpty else { return nil }
let decision = Self.parseExecApprovalDecision(payload["decision"])
let resolvedAtMs = (payload["resolvedAtMs"] as? Int)
?? (payload["resolvedAtMs"] as? NSNumber)?.intValue
let source = (payload["source"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
return WatchExecApprovalResolvedMessage(
approvalId: approvalId,
decision: decision,
resolvedAtMs: resolvedAtMs,
source: source)
}
private static func parseExecApprovalExpiredPayload(
_ payload: [String: Any]) -> WatchExecApprovalExpiredMessage?
{
guard let type = payload["type"] as? String,
type == WatchPayloadType.execApprovalExpired.rawValue
else {
return nil
}
let approvalId = (payload["approvalId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let rawReason = (payload["reason"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !approvalId.isEmpty,
let reason = WatchExecApprovalCloseReason(rawValue: rawReason)
else {
return nil
}
let expiredAtMs = (payload["expiredAtMs"] as? Int) ?? (payload["expiredAtMs"] as? NSNumber)?.intValue
return WatchExecApprovalExpiredMessage(
approvalId: approvalId,
reason: reason,
expiredAtMs: expiredAtMs)
}
private static func parseExecApprovalSnapshotPayload(
_ payload: [String: Any]) -> WatchExecApprovalSnapshotMessage?
{
guard let type = payload["type"] as? String,
type == WatchPayloadType.execApprovalSnapshot.rawValue
else {
return nil
}
let approvals = (payload["approvals"] as? [Any] ?? []).compactMap { item in
Self.parseExecApprovalItem(item)
}
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
let snapshotId = (payload["snapshotId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
return WatchExecApprovalSnapshotMessage(
approvals: approvals,
sentAtMs: sentAtMs,
snapshotId: snapshotId)
}
private static func encodeSnapshotRequestPayload(
_ request: WatchExecApprovalSnapshotRequestMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": WatchPayloadType.execApprovalSnapshotRequest.rawValue,
"requestId": request.requestId,
]
if let sentAtMs = request.sentAtMs {
payload["sentAtMs"] = sentAtMs
}
return payload
}
private static func encodeExecApprovalResolvePayload(
_ message: WatchExecApprovalResolveMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": WatchPayloadType.execApprovalResolve.rawValue,
"approvalId": message.approvalId,
"decision": message.decision.rawValue,
"replyId": message.replyId,
]
if let sentAtMs = message.sentAtMs {
payload["sentAtMs"] = sentAtMs
}
return payload
}
}
extension WatchConnectivityReceiver: WCSessionDelegate {
@@ -196,13 +401,14 @@ extension WatchConnectivityReceiver: WCSessionDelegate {
_: WCSession,
activationDidCompleteWith _: WCSessionActivationState,
error _: (any Error)?)
{}
{
Task {
await self.requestExecApprovalSnapshot()
}
}
func session(_: WCSession, didReceiveMessage message: [String: Any]) {
guard let incoming = Self.parseNotificationPayload(message) else { return }
Task { @MainActor in
self.store.consume(message: incoming, transport: "sendMessage")
}
self.consumeIncomingPayload(message, transport: "sendMessage")
}
func session(
@@ -210,27 +416,47 @@ extension WatchConnectivityReceiver: WCSessionDelegate {
didReceiveMessage message: [String: Any],
replyHandler: @escaping ([String: Any]) -> Void)
{
guard let incoming = Self.parseNotificationPayload(message) else {
replyHandler(["ok": false])
return
}
replyHandler(["ok": true])
Task { @MainActor in
self.store.consume(message: incoming, transport: "sendMessage")
}
self.consumeIncomingPayload(message, transport: "sendMessage")
}
func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
guard let incoming = Self.parseNotificationPayload(userInfo) else { return }
Task { @MainActor in
self.store.consume(message: incoming, transport: "transferUserInfo")
}
self.consumeIncomingPayload(userInfo, transport: "transferUserInfo")
}
func session(_: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
guard let incoming = Self.parseNotificationPayload(applicationContext) else { return }
Task { @MainActor in
self.store.consume(message: incoming, transport: "applicationContext")
self.consumeIncomingPayload(applicationContext, transport: "applicationContext")
}
private func consumeIncomingPayload(_ payload: [String: Any], transport: String) {
if let incoming = Self.parseNotificationPayload(payload) {
Task { @MainActor in
self.store.consume(message: incoming, transport: transport)
}
return
}
if let prompt = Self.parseExecApprovalPromptPayload(payload) {
Task { @MainActor in
self.store.consume(execApprovalPrompt: prompt, transport: transport)
}
return
}
if let resolved = Self.parseExecApprovalResolvedPayload(payload) {
Task { @MainActor in
self.store.consume(execApprovalResolved: resolved)
}
return
}
if let expired = Self.parseExecApprovalExpiredPayload(payload) {
Task { @MainActor in
self.store.consume(execApprovalExpired: expired)
}
return
}
if let snapshot = Self.parseExecApprovalSnapshotPayload(payload) {
Task { @MainActor in
self.store.consume(execApprovalSnapshot: snapshot, transport: transport)
}
}
}
}

View File

@@ -3,6 +3,86 @@ import Observation
import UserNotifications
import WatchKit
enum WatchPayloadType: String, Codable, Sendable, Equatable {
case notify = "watch.notify"
case reply = "watch.reply"
case execApprovalPrompt = "watch.execApproval.prompt"
case execApprovalResolve = "watch.execApproval.resolve"
case execApprovalResolved = "watch.execApproval.resolved"
case execApprovalExpired = "watch.execApproval.expired"
case execApprovalSnapshot = "watch.execApproval.snapshot"
case execApprovalSnapshotRequest = "watch.execApproval.snapshotRequest"
}
enum WatchRiskLevel: String, Codable, Sendable, Equatable {
case low
case medium
case high
}
enum WatchExecApprovalDecision: String, Codable, Sendable, Equatable {
case allowOnce = "allow-once"
case deny
}
enum WatchExecApprovalCloseReason: String, Codable, Sendable, Equatable {
case expired
case notFound = "not-found"
case unavailable
case replaced
case resolved
}
struct WatchExecApprovalItem: Codable, Sendable, Equatable, Identifiable {
var id: String
var commandText: String
var commandPreview: String?
var host: String?
var nodeId: String?
var agentId: String?
var expiresAtMs: Int?
var allowedDecisions: [WatchExecApprovalDecision]
var risk: WatchRiskLevel?
}
struct WatchExecApprovalPromptMessage: Codable, Sendable, Equatable {
var approval: WatchExecApprovalItem
var sentAtMs: Int?
var deliveryId: String?
var resetResolvingState: Bool?
}
struct WatchExecApprovalResolvedMessage: Codable, Sendable, Equatable {
var approvalId: String
var decision: WatchExecApprovalDecision?
var resolvedAtMs: Int?
var source: String?
}
struct WatchExecApprovalExpiredMessage: Codable, Sendable, Equatable {
var approvalId: String
var reason: WatchExecApprovalCloseReason
var expiredAtMs: Int?
}
struct WatchExecApprovalSnapshotMessage: Codable, Sendable, Equatable {
var approvals: [WatchExecApprovalItem]
var sentAtMs: Int?
var snapshotId: String?
}
struct WatchExecApprovalSnapshotRequestMessage: Codable, Sendable, Equatable {
var requestId: String
var sentAtMs: Int?
}
struct WatchExecApprovalResolveMessage: Codable, Sendable, Equatable {
var approvalId: String
var decision: WatchExecApprovalDecision
var replyId: String
var sentAtMs: Int?
}
struct WatchPromptAction: Codable, Sendable, Equatable, Identifiable {
var id: String
var label: String
@@ -23,6 +103,18 @@ struct WatchNotifyMessage: Sendable {
var actions: [WatchPromptAction]
}
struct WatchExecApprovalRecord: Codable, Sendable, Equatable, Identifiable {
var approval: WatchExecApprovalItem
var transport: String
var updatedAt: Date
var isResolving: Bool
var pendingDecision: WatchExecApprovalDecision?
var statusText: String?
var statusAt: Date?
var id: String { self.approval.id }
}
@MainActor @Observable final class WatchInboxStore {
private struct PersistedState: Codable {
var title: String
@@ -39,13 +131,20 @@ struct WatchNotifyMessage: Sendable {
var actions: [WatchPromptAction]?
var replyStatusText: String?
var replyStatusAt: Date?
var execApprovals: [WatchExecApprovalRecord]
var selectedExecApprovalID: String?
var lastExecApprovalSnapshotID: String?
var lastExecApprovalOutcomeText: String?
var lastExecApprovalOutcomeAt: Date?
}
private static let persistedStateKey = "watch.inbox.state.v1"
private static let persistedStateKey = "watch.inbox.state.v2"
private static let defaultTitle = "OpenClaw"
private static let defaultBody = "Waiting for messages from your iPhone."
private let defaults: UserDefaults
var title = "OpenClaw"
var body = "Waiting for messages from your iPhone."
var title = WatchInboxStore.defaultTitle
var body = WatchInboxStore.defaultBody
var transport = "none"
var updatedAt: Date?
var promptId: String?
@@ -58,16 +157,88 @@ struct WatchNotifyMessage: Sendable {
var replyStatusText: String?
var replyStatusAt: Date?
var isReplySending = false
var execApprovals: [WatchExecApprovalRecord] = []
var selectedExecApprovalID: String?
var lastExecApprovalOutcomeText: String?
var lastExecApprovalOutcomeAt: Date?
var isExecApprovalReviewLoading = false
var execApprovalReviewStatusText: String?
var execApprovalReviewStatusAt: Date?
private var lastExecApprovalSnapshotID: String?
private var hasCompletedExecApprovalSnapshotRefreshInSession = false
private var lastDeliveryKey: String?
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
self.restorePersistedState()
self.pruneExpiredExecApprovals(nowMs: Self.nowMs())
Task {
await self.ensureNotificationAuthorization()
}
}
var sortedExecApprovals: [WatchExecApprovalRecord] {
self.execApprovals.sorted { lhs, rhs in
let lhsExpires = lhs.approval.expiresAtMs ?? Int.max
let rhsExpires = rhs.approval.expiresAtMs ?? Int.max
if lhsExpires != rhsExpires {
return lhsExpires < rhsExpires
}
return lhs.updatedAt > rhs.updatedAt
}
}
var activeExecApproval: WatchExecApprovalRecord? {
if let selectedExecApprovalID,
let selected = self.execApprovals.first(where: { $0.id == selectedExecApprovalID })
{
return selected
}
return self.sortedExecApprovals.first
}
var shouldAutoRequestExecApprovalSnapshot: Bool {
self.execApprovals.isEmpty
&& self.actions.isEmpty
&& self.title == Self.defaultTitle
&& self.body == Self.defaultBody
&& !self.hasCompletedExecApprovalSnapshotRefreshInSession
}
var hasCompletedExecApprovalSnapshotRefresh: Bool {
self.hasCompletedExecApprovalSnapshotRefreshInSession
}
var shouldShowExecApprovalReviewStatus: Bool {
self.execApprovals.isEmpty && !(self.execApprovalReviewStatusText?.isEmpty ?? true)
}
func beginExecApprovalReviewLoading() {
guard self.execApprovals.isEmpty else {
self.markExecApprovalReviewLoaded()
return
}
self.isExecApprovalReviewLoading = true
self.execApprovalReviewStatusText = "Loading approval from iPhone…"
self.execApprovalReviewStatusAt = Date()
}
func markExecApprovalReviewLoaded() {
self.isExecApprovalReviewLoading = false
self.execApprovalReviewStatusText = nil
self.execApprovalReviewStatusAt = nil
}
func markExecApprovalReviewUnavailable(_ message: String) {
guard self.execApprovals.isEmpty else {
self.markExecApprovalReviewLoaded()
return
}
self.isExecApprovalReviewLoading = false
self.execApprovalReviewStatusText = message
self.execApprovalReviewStatusAt = Date()
}
func consume(message: WatchNotifyMessage, transport: String) {
let messageID = message.id?
.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -82,6 +253,7 @@ struct WatchNotifyMessage: Sendable {
self.title = normalizedTitle
self.body = message.body
self.transport = transport
self.markExecApprovalReviewLoaded()
self.updatedAt = Date()
self.promptId = message.promptId
self.sessionKey = message.sessionKey
@@ -105,6 +277,209 @@ struct WatchNotifyMessage: Sendable {
}
}
func consume(
execApprovalPrompt message: WatchExecApprovalPromptMessage,
transport: String)
{
self.pruneExpiredExecApprovals(nowMs: Self.nowMs())
self.upsertExecApproval(
message.approval,
transport: transport,
keepSelectionIfPossible: true,
resetResolvingState: message.resetResolvingState == true)
self.markExecApprovalReviewLoaded()
self.lastExecApprovalOutcomeText = nil
self.lastExecApprovalOutcomeAt = nil
Task {
await self.postLocalNotification(
identifier: "watch.execApproval.\(message.approval.id)",
title: "Exec approval required",
body: message.approval.commandPreview ?? message.approval.commandText,
risk: message.approval.risk?.rawValue)
}
}
func consume(
execApprovalSnapshot message: WatchExecApprovalSnapshotMessage,
transport: String)
{
let snapshotID = message.snapshotId?.trimmingCharacters(in: .whitespacesAndNewlines)
if let snapshotID, !snapshotID.isEmpty, snapshotID == self.lastExecApprovalSnapshotID {
return
}
let existingRecordsByID = Dictionary(
uniqueKeysWithValues: self.execApprovals.map { ($0.id, $0) })
self.execApprovals = message.approvals.map { approval in
self.mergedExecApprovalRecord(
approval: approval,
transport: transport,
existingRecord: existingRecordsByID[approval.id])
}
self.lastExecApprovalSnapshotID = snapshotID
self.hasCompletedExecApprovalSnapshotRefreshInSession = true
if let selectedExecApprovalID,
!self.execApprovals.contains(where: { $0.id == selectedExecApprovalID })
{
self.selectedExecApprovalID = self.sortedExecApprovals.first?.id
} else if self.selectedExecApprovalID == nil {
self.selectedExecApprovalID = self.sortedExecApprovals.first?.id
}
self.pruneExpiredExecApprovals(nowMs: Self.nowMs())
self.markExecApprovalReviewLoaded()
self.persistState()
}
func consume(execApprovalResolved message: WatchExecApprovalResolvedMessage) {
self.removeExecApproval(id: message.approvalId)
let statusText: String
switch message.decision {
case .allowOnce:
statusText = "Allowed once"
case .deny:
statusText = "Denied"
case nil:
statusText = "Approval resolved"
}
self.lastExecApprovalOutcomeText = statusText
self.lastExecApprovalOutcomeAt = Date()
self.persistState()
}
func consume(execApprovalExpired message: WatchExecApprovalExpiredMessage) {
self.removeExecApproval(id: message.approvalId)
let statusText: String
switch message.reason {
case .expired:
statusText = "Approval expired"
case .notFound:
statusText = "Approval no longer available"
case .resolved:
statusText = "Approval resolved elsewhere"
case .replaced:
statusText = "Approval replaced"
case .unavailable:
statusText = "Approval unavailable"
}
self.lastExecApprovalOutcomeText = statusText
self.lastExecApprovalOutcomeAt = Date()
self.persistState()
}
func selectExecApproval(id: String) {
let normalizedID = id.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalizedID.isEmpty else { return }
guard self.execApprovals.contains(where: { $0.id == normalizedID }) else { return }
self.selectedExecApprovalID = normalizedID
self.persistState()
}
func markExecApprovalSending(approvalId: String, decision: WatchExecApprovalDecision) {
guard let index = self.execApprovals.firstIndex(where: { $0.id == approvalId }) else { return }
self.execApprovals[index].isResolving = true
self.execApprovals[index].pendingDecision = decision
self.execApprovals[index].statusText = "Sending \(Self.decisionLabel(decision))"
self.execApprovals[index].statusAt = Date()
self.persistState()
}
func markExecApprovalSendResult(
approvalId: String,
decision: WatchExecApprovalDecision,
result: WatchReplySendResult)
{
guard let index = self.execApprovals.firstIndex(where: { $0.id == approvalId }) else { return }
if let errorMessage = result.errorMessage, !errorMessage.isEmpty {
self.execApprovals[index].isResolving = false
self.execApprovals[index].statusText = "Failed: \(errorMessage)"
} else if result.deliveredImmediately {
self.execApprovals[index].isResolving = true
self.execApprovals[index].statusText = "\(Self.decisionLabel(decision)): sent"
} else if result.queuedForDelivery {
self.execApprovals[index].isResolving = true
self.execApprovals[index].statusText = "\(Self.decisionLabel(decision)): queued"
} else {
self.execApprovals[index].isResolving = true
self.execApprovals[index].statusText = "\(Self.decisionLabel(decision)): sent"
}
self.execApprovals[index].pendingDecision = result.errorMessage == nil ? decision : nil
self.execApprovals[index].statusAt = Date()
self.persistState()
}
private func upsertExecApproval(
_ approval: WatchExecApprovalItem,
transport: String,
keepSelectionIfPossible: Bool,
resetResolvingState: Bool = false)
{
if let index = self.execApprovals.firstIndex(where: { $0.id == approval.id }) {
self.execApprovals[index] = self.mergedExecApprovalRecord(
approval: approval,
transport: transport,
existingRecord: self.execApprovals[index],
resetResolvingState: resetResolvingState)
} else {
self.execApprovals.append(
self.mergedExecApprovalRecord(
approval: approval,
transport: transport,
existingRecord: nil,
resetResolvingState: resetResolvingState))
}
if !keepSelectionIfPossible || self.selectedExecApprovalID == nil {
self.selectedExecApprovalID = approval.id
}
self.persistState()
}
private func mergedExecApprovalRecord(
approval: WatchExecApprovalItem,
transport: String,
existingRecord: WatchExecApprovalRecord?,
resetResolvingState: Bool = false) -> WatchExecApprovalRecord
{
// Preserve in-flight state across ordinary snapshot/prompt refreshes so duplicate
// submissions stay disabled, but clear it when the iPhone explicitly republishes a
// prompt after a failed resolve so the watch can retry.
let isResolving = resetResolvingState ? false : (existingRecord?.isResolving ?? false)
let pendingDecision = resetResolvingState ? nil : existingRecord?.pendingDecision
let statusText = resetResolvingState ? nil : existingRecord?.statusText
let statusAt = resetResolvingState ? nil : existingRecord?.statusAt
return WatchExecApprovalRecord(
approval: approval,
transport: transport,
updatedAt: Date(),
isResolving: isResolving,
pendingDecision: pendingDecision,
statusText: statusText,
statusAt: statusAt)
}
private func removeExecApproval(id: String) {
let normalizedID = id.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalizedID.isEmpty else { return }
self.execApprovals.removeAll { $0.id == normalizedID }
if self.selectedExecApprovalID == normalizedID {
self.selectedExecApprovalID = self.sortedExecApprovals.first?.id
}
self.persistState()
}
private func pruneExpiredExecApprovals(nowMs: Int) {
self.execApprovals.removeAll { record in
guard let expiresAtMs = record.approval.expiresAtMs else { return false }
return expiresAtMs <= nowMs
}
if let selectedExecApprovalID,
!self.execApprovals.contains(where: { $0.id == selectedExecApprovalID })
{
self.selectedExecApprovalID = self.sortedExecApprovals.first?.id
}
self.persistState()
}
private func restorePersistedState() {
guard let data = self.defaults.data(forKey: Self.persistedStateKey),
let state = try? JSONDecoder().decode(PersistedState.self, from: data)
@@ -126,10 +501,15 @@ struct WatchNotifyMessage: Sendable {
self.actions = state.actions ?? []
self.replyStatusText = state.replyStatusText
self.replyStatusAt = state.replyStatusAt
self.execApprovals = state.execApprovals
self.selectedExecApprovalID = state.selectedExecApprovalID
self.lastExecApprovalSnapshotID = state.lastExecApprovalSnapshotID
self.lastExecApprovalOutcomeText = state.lastExecApprovalOutcomeText
self.lastExecApprovalOutcomeAt = state.lastExecApprovalOutcomeAt
}
private func persistState() {
guard let updatedAt = self.updatedAt else { return }
let updatedAt = self.updatedAt ?? self.lastExecApprovalOutcomeAt ?? Date()
let state = PersistedState(
title: self.title,
body: self.body,
@@ -144,7 +524,12 @@ struct WatchNotifyMessage: Sendable {
risk: self.risk,
actions: self.actions,
replyStatusText: self.replyStatusText,
replyStatusAt: self.replyStatusAt)
replyStatusAt: self.replyStatusAt,
execApprovals: self.execApprovals,
selectedExecApprovalID: self.selectedExecApprovalID,
lastExecApprovalSnapshotID: self.lastExecApprovalSnapshotID,
lastExecApprovalOutcomeText: self.lastExecApprovalOutcomeText,
lastExecApprovalOutcomeAt: self.lastExecApprovalOutcomeAt)
guard let data = try? JSONEncoder().encode(state) else { return }
self.defaults.set(data, forKey: Self.persistedStateKey)
}
@@ -187,7 +572,7 @@ struct WatchNotifyMessage: Sendable {
actionLabel: action.label,
sessionKey: self.sessionKey,
note: nil,
sentAtMs: Int(Date().timeIntervalSince1970 * 1000))
sentAtMs: Self.nowMs())
}
func markReplySending(actionLabel: String) {
@@ -227,4 +612,17 @@ struct WatchNotifyMessage: Sendable {
_ = try? await UNUserNotificationCenter.current().add(request)
WKInterfaceDevice.current().play(self.mapHapticRisk(risk))
}
private static func decisionLabel(_ decision: WatchExecApprovalDecision) -> String {
switch decision {
case .allowOnce:
"Allow Once"
case .deny:
"Deny"
}
}
private static func nowMs() -> Int {
Int(Date().timeIntervalSince1970 * 1000)
}
}

View File

@@ -1,7 +1,246 @@
import SwiftUI
struct WatchInboxView: View {
@Bindable var store: WatchInboxStore
var store: WatchInboxStore
var onAction: ((WatchPromptAction) -> Void)?
var onExecApprovalDecision: ((String, WatchExecApprovalDecision) -> Void)?
var onRefreshExecApprovalReview: (() -> Void)?
var body: some View {
NavigationStack {
if self.store.sortedExecApprovals.count == 1,
let record = self.store.activeExecApproval
{
WatchExecApprovalDetailView(
store: self.store,
record: record,
onDecision: self.onExecApprovalDecision)
} else if !self.store.sortedExecApprovals.isEmpty {
WatchExecApprovalListView(
store: self.store,
onDecision: self.onExecApprovalDecision)
} else if self.store.shouldShowExecApprovalReviewStatus {
WatchExecApprovalLoadingView(
store: self.store,
onRetry: self.onRefreshExecApprovalReview)
} else {
WatchGenericInboxView(store: self.store, onAction: self.onAction)
}
}
}
}
private struct WatchExecApprovalLoadingView: View {
var store: WatchInboxStore
var onRetry: (() -> Void)?
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
Text("Exec approval")
.font(.headline)
if self.store.isExecApprovalReviewLoading {
ProgressView()
.frame(maxWidth: .infinity, alignment: .leading)
}
if let statusText = self.store.execApprovalReviewStatusText, !statusText.isEmpty {
Text(statusText)
.font(.body)
.fixedSize(horizontal: false, vertical: true)
}
if !self.store.isExecApprovalReviewLoading {
Button("Retry") {
self.onRetry?()
}
}
Text("Keep your iPhone nearby and unlocked if review details take a moment to appear.")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
.navigationTitle("Exec approval")
}
}
private struct WatchExecApprovalListView: View {
var store: WatchInboxStore
var onDecision: ((String, WatchExecApprovalDecision) -> Void)?
var body: some View {
List {
Section("Exec approvals") {
ForEach(self.store.sortedExecApprovals) { record in
NavigationLink {
WatchExecApprovalDetailView(
store: self.store,
record: record,
onDecision: self.onDecision)
} label: {
VStack(alignment: .leading, spacing: 4) {
Text(record.approval.commandPreview ?? record.approval.commandText)
.font(.headline)
.lineLimit(2)
Text(self.metadataLine(for: record))
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(2)
if let statusText = record.statusText, !statusText.isEmpty {
Text(statusText)
.font(.footnote)
.foregroundStyle(record.isResolving ? Color.secondary : Color.red)
.lineLimit(2)
}
}
}
}
}
if let outcome = self.store.lastExecApprovalOutcomeText, !outcome.isEmpty {
Section("Last result") {
Text(outcome)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
.navigationTitle("Approvals")
}
private func metadataLine(for record: WatchExecApprovalRecord) -> String {
var parts: [String] = []
if let host = record.approval.host, !host.isEmpty {
parts.append(host)
}
if let nodeId = record.approval.nodeId, !nodeId.isEmpty {
parts.append(nodeId)
}
if let expiresText = Self.expiresText(record.approval.expiresAtMs) {
parts.append(expiresText)
}
return parts.isEmpty ? "Pending review" : parts.joined(separator: " · ")
}
private static func expiresText(_ expiresAtMs: Int?) -> String? {
guard let expiresAtMs else { return nil }
let deltaSeconds = max(0, (expiresAtMs - Int(Date().timeIntervalSince1970 * 1000)) / 1000)
if deltaSeconds < 60 {
return "Expires in <1m"
}
return "Expires in \(deltaSeconds / 60)m"
}
}
private struct WatchExecApprovalDetailView: View {
var store: WatchInboxStore
let record: WatchExecApprovalRecord
var onDecision: ((String, WatchExecApprovalDecision) -> Void)?
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
Text(self.record.approval.commandText)
.font(.headline)
.fixedSize(horizontal: false, vertical: true)
if let host = self.record.approval.host, !host.isEmpty {
self.metadataRow(label: "Host", value: host)
}
if let nodeId = self.record.approval.nodeId, !nodeId.isEmpty {
self.metadataRow(label: "Node", value: nodeId)
}
if let agentId = self.record.approval.agentId, !agentId.isEmpty {
self.metadataRow(label: "Agent", value: agentId)
}
if let expiresText = Self.expiresText(self.record.approval.expiresAtMs) {
self.metadataRow(label: "Expires", value: expiresText)
}
if let riskText = self.riskText(self.record.approval.risk) {
self.metadataRow(label: "Risk", value: riskText)
}
if let statusText = self.currentRecord?.statusText, !statusText.isEmpty {
Text(statusText)
.font(.footnote)
.foregroundStyle((self.currentRecord?.isResolving ?? false) ? Color.secondary : Color.red)
}
if let currentRecord,
currentRecord.approval.allowedDecisions.contains(.allowOnce)
{
Button("Allow Once") {
self.onDecision?(currentRecord.id, .allowOnce)
}
.disabled(currentRecord.isResolving)
}
if let currentRecord,
currentRecord.approval.allowedDecisions.contains(.deny)
{
Button(role: .destructive) {
self.onDecision?(currentRecord.id, .deny)
} label: {
Text("Deny")
.frame(maxWidth: .infinity)
}
.disabled(currentRecord.isResolving)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
.navigationTitle("Exec approval")
.onAppear {
self.store.selectExecApproval(id: self.record.id)
}
}
private var currentRecord: WatchExecApprovalRecord? {
self.store.execApprovals.first(where: { $0.id == self.record.id })
}
private func metadataRow(label: String, value: String) -> some View {
VStack(alignment: .leading, spacing: 2) {
Text(label)
.font(.caption2)
.foregroundStyle(.secondary)
Text(value)
.font(.footnote)
.fixedSize(horizontal: false, vertical: true)
}
}
private func riskText(_ risk: WatchRiskLevel?) -> String? {
switch risk {
case .high:
return "High"
case .medium:
return "Medium"
case .low:
return "Low"
case nil:
return nil
}
}
private static func expiresText(_ expiresAtMs: Int?) -> String? {
guard let expiresAtMs else { return nil }
let deltaSeconds = max(0, (expiresAtMs - Int(Date().timeIntervalSince1970 * 1000)) / 1000)
if deltaSeconds < 60 {
return "<1 minute"
}
return "\(deltaSeconds / 60) minutes"
}
}
private struct WatchGenericInboxView: View {
var store: WatchInboxStore
var onAction: ((WatchPromptAction) -> Void)?
private func role(for action: WatchPromptAction) -> ButtonRole? {
@@ -18,40 +257,46 @@ struct WatchInboxView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
Text(store.title)
Text(self.store.title)
.font(.headline)
.lineLimit(2)
Text(store.body)
Text(self.store.body)
.font(.body)
.fixedSize(horizontal: false, vertical: true)
if let details = store.details, !details.isEmpty {
if let details = self.store.details, !details.isEmpty {
Text(details)
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
if !store.actions.isEmpty {
ForEach(store.actions) { action in
if let outcome = self.store.lastExecApprovalOutcomeText, !outcome.isEmpty {
Text(outcome)
.font(.footnote)
.foregroundStyle(.secondary)
}
if !self.store.actions.isEmpty {
ForEach(self.store.actions) { action in
Button(role: self.role(for: action)) {
self.onAction?(action)
} label: {
Text(action.label)
.frame(maxWidth: .infinity)
}
.disabled(store.isReplySending)
.disabled(self.store.isReplySending)
}
}
if let replyStatusText = store.replyStatusText, !replyStatusText.isEmpty {
if let replyStatusText = self.store.replyStatusText, !replyStatusText.isEmpty {
Text(replyStatusText)
.font(.footnote)
.foregroundStyle(.secondary)
}
if let updatedAt = store.updatedAt {
if let updatedAt = self.store.updatedAt {
Text("Updated \(updatedAt.formatted(date: .omitted, time: .shortened))")
.font(.footnote)
.foregroundStyle(.secondary)
@@ -60,5 +305,6 @@ struct WatchInboxView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
.navigationTitle("OpenClaw")
}
}

View File

@@ -29,6 +29,8 @@ ASC_KEYCHAIN_SERVICE=openclaw-asc-key
ASC_KEYCHAIN_ACCOUNT=YOUR_MAC_USERNAME
```
Important: `apps/ios/fastlane/.env` is only for Fastlane/App Store Connect auth and optional beta-archive settings. It does **not** configure gateway-side direct APNs push delivery for local iOS builds.
Optional app targeting variables (helpful if Fastlane cannot auto-resolve app by bundle):
```bash
@@ -53,6 +55,8 @@ IOS_DEVELOPMENT_TEAM=YOUR_TEAM_ID
Tip: run `scripts/ios-team-id.sh` from repo root to print a Team ID for `.env`. The helper prefers the canonical OpenClaw team (`Y5PE65HELJ`) when present locally; otherwise it prefers the first non-personal team from your Xcode account (then personal team if needed). Fastlane uses this helper automatically if `IOS_DEVELOPMENT_TEAM` is missing.
For local/manual iOS builds that stay on direct APNs, configure the gateway host separately with `OPENCLAW_APNS_TEAM_ID`, `OPENCLAW_APNS_KEY_ID`, and either `OPENCLAW_APNS_PRIVATE_KEY_P8` or `OPENCLAW_APNS_PRIVATE_KEY_PATH`. Those gateway runtime env vars are separate from Fastlane's `.env`.
Validate auth:
```bash
@@ -86,6 +90,43 @@ cd apps/ios
fastlane ios beta
```
Maintainer recovery path for a fresh clone on the same Mac:
1. Reuse the existing Keychain-backed ASC key on that machine.
2. Restore or recreate `apps/ios/fastlane/.env` so it contains the non-secret variables:
```bash
ASC_KEY_ID=YOUR_KEY_ID
ASC_ISSUER_ID=YOUR_ISSUER_ID
ASC_KEYCHAIN_SERVICE=openclaw-asc-key
ASC_KEYCHAIN_ACCOUNT=YOUR_MAC_USERNAME
```
3. Re-run auth validation:
```bash
cd apps/ios
fastlane ios auth_check
```
4. Set the official/TestFlight relay URL before release:
```bash
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
```
5. Upload:
```bash
pnpm ios:beta
```
Quick verification after upload:
- confirm `apps/ios/build/beta/OpenClaw-<version>.ipa` exists
- confirm Fastlane prints `Uploaded iOS beta: version=<version> short=<short> build=<build>`
- remember that TestFlight processing can take a few minutes after the upload succeeds
Versioning rules:
- Root `package.json.version` is the single source of truth for iOS

View File

@@ -237,12 +237,19 @@ targets:
configFiles:
Debug: Config/Signing.xcconfig
Release: Config/Signing.xcconfig
attributes:
DevelopmentTeam: "$(OPENCLAW_DEVELOPMENT_TEAM)"
ProvisioningStyle: "$(OPENCLAW_CODE_SIGN_STYLE)"
settings:
base:
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
ENABLE_APPINTENTS_METADATA: NO
ENABLE_APP_INTENTS_METADATA_GENERATION: NO
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_WATCH_APP_BUNDLE_ID)"
PROVISIONING_PROFILE_SPECIFIER: "$(OPENCLAW_WATCH_APP_PROFILE)"
info:
path: WatchApp/Info.plist
properties:
@@ -265,9 +272,16 @@ targets:
configFiles:
Debug: Config/Signing.xcconfig
Release: Config/Signing.xcconfig
attributes:
DevelopmentTeam: "$(OPENCLAW_DEVELOPMENT_TEAM)"
ProvisioningStyle: "$(OPENCLAW_CODE_SIGN_STYLE)"
settings:
base:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_WATCH_EXTENSION_BUNDLE_ID)"
PROVISIONING_PROFILE_SPECIFIER: "$(OPENCLAW_WATCH_EXTENSION_PROFILE)"
info:
path: WatchExtension/Info.plist
properties:

View File

@@ -299,6 +299,10 @@ enum GatewayEnvironment {
if normalized.lowercased().hasPrefix("openclaw ") {
normalized = String(normalized.dropFirst("openclaw ".count))
}
// Strip trailing commit metadata, e.g. "2026.4.2 (d74a122)" "2026.4.2"
if let parenRange = normalized.range(of: #"\s*\([0-9a-fA-F]+\)\s*$"#, options: .regularExpression) {
normalized = String(normalized[normalized.startIndex..<parenRange.lowerBound])
}
return normalized
}

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.4.3</string>
<string>2026.4.6</string>
<key>CFBundleVersion</key>
<string>2026040301</string>
<string>2026040601</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -43,8 +43,8 @@ enum WideAreaGatewayDiscovery {
guard let statusJson = context.tailscaleStatus(),
!collectTailnetIPv4s(statusJson: statusJson).isEmpty,
let discovery = loadWideAreaPtrRecords(
remaining: remaining,
dig: context.dig)
remaining: remaining,
dig: context.dig)
else { return [] }
let domainTrimmed = discovery.domainTrimmed

View File

@@ -2019,6 +2019,7 @@ public struct TalkSpeakParams: Codable, Sendable {
public let modelid: String?
public let outputformat: String?
public let speed: Double?
public let ratewpm: Int?
public let stability: Double?
public let similarity: Double?
public let style: Double?
@@ -2026,6 +2027,7 @@ public struct TalkSpeakParams: Codable, Sendable {
public let seed: Int?
public let normalize: String?
public let language: String?
public let latencytier: Int?
public init(
text: String,
@@ -2033,19 +2035,22 @@ public struct TalkSpeakParams: Codable, Sendable {
modelid: String?,
outputformat: String?,
speed: Double?,
ratewpm: Int?,
stability: Double?,
similarity: Double?,
style: Double?,
speakerboost: Bool?,
seed: Int?,
normalize: String?,
language: String?)
language: String?,
latencytier: Int?)
{
self.text = text
self.voiceid = voiceid
self.modelid = modelid
self.outputformat = outputformat
self.speed = speed
self.ratewpm = ratewpm
self.stability = stability
self.similarity = similarity
self.style = style
@@ -2053,6 +2058,7 @@ public struct TalkSpeakParams: Codable, Sendable {
self.seed = seed
self.normalize = normalize
self.language = language
self.latencytier = latencytier
}
private enum CodingKeys: String, CodingKey {
@@ -2061,6 +2067,7 @@ public struct TalkSpeakParams: Codable, Sendable {
case modelid = "modelId"
case outputformat = "outputFormat"
case speed
case ratewpm = "rateWpm"
case stability
case similarity
case style
@@ -2068,6 +2075,7 @@ public struct TalkSpeakParams: Codable, Sendable {
case seed
case normalize
case language
case latencytier = "latencyTier"
}
}
@@ -3411,6 +3419,20 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
}
}
public struct ExecApprovalGetParams: Codable, Sendable {
public let id: String
public init(
id: String)
{
self.id = id
}
private enum CodingKeys: String, CodingKey {
case id
}
}
public struct ExecApprovalRequestParams: Codable, Sendable {
public let id: String?
public let command: String?

View File

@@ -30,6 +30,17 @@ struct GatewayEnvironmentTests {
#expect(Semver.parse(normalized) == Semver(major: 2026, minor: 3, patch: 23))
}
@Test func `gateway version output strips trailing commit hash`() {
let normalized = GatewayEnvironment.normalizeGatewayVersionOutput("OpenClaw 2026.4.2 (d74a122)")
#expect(normalized == "2026.4.2")
#expect(Semver.parse(normalized) == Semver(major: 2026, minor: 4, patch: 2))
// Pre-release suffix + commit hash combined
let normalized2 = GatewayEnvironment.normalizeGatewayVersionOutput("OpenClaw 2026.4.2-1 (d74a122)")
#expect(normalized2 == "2026.4.2-1")
#expect(Semver.parse(normalized2) == Semver(major: 2026, minor: 4, patch: 2))
}
@Test func `semver compatibility requires same major and not older`() {
let required = Semver(major: 2, minor: 1, patch: 0)
#expect(Semver(major: 2, minor: 1, patch: 0).compatible(with: required))

View File

@@ -542,6 +542,77 @@ public actor GatewayChannelActor {
authSource: authSource)
}
private func shouldPersistBootstrapHandoffTokens() -> Bool {
guard self.lastAuthSource == .bootstrapToken else { return false }
let scheme = self.url.scheme?.lowercased()
if scheme == "wss" {
return true
}
if let host = self.url.host, LoopbackHost.isLoopback(host) {
return true
}
return false
}
private func filteredBootstrapHandoffScopes(role: String, scopes: [String]) -> [String]? {
let normalizedRole = role.trimmingCharacters(in: .whitespacesAndNewlines)
switch normalizedRole {
case "node":
return []
case "operator":
let allowedOperatorScopes: Set<String> = [
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
]
return Array(Set(scopes.filter { allowedOperatorScopes.contains($0) })).sorted()
default:
return nil
}
}
private func persistBootstrapHandoffToken(
deviceId: String,
role: String,
token: String,
scopes: [String]
) {
guard let filteredScopes = self.filteredBootstrapHandoffScopes(role: role, scopes: scopes) else {
return
}
_ = DeviceAuthStore.storeToken(
deviceId: deviceId,
role: role,
token: token,
scopes: filteredScopes)
}
private func persistIssuedDeviceToken(
authSource: GatewayAuthSource,
deviceId: String,
role: String,
token: String,
scopes: [String]
) {
if authSource == .bootstrapToken {
guard self.shouldPersistBootstrapHandoffTokens() else {
return
}
self.persistBootstrapHandoffToken(
deviceId: deviceId,
role: role,
token: token,
scopes: scopes)
return
}
_ = DeviceAuthStore.storeToken(
deviceId: deviceId,
role: role,
token: token,
scopes: scopes)
}
private func handleConnectResponse(
_ res: ResponseFrame,
identity: DeviceIdentity?,
@@ -572,18 +643,37 @@ public actor GatewayChannelActor {
} else if let tick = ok.policy["tickIntervalMs"]?.value as? Int {
self.tickIntervalMs = Double(tick)
}
if let auth = ok.auth,
let deviceToken = auth["deviceToken"]?.value as? String {
let authRole = auth["role"]?.value as? String ?? role
let scopes = (auth["scopes"]?.value as? [ProtoAnyCodable])?
.compactMap { $0.value as? String } ?? []
if let identity {
_ = DeviceAuthStore.storeToken(
if let auth = ok.auth, let identity {
if let deviceToken = auth["deviceToken"]?.value as? String {
let authRole = auth["role"]?.value as? String ?? role
let scopes = (auth["scopes"]?.value as? [ProtoAnyCodable])?
.compactMap { $0.value as? String } ?? []
self.persistIssuedDeviceToken(
authSource: self.lastAuthSource,
deviceId: identity.deviceId,
role: authRole,
token: deviceToken,
scopes: scopes)
}
if self.shouldPersistBootstrapHandoffTokens(),
let tokenEntries = auth["deviceTokens"]?.value as? [ProtoAnyCodable]
{
for entry in tokenEntries {
guard let rawEntry = entry.value as? [String: ProtoAnyCodable],
let deviceToken = rawEntry["deviceToken"]?.value as? String,
let authRole = rawEntry["role"]?.value as? String
else {
continue
}
let scopes = (rawEntry["scopes"]?.value as? [ProtoAnyCodable])?
.compactMap { $0.value as? String } ?? []
self.persistBootstrapHandoffToken(
deviceId: identity.deviceId,
role: authRole,
token: deviceToken,
scopes: scopes)
}
}
}
self.lastTick = Date()
self.tickTask?.cancel()

View File

@@ -127,6 +127,12 @@ public struct GatewayResponseError: LocalizedError, @unchecked Sendable {
self.details = details ?? [:]
}
public var detailsReason: String? {
let raw = self.details["reason"]?.value as? String
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}
public var errorDescription: String? {
if self.code == "GATEWAY_ERROR" { return "\(self.method): \(self.message)" }
return "\(self.method): [\(self.code)] \(self.message)"

View File

@@ -130,44 +130,7 @@ public enum ToolDisplayRegistry {
"messageId",
],
actions: nil),
tools: [
"bash": ToolDisplaySpec(
emoji: "🛠️",
title: "Bash",
label: nil,
detailKeys: ["command"],
actions: nil),
"read": ToolDisplaySpec(
emoji: "📖",
title: "Read",
label: nil,
detailKeys: ["path"],
actions: nil),
"write": ToolDisplaySpec(
emoji: "✍️",
title: "Write",
label: nil,
detailKeys: ["path"],
actions: nil),
"edit": ToolDisplaySpec(
emoji: "📝",
title: "Edit",
label: nil,
detailKeys: ["path"],
actions: nil),
"attach": ToolDisplaySpec(
emoji: "📎",
title: "Attach",
label: nil,
detailKeys: ["path", "url", "fileName"],
actions: nil),
"process": ToolDisplaySpec(
emoji: "🧰",
title: "Process",
label: nil,
detailKeys: ["sessionId"],
actions: nil),
])
tools: nil)
}
private static func titleFromName(_ name: String) -> String {

View File

@@ -5,12 +5,36 @@ public enum OpenClawWatchCommand: String, Codable, Sendable {
case notify = "watch.notify"
}
public enum OpenClawWatchPayloadType: String, Codable, Sendable, Equatable {
case notify = "watch.notify"
case reply = "watch.reply"
case execApprovalPrompt = "watch.execApproval.prompt"
case execApprovalResolve = "watch.execApproval.resolve"
case execApprovalResolved = "watch.execApproval.resolved"
case execApprovalExpired = "watch.execApproval.expired"
case execApprovalSnapshot = "watch.execApproval.snapshot"
case execApprovalSnapshotRequest = "watch.execApproval.snapshotRequest"
}
public enum OpenClawWatchRisk: String, Codable, Sendable, Equatable {
case low
case medium
case high
}
public enum OpenClawWatchExecApprovalDecision: String, Codable, Sendable, Equatable {
case allowOnce = "allow-once"
case deny
}
public enum OpenClawWatchExecApprovalCloseReason: String, Codable, Sendable, Equatable {
case expired
case notFound = "not-found"
case unavailable
case replaced
case resolved
}
public struct OpenClawWatchAction: Codable, Sendable, Equatable {
public var id: String
public var label: String
@@ -23,6 +47,151 @@ public struct OpenClawWatchAction: Codable, Sendable, Equatable {
}
}
public struct OpenClawWatchExecApprovalItem: Codable, Sendable, Equatable, Identifiable {
public var id: String
public var commandText: String
public var commandPreview: String?
public var host: String?
public var nodeId: String?
public var agentId: String?
public var expiresAtMs: Int?
public var allowedDecisions: [OpenClawWatchExecApprovalDecision]
public var risk: OpenClawWatchRisk?
public init(
id: String,
commandText: String,
commandPreview: String? = nil,
host: String? = nil,
nodeId: String? = nil,
agentId: String? = nil,
expiresAtMs: Int? = nil,
allowedDecisions: [OpenClawWatchExecApprovalDecision] = [],
risk: OpenClawWatchRisk? = nil)
{
self.id = id
self.commandText = commandText
self.commandPreview = commandPreview
self.host = host
self.nodeId = nodeId
self.agentId = agentId
self.expiresAtMs = expiresAtMs
self.allowedDecisions = allowedDecisions
self.risk = risk
}
}
public struct OpenClawWatchExecApprovalPromptMessage: Codable, Sendable, Equatable {
public var type: OpenClawWatchPayloadType
public var approval: OpenClawWatchExecApprovalItem
public var sentAtMs: Int?
public var deliveryId: String?
public var resetResolvingState: Bool?
public init(
approval: OpenClawWatchExecApprovalItem,
sentAtMs: Int? = nil,
deliveryId: String? = nil,
resetResolvingState: Bool? = nil)
{
self.type = .execApprovalPrompt
self.approval = approval
self.sentAtMs = sentAtMs
self.deliveryId = deliveryId
self.resetResolvingState = resetResolvingState
}
}
public struct OpenClawWatchExecApprovalResolveMessage: Codable, Sendable, Equatable {
public var type: OpenClawWatchPayloadType
public var approvalId: String
public var decision: OpenClawWatchExecApprovalDecision
public var replyId: String
public var sentAtMs: Int?
public init(
approvalId: String,
decision: OpenClawWatchExecApprovalDecision,
replyId: String,
sentAtMs: Int? = nil)
{
self.type = .execApprovalResolve
self.approvalId = approvalId
self.decision = decision
self.replyId = replyId
self.sentAtMs = sentAtMs
}
}
public struct OpenClawWatchExecApprovalResolvedMessage: Codable, Sendable, Equatable {
public var type: OpenClawWatchPayloadType
public var approvalId: String
public var decision: OpenClawWatchExecApprovalDecision?
public var resolvedAtMs: Int?
public var source: String?
public init(
approvalId: String,
decision: OpenClawWatchExecApprovalDecision? = nil,
resolvedAtMs: Int? = nil,
source: String? = nil)
{
self.type = .execApprovalResolved
self.approvalId = approvalId
self.decision = decision
self.resolvedAtMs = resolvedAtMs
self.source = source
}
}
public struct OpenClawWatchExecApprovalExpiredMessage: Codable, Sendable, Equatable {
public var type: OpenClawWatchPayloadType
public var approvalId: String
public var reason: OpenClawWatchExecApprovalCloseReason
public var expiredAtMs: Int?
public init(
approvalId: String,
reason: OpenClawWatchExecApprovalCloseReason,
expiredAtMs: Int? = nil)
{
self.type = .execApprovalExpired
self.approvalId = approvalId
self.reason = reason
self.expiredAtMs = expiredAtMs
}
}
public struct OpenClawWatchExecApprovalSnapshotMessage: Codable, Sendable, Equatable {
public var type: OpenClawWatchPayloadType
public var approvals: [OpenClawWatchExecApprovalItem]
public var sentAtMs: Int?
public var snapshotId: String?
public init(
approvals: [OpenClawWatchExecApprovalItem],
sentAtMs: Int? = nil,
snapshotId: String? = nil)
{
self.type = .execApprovalSnapshot
self.approvals = approvals
self.sentAtMs = sentAtMs
self.snapshotId = snapshotId
}
}
public struct OpenClawWatchExecApprovalSnapshotRequestMessage: Codable, Sendable, Equatable {
public var type: OpenClawWatchPayloadType
public var requestId: String
public var sentAtMs: Int?
public init(requestId: String, sentAtMs: Int? = nil) {
self.type = .execApprovalSnapshotRequest
self.requestId = requestId
self.sentAtMs = sentAtMs
}
}
public struct OpenClawWatchStatusPayload: Codable, Sendable, Equatable {
public var supported: Bool
public var paired: Bool

View File

@@ -2019,6 +2019,7 @@ public struct TalkSpeakParams: Codable, Sendable {
public let modelid: String?
public let outputformat: String?
public let speed: Double?
public let ratewpm: Int?
public let stability: Double?
public let similarity: Double?
public let style: Double?
@@ -2026,6 +2027,7 @@ public struct TalkSpeakParams: Codable, Sendable {
public let seed: Int?
public let normalize: String?
public let language: String?
public let latencytier: Int?
public init(
text: String,
@@ -2033,19 +2035,22 @@ public struct TalkSpeakParams: Codable, Sendable {
modelid: String?,
outputformat: String?,
speed: Double?,
ratewpm: Int?,
stability: Double?,
similarity: Double?,
style: Double?,
speakerboost: Bool?,
seed: Int?,
normalize: String?,
language: String?)
language: String?,
latencytier: Int?)
{
self.text = text
self.voiceid = voiceid
self.modelid = modelid
self.outputformat = outputformat
self.speed = speed
self.ratewpm = ratewpm
self.stability = stability
self.similarity = similarity
self.style = style
@@ -2053,6 +2058,7 @@ public struct TalkSpeakParams: Codable, Sendable {
self.seed = seed
self.normalize = normalize
self.language = language
self.latencytier = latencytier
}
private enum CodingKeys: String, CodingKey {
@@ -2061,6 +2067,7 @@ public struct TalkSpeakParams: Codable, Sendable {
case modelid = "modelId"
case outputformat = "outputFormat"
case speed
case ratewpm = "rateWpm"
case stability
case similarity
case style
@@ -2068,6 +2075,7 @@ public struct TalkSpeakParams: Codable, Sendable {
case seed
case normalize
case language
case latencytier = "latencyTier"
}
}
@@ -3411,6 +3419,20 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
}
}
public struct ExecApprovalGetParams: Codable, Sendable {
public let id: String
public init(
id: String)
{
self.id = id
}
private enum CodingKeys: String, CodingKey {
case id
}
}
public struct ExecApprovalRequestParams: Codable, Sendable {
public let id: String?
public let command: String?

View File

@@ -13,6 +13,7 @@ private extension NSLock {
private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Sendable {
private let lock = NSLock()
private let helloAuth: [String: Any]?
private var _state: URLSessionTask.State = .suspended
private var connectRequestId: String?
private var connectAuth: [String: Any]?
@@ -20,6 +21,10 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
private var pendingReceiveHandler:
(@Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)?
init(helloAuth: [String: Any]? = nil) {
self.helloAuth = helloAuth
}
var state: URLSessionTask.State {
get { self.lock.withLock { self._state } }
set { self.lock.withLock { self._state = newValue } }
@@ -79,11 +84,11 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
for _ in 0..<50 {
let id = self.lock.withLock { self.connectRequestId }
if let id {
return .data(Self.connectOkData(id: id))
return .data(Self.connectOkData(id: id, auth: self.helloAuth))
}
try await Task.sleep(nanoseconds: 1_000_000)
}
return .data(Self.connectOkData(id: "connect"))
return .data(Self.connectOkData(id: "connect", auth: self.helloAuth))
}
func receive(
@@ -110,8 +115,8 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
return (try? JSONSerialization.data(withJSONObject: frame)) ?? Data()
}
private static func connectOkData(id: String) -> Data {
let payload: [String: Any] = [
private static func connectOkData(id: String, auth: [String: Any]? = nil) -> Data {
var payload: [String: Any] = [
"type": "hello-ok",
"protocol": 2,
"server": [
@@ -137,6 +142,9 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
"tickIntervalMs": 30_000,
],
]
if let auth {
payload["auth"] = auth
}
let frame: [String: Any] = [
"type": "res",
"id": id,
@@ -149,9 +157,14 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
private final class FakeGatewayWebSocketSession: WebSocketSessioning, @unchecked Sendable {
private let lock = NSLock()
private let helloAuth: [String: Any]?
private var tasks: [FakeGatewayWebSocketTask] = []
private var makeCount = 0
init(helloAuth: [String: Any]? = nil) {
self.helloAuth = helloAuth
}
func snapshotMakeCount() -> Int {
self.lock.withLock { self.makeCount }
}
@@ -164,7 +177,7 @@ private final class FakeGatewayWebSocketSession: WebSocketSessioning, @unchecked
_ = url
return self.lock.withLock {
self.makeCount += 1
let task = FakeGatewayWebSocketTask()
let task = FakeGatewayWebSocketTask(helloAuth: self.helloAuth)
self.tasks.append(task)
return WebSocketTaskBox(task: task)
}
@@ -177,6 +190,7 @@ private actor SeqGapProbe {
func value() -> Bool { self.saw }
}
@Suite(.serialized)
struct GatewayNodeSessionTests {
@Test
func scannedSetupCodePrefersBootstrapAuthOverStoredDeviceToken() async throws {
@@ -234,6 +248,210 @@ struct GatewayNodeSessionTests {
await gateway.disconnect()
}
@Test
func bootstrapHelloStoresAdditionalDeviceTokens() async throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
let previousStateDir = ProcessInfo.processInfo.environment["OPENCLAW_STATE_DIR"]
setenv("OPENCLAW_STATE_DIR", tempDir.path, 1)
defer {
if let previousStateDir {
setenv("OPENCLAW_STATE_DIR", previousStateDir, 1)
} else {
unsetenv("OPENCLAW_STATE_DIR")
}
try? FileManager.default.removeItem(at: tempDir)
}
let identity = DeviceIdentityStore.loadOrCreate()
let session = FakeGatewayWebSocketSession(helloAuth: [
"deviceToken": "node-device-token",
"role": "node",
"scopes": [],
"issuedAtMs": 1000,
"deviceTokens": [
[
"deviceToken": "operator-device-token",
"role": "operator",
"scopes": [
"node.exec",
"operator.admin",
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.talk.secrets",
"operator.write",
],
"issuedAtMs": 1001,
],
],
])
let gateway = GatewayNodeSession()
let options = GatewayConnectOptions(
role: "node",
scopes: [],
caps: [],
commands: [],
permissions: [:],
clientId: "openclaw-ios-test",
clientMode: "node",
clientDisplayName: "iOS Test",
includeDeviceIdentity: true)
try await gateway.connect(
url: URL(string: "wss://example.invalid")!,
token: nil,
bootstrapToken: "fresh-bootstrap-token",
password: nil,
connectOptions: options,
sessionBox: WebSocketSessionBox(session: session),
onConnected: {},
onDisconnected: { _ in },
onInvoke: { req in
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
})
let nodeEntry = try #require(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "node"))
let operatorEntry = try #require(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "operator"))
#expect(nodeEntry.token == "node-device-token")
#expect(nodeEntry.scopes == [])
#expect(operatorEntry.token == "operator-device-token")
#expect(operatorEntry.scopes == [
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
])
await gateway.disconnect()
}
@Test
func nonBootstrapHelloStoresPrimaryDeviceTokenButNotAdditionalBootstrapTokens() async throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
let previousStateDir = ProcessInfo.processInfo.environment["OPENCLAW_STATE_DIR"]
setenv("OPENCLAW_STATE_DIR", tempDir.path, 1)
defer {
if let previousStateDir {
setenv("OPENCLAW_STATE_DIR", previousStateDir, 1)
} else {
unsetenv("OPENCLAW_STATE_DIR")
}
try? FileManager.default.removeItem(at: tempDir)
}
let identity = DeviceIdentityStore.loadOrCreate()
let session = FakeGatewayWebSocketSession(helloAuth: [
"deviceToken": "server-node-token",
"role": "node",
"scopes": [],
"deviceTokens": [
[
"deviceToken": "server-operator-token",
"role": "operator",
"scopes": ["operator.admin"],
],
],
])
let gateway = GatewayNodeSession()
let options = GatewayConnectOptions(
role: "node",
scopes: [],
caps: [],
commands: [],
permissions: [:],
clientId: "openclaw-ios-test",
clientMode: "node",
clientDisplayName: "iOS Test",
includeDeviceIdentity: true)
try await gateway.connect(
url: URL(string: "wss://example.invalid")!,
token: "shared-token",
bootstrapToken: nil,
password: nil,
connectOptions: options,
sessionBox: WebSocketSessionBox(session: session),
onConnected: {},
onDisconnected: { _ in },
onInvoke: { req in
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
})
let nodeEntry = try #require(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "node"))
#expect(nodeEntry.token == "server-node-token")
#expect(nodeEntry.scopes == [])
#expect(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "operator") == nil)
await gateway.disconnect()
}
@Test
func untrustedBootstrapHelloDoesNotPersistBootstrapHandoffTokens() async throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
let previousStateDir = ProcessInfo.processInfo.environment["OPENCLAW_STATE_DIR"]
setenv("OPENCLAW_STATE_DIR", tempDir.path, 1)
defer {
if let previousStateDir {
setenv("OPENCLAW_STATE_DIR", previousStateDir, 1)
} else {
unsetenv("OPENCLAW_STATE_DIR")
}
try? FileManager.default.removeItem(at: tempDir)
}
let identity = DeviceIdentityStore.loadOrCreate()
let session = FakeGatewayWebSocketSession(helloAuth: [
"deviceToken": "untrusted-node-token",
"role": "node",
"scopes": [],
"deviceTokens": [
[
"deviceToken": "untrusted-operator-token",
"role": "operator",
"scopes": [
"operator.approvals",
"operator.read",
],
],
],
])
let gateway = GatewayNodeSession()
let options = GatewayConnectOptions(
role: "node",
scopes: [],
caps: [],
commands: [],
permissions: [:],
clientId: "openclaw-ios-test",
clientMode: "node",
clientDisplayName: "iOS Test",
includeDeviceIdentity: true)
try await gateway.connect(
url: URL(string: "ws://example.invalid")!,
token: nil,
bootstrapToken: "fresh-bootstrap-token",
password: nil,
connectOptions: options,
sessionBox: WebSocketSessionBox(session: session),
onConnected: {},
onDisconnected: { _ in },
onInvoke: { req in
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
})
#expect(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "node") == nil)
#expect(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "operator") == nil)
await gateway.disconnect()
}
@Test
func normalizeCanvasHostUrlPreservesExplicitSecureCanvasPort() {
let normalized = canonicalizeCanvasHostUrl(

View File

@@ -1,6 +1,7 @@
import XCTest
@testable import OpenClawKit
@MainActor
final class TalkSystemSpeechSynthesizerTests: XCTestCase {
func testWatchdogTimeoutDefaultsToLatinProfile() {
let timeout = TalkSystemSpeechSynthesizer.watchdogTimeoutSeconds(

View File

@@ -9,8 +9,8 @@ import Testing
}
@Test func resolvesKnownToolFromConfig() {
let summary = ToolDisplayRegistry.resolve(name: "bash", args: nil)
let summary = ToolDisplayRegistry.resolve(name: "exec", args: nil)
#expect(summary.emoji == "🛠️")
#expect(summary.title == "Bash")
#expect(summary.title == "Exec")
}
}

View File

@@ -1,14 +1,21 @@
# Generated Docs Artifacts
These baseline artifacts are generated from the repo-owned OpenClaw config schema and bundled channel/plugin metadata.
SHA-256 hash files are the tracked drift-detection artifacts. The full JSON
baselines are generated locally (gitignored) for inspection only.
- Do not edit `config-baseline.json` by hand.
- Do not edit `config-baseline.core.json` by hand.
- Do not edit `config-baseline.channel.json` by hand.
- Do not edit `config-baseline.plugin.json` by hand.
- Do not edit `plugin-sdk-api-baseline.json` by hand.
- Do not edit `plugin-sdk-api-baseline.jsonl` by hand.
- Regenerate config baseline artifacts with `pnpm config:docs:gen`.
- Validate config baseline artifacts in CI or locally with `pnpm config:docs:check`.
- Regenerate Plugin SDK API baseline artifacts with `pnpm plugin-sdk:api:gen`.
- Validate Plugin SDK API baseline artifacts in CI or locally with `pnpm plugin-sdk:api:check`.
**Tracked (committed to git):**
- `config-baseline.sha256` — hashes of config baseline JSON artifacts.
- `plugin-sdk-api-baseline.sha256` — hashes of Plugin SDK API baseline artifacts.
**Local only (gitignored):**
- `config-baseline.json`, `config-baseline.core.json`, `config-baseline.channel.json`, `config-baseline.plugin.json`
- `plugin-sdk-api-baseline.json`, `plugin-sdk-api-baseline.jsonl`
Do not edit any of these files by hand.
- Regenerate config baseline: `pnpm config:docs:gen`
- Validate config baseline: `pnpm config:docs:check`
- Regenerate Plugin SDK API baseline: `pnpm plugin-sdk:api:gen`
- Validate Plugin SDK API baseline: `pnpm plugin-sdk:api:check`

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
1c74540dd152c55dbda3e5dee1e37008ee3e6aabb0608e571292832c7a1c012c config-baseline.json
7e30316f2326b7d07b71d7b8a96049a74b81428921299b5c4b5aa3d080e03305 config-baseline.core.json
66edc86a9d16db1b9e9e7dd99b7032e2d9bcfb9ff210256a21f4b4f088cb3dc1 config-baseline.channel.json
d6ebc4948499b997c4a3727cf31849d4a598de9f1a4c197417dcc0b0ec1b734f config-baseline.plugin.json

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
9883b1242051e830bafa7035351c9a2dd0fb84f81be28d7b5be2b69a1179e519 plugin-sdk-api-baseline.json
43dd28ba4502b207413d00471ea2e4ae5cf644922ab153387fa4bf99e540e6d1 plugin-sdk-api-baseline.jsonl

View File

@@ -1,11 +1,42 @@
# OpenClaw docs i18n assets
This folder stores **generated** and **config** files for documentation translations.
This folder stores translation config for the source docs repo.
## Files
Generated locale trees and live translation memory now live in the publish repo:
- `glossary.<lang>.json` — preferred term mappings (used in prompt guidance).
- `<lang>.tm.jsonl` — translation memory (cache) keyed by workflow + model + text hash.
- repo: `openclaw/docs`
- local checkout: `~/Projects/openclaw-docs`
## Source of truth
- English docs are authored in `openclaw/openclaw`.
- The source docs tree lives under `docs/`.
- The source repo no longer keeps committed generated locale trees such as `docs/zh-CN/**`, `docs/ja-JP/**`, `docs/es/**`, `docs/pt-BR/**`, `docs/ko/**`, `docs/de/**`, `docs/fr/**`, `docs/ar/**`, `docs/it/**`, `docs/tr/**`, `docs/uk/**`, `docs/id/**`, or `docs/pl/**`.
## End-to-end flow
1. Edit English docs in `openclaw/openclaw`.
2. Push to `main`.
3. `openclaw/openclaw/.github/workflows/docs-sync-publish.yml` mirrors the docs tree into `openclaw/docs`.
4. The sync script rewrites the publish `docs/docs.json` so the generated locale picker blocks exist there even though they are no longer committed in the source repo.
5. `openclaw/docs/.github/workflows/translate-zh-cn.yml` refreshes `docs/zh-CN/**` once a day, on demand, and after source-repo release dispatches.
6. `openclaw/docs/.github/workflows/translate-ja-jp.yml` does the same for `docs/ja-JP/**`.
7. `openclaw/docs/.github/workflows/translate-es.yml`, `translate-pt-br.yml`, `translate-ko.yml`, `translate-de.yml`, `translate-fr.yml`, `translate-ar.yml`, `translate-it.yml`, `translate-tr.yml`, `translate-uk.yml`, `translate-id.yml`, and `translate-pl.yml` do the same for `docs/es/**`, `docs/pt-BR/**`, `docs/ko/**`, `docs/de/**`, `docs/fr/**`, `docs/ar/**`, `docs/it/**`, `docs/tr/**`, `docs/uk/**`, `docs/id/**`, and `docs/pl/**`.
## Why the split exists
- Keep generated locale output out of the main product repo.
- Keep Mintlify on a single published docs tree.
- Preserve the built-in language switcher by letting the publish repo own generated locale trees.
## Files in this folder
- `glossary.<lang>.json` — preferred term mappings used as prompt guidance.
- `zh-Hans-navigation.json` — curated zh-Hans Mintlify locale navigation reinserted into the publish repo during sync.
- `ar-navigation.json`, `de-navigation.json`, `es-navigation.json`, `fr-navigation.json`, `id-navigation.json`, `it-navigation.json`, `ja-navigation.json`, `ko-navigation.json`, `pl-navigation.json`, `pt-BR-navigation.json`, `tr-navigation.json` — starter locale metadata kept alongside the source repo, but the publish sync now clones the full English nav tree for these locales so translated pages are visible in Mintlify without hand-maintaining per-locale nav JSON.
- `<lang>.tm.jsonl` — translation memory keyed by workflow + model + text hash.
In this repo, generated locale TM files such as `docs/.i18n/zh-CN.tm.jsonl`, `docs/.i18n/ja-JP.tm.jsonl`, `docs/.i18n/es.tm.jsonl`, `docs/.i18n/pt-BR.tm.jsonl`, `docs/.i18n/ko.tm.jsonl`, `docs/.i18n/de.tm.jsonl`, `docs/.i18n/fr.tm.jsonl`, `docs/.i18n/ar.tm.jsonl`, `docs/.i18n/it.tm.jsonl`, `docs/.i18n/tr.tm.jsonl`, `docs/.i18n/uk.tm.jsonl`, `docs/.i18n/id.tm.jsonl`, and `docs/.i18n/pl.tm.jsonl` are intentionally no longer committed.
## Glossary format
@@ -14,9 +45,7 @@ This folder stores **generated** and **config** files for documentation translat
```json
{
"source": "troubleshooting",
"target": "故障排除",
"ignore_case": true,
"whole_word": false
"target": "故障排除"
}
```
@@ -25,7 +54,19 @@ Fields:
- `source`: English (or source) phrase to prefer.
- `target`: preferred translation output.
## Notes
## Translation mechanics
- Glossary entries are passed to the model as **prompt guidance** (no deterministic rewrites).
- The translation memory is updated by `scripts/docs-i18n`.
- `scripts/docs-i18n` still owns translation generation.
- Doc mode writes `x-i18n.source_hash` into each translated page.
- Each publish workflow precomputes a pending file list by comparing the current English source hash to the stored locale `x-i18n.source_hash`.
- If the pending count is `0`, the expensive translation step is skipped entirely.
- If there are pending files, the workflow translates only those files.
- The publish workflow retries transient model-format failures, but unchanged files stay skipped because the same hash check runs on each retry.
- The source repo also dispatches zh-CN, ja-JP, es, pt-BR, ko, de, fr, ar, it, tr, uk, id, and pl refreshes after published GitHub releases so release docs can catch up without waiting for the daily cron.
## Operational notes
- Sync metadata is written to `.openclaw-sync/source.json` in the publish repo.
- Source repo secret: `OPENCLAW_DOCS_SYNC_TOKEN`
- Publish repo secret: `OPENCLAW_DOCS_I18N_OPENAI_API_KEY`
- If locale output looks stale, check the matching `Translate <locale>` workflow in `openclaw/docs` first.

View File

@@ -0,0 +1,18 @@
{
"language": "ar",
"tabs": [
{
"tab": "ابدأ",
"groups": [
{
"group": "نظرة عامة",
"pages": ["ar/index"]
},
{
"group": "الخطوات الأولى",
"pages": ["ar/start/getting-started", "ar/start/wizard"]
}
]
}
]
}

View File

@@ -0,0 +1,18 @@
{
"language": "de",
"tabs": [
{
"tab": "Loslegen",
"groups": [
{
"group": "Überblick",
"pages": ["de/index"]
},
{
"group": "Erste Schritte",
"pages": ["de/start/getting-started", "de/start/wizard"]
}
]
}
]
}

View File

@@ -0,0 +1,18 @@
{
"language": "es",
"tabs": [
{
"tab": "Comenzar",
"groups": [
{
"group": "Resumen",
"pages": ["es/index"]
},
{
"group": "Primeros pasos",
"pages": ["es/start/getting-started", "es/start/wizard"]
}
]
}
]
}

View File

@@ -0,0 +1,18 @@
{
"language": "fr",
"tabs": [
{
"tab": "Commencer",
"groups": [
{
"group": "Vue d'ensemble",
"pages": ["fr/index"]
},
{
"group": "Premiers pas",
"pages": ["fr/start/getting-started", "fr/start/wizard"]
}
]
}
]
}

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