Compare commits

..

1827 Commits

Author SHA1 Message Date
Gustavo Madeira Santana
4ceb4ada89 fix(auto-reply): harden fallback lifecycle formatting 2026-02-19 03:27:36 -05:00
Gustavo Madeira Santana
d9bf54fe8a status: compact fallback model presentation 2026-02-19 03:20:21 -05:00
Gustavo Madeira Santana
e4251b0d25 fix(auto-reply): emit fallback lifecycle events with verbose off 2026-02-19 03:16:14 -05:00
joshavant
6d9ecdf432 feat(auto-reply): add model fallback transition visibility in verbose logs, status, and web ui
Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-02-19 03:16:14 -05:00
Peter Steinberger
ad4c784f20 test: collapse duplicate gateway token-generation cases 2026-02-19 08:15:32 +00:00
Peter Steinberger
b78fa57401 test: remove duplicate telegram de-linkify case 2026-02-19 08:11:42 +00:00
Vignesh Natarajan
d3dab089d7 fix: preserve reasoning stream partial contract (#20635) (thanks @obviyus) 2026-02-19 00:05:10 -08:00
Vignesh Natarajan
0ff506140d fix: clear matched tool errors and dedupe reasoning end 2026-02-19 00:05:10 -08:00
Ayaan Zaidi
221d50bc18 fix: preserve assistant partial stream during reasoning 2026-02-19 00:05:10 -08:00
Peter Steinberger
2cbf15eb66 ci: pin bun setup version to avoid API rate-limit flakes 2026-02-19 08:04:18 +00:00
Peter Steinberger
b97b8908b9 test: remove duplicate telegram .co link formatting case 2026-02-19 08:00:05 +00:00
Peter Steinberger
5f2bcfc4d2 ci: skip bun bootstrap in check and docs-check jobs 2026-02-19 07:58:54 +00:00
Peter Steinberger
9a490fbbeb test: drop duplicate followup compaction token assertion 2026-02-19 07:57:24 +00:00
Peter Steinberger
a82a41236e test(web): dedupe creds-update trigger helper in session tests 2026-02-19 07:52:32 +00:00
Peter Steinberger
18d4ad6aab test: trim duplicate cross-context policy cases 2026-02-19 07:50:38 +00:00
Peter Steinberger
bbb07bdc19 test(media): dedupe active-model fallback resolver setup 2026-02-19 07:50:10 +00:00
Peter Steinberger
ca71b5cc51 test(shell-env): dedupe repeated login-shell path lookups 2026-02-19 07:50:10 +00:00
Nimrod Gutman
9bd2261c0f fix(ios): auto-generate local signing overrides (#20716) 2026-02-19 15:48:46 +08:00
Peter Steinberger
8d7df30ee0 test: remove duplicate target-resolution cases from outbound suite 2026-02-19 07:47:17 +00:00
Peter Steinberger
57ea6feb03 test(gateway): dedupe startup auth override token checks 2026-02-19 07:45:27 +00:00
Peter Steinberger
ccd68d8166 test(subagents): dedupe sessions_spawn model expectation paths 2026-02-19 07:45:27 +00:00
Peter Steinberger
d7b2efc2e7 test(agents): dedupe ping-pong loop test scaffolding 2026-02-19 07:45:27 +00:00
Peter Steinberger
3cb0c96740 test(image-tool): dedupe repeated image tool fixture assertions 2026-02-19 07:45:27 +00:00
Peter Steinberger
1c04f5fcbb style: format extension relay imports 2026-02-19 07:44:06 +00:00
Peter Steinberger
ff1189c6d6 test: remove duplicate inbound-meta coverage from reply-flow 2026-02-19 07:41:52 +00:00
Peter Steinberger
7e54b6c96f fix(browser): unify extension relay auth on gateway token 2026-02-19 08:40:40 +01:00
Peter Steinberger
781b1c1e09 test(memory): dedupe voyage embedding provider test setup 2026-02-19 07:37:06 +00:00
Peter Steinberger
bd4fdfc356 test(reply): dedupe compaction session fixture setup 2026-02-19 07:37:06 +00:00
Gustavo Madeira Santana
c5698caca3 Security: default gateway auth bootstrap and explicit mode none (#20686)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: be1b73182c
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-19 02:35:50 -05:00
Peter Steinberger
a2e846f649 test: drop duplicate skills-cli integration coverage 2026-02-19 07:33:37 +00:00
Peter Steinberger
a4da6cfd53 test(update-cli): dedupe restart script test setup helpers 2026-02-19 07:33:16 +00:00
Peter Steinberger
4c68a09f08 test(discord): dedupe gateway proxy runtime fixture 2026-02-19 07:33:16 +00:00
Peter Steinberger
5556675aae test(gateway): dedupe APNs wake fixture setup in node invoke tests 2026-02-19 07:33:16 +00:00
Peter Steinberger
87d8331150 docs: warn against third-party 1-click marketplace images 2026-02-19 08:30:29 +01:00
Peter Steinberger
1d71c21aac test(web): dedupe media-failure setup in deliver reply tests 2026-02-19 07:27:47 +00:00
Peter Steinberger
0383c79c9c test(cli): dedupe account-option assertion in message helper tests 2026-02-19 07:27:42 +00:00
Peter Steinberger
9ac6f46735 test(messaging): dedupe parser/proxy/followup test scaffolding 2026-02-19 07:24:02 +00:00
Peter Steinberger
c085c9e6d0 test(browser): dedupe CDP and download setup helpers 2026-02-19 07:24:02 +00:00
Peter Steinberger
192366e0e8 test: dedupe shell env coverage from infra runtime suite 2026-02-19 07:21:26 +00:00
Peter Steinberger
c37cf02f29 test: make shell env path cache tests platform deterministic 2026-02-19 07:02:33 +00:00
Peter Steinberger
231f2af7df refactor(config): dedupe redacted snapshot array/object restore paths 2026-02-19 07:01:54 +00:00
Peter Steinberger
742fb90571 test(queue): cover collect drain helper states 2026-02-19 07:01:54 +00:00
Peter Steinberger
b22deada9e refactor(queue): reuse collect-mode item drain flow 2026-02-19 07:01:54 +00:00
Peter Steinberger
2f6b8663ff refactor(shared): reuse outbound text chunking core 2026-02-19 07:01:54 +00:00
Peter Steinberger
d5c58ce8d9 test: normalize boot-md mock workspace paths for cross-platform 2026-02-19 06:43:45 +00:00
Peter Steinberger
858286aecb refactor(cli): centralize memory manager setup wiring 2026-02-19 06:43:36 +00:00
Peter Steinberger
fa31f1cad2 refactor(cli): reuse allowlist mutation flow in approvals CLI 2026-02-19 06:43:36 +00:00
Peter Steinberger
8d048d412f refactor(queue): share next-item drain helper across queue drains 2026-02-19 06:43:36 +00:00
Gustavo Madeira Santana
6355bae1f9 test: make boot-md startup integration workspace assertion cross-platform 2026-02-19 01:14:06 -05:00
vikpos
f855d0be4f fix: skip heartbeat when HEARTBEAT.md does not exist (#20461)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: f6e5f8172a
Co-authored-by: vikpos <24960005+vikpos@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-19 01:09:33 -05:00
Marcus Castro
48e6b4fca3 fix: run BOOT.md for each configured agent at startup (#20569)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 9098a4cc64
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-19 00:58:56 -05:00
Ayaan Zaidi
d17a1f387b fix(telegram): unify inbound handling for message-like updates (#20591)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 442a100071
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-19 09:54:47 +05:30
Ayaan Zaidi
6b05916c14 fix: gate Telegram exec tool warnings behind verbose mode (#20560)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7ce94931f0
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-19 09:05:49 +05:30
Gustavo Madeira Santana
b228c06bbd chore: polish PR review skills 2026-02-18 22:24:41 -05:00
青雲
3d4ef56044 fix: include provider and model name in billing error message (#20510)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 40dbdf62e8
Co-authored-by: echoVic <16428813+echoVic@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-18 21:56:00 -05:00
Clawborn
2bb8ead187 Fix LaunchAgent missing TMPDIR causing SQLITE_CANTOPEN on macOS (#20512)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 25ba59765d
Co-authored-by: Clawborn <261310391+Clawborn@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-18 21:42:35 -05:00
Tyler Yust
c2b6f099c6 fix(agents): update SUBAGENT_SPAWN_ACCEPTED_NOTE to clarify response type 2026-02-18 16:57:13 -08:00
Peter Steinberger
e426a9bb6f refactor(config): reuse default group entry migration helper 2026-02-19 00:33:21 +00:00
Peter Steinberger
d6768098a1 refactor(security): share installed plugin directory scan helper 2026-02-19 00:29:07 +00:00
Peter Steinberger
6ae7e6fd1f refactor(config): reuse legacy audio transcription migration path 2026-02-19 00:29:00 +00:00
Peter Steinberger
2dd361c071 refactor(discord): share send target resolution and result mapping 2026-02-19 00:28:56 +00:00
Peter Steinberger
ac44190952 refactor(cli): dedupe device role validation for token ops 2026-02-19 00:28:51 +00:00
Peter Steinberger
c8bdefd8b4 refactor(security): reuse shared scan path containment helper 2026-02-19 00:20:15 +00:00
Peter Steinberger
ae2e6896da refactor(hooks): dedupe command result formatting 2026-02-19 00:20:10 +00:00
Peter Steinberger
aee002a39b refactor(agents): dedupe paragraph/newline break search in chunker 2026-02-19 00:17:38 +00:00
Peter Steinberger
989c9dbd37 refactor(auth): share remaining-time formatter 2026-02-19 00:17:31 +00:00
Peter Steinberger
b2c2737452 refactor(shared): reuse runtime entry requirement evaluator 2026-02-19 00:17:24 +00:00
Peter Steinberger
ef5d7cee22 refactor(agents): share fallback failure summary builder 2026-02-19 00:10:08 +00:00
Peter Steinberger
8e1f25631b test(agents): cover anthropic 4.6 forward-compat mapping 2026-02-19 00:06:30 +00:00
Peter Steinberger
cb9e098554 refactor(agents): dedupe anthropic 4.6 forward-compat resolver 2026-02-19 00:06:26 +00:00
Peter Steinberger
8b17a369e9 refactor(agents): share agent entry and block reply payload types 2026-02-19 00:06:19 +00:00
Peter Steinberger
5c5c032f42 refactor(security): share DM allowlist state resolver 2026-02-18 23:58:11 +00:00
Peter Steinberger
2709c0ba51 refactor(daemon): dedupe install output line writing 2026-02-18 23:58:05 +00:00
Peter Steinberger
89a0b95af4 refactor(security): reuse shared allowlist normalization 2026-02-18 23:48:32 +00:00
Peter Steinberger
54e9924fc3 refactor(agents): dedupe subagent inline text extraction 2026-02-18 23:48:32 +00:00
Peter Steinberger
3267f09264 refactor(node-host): extract invoke result helpers 2026-02-18 23:48:32 +00:00
Peter Steinberger
a376605812 refactor(infra): dedupe APNs send context setup 2026-02-18 23:48:32 +00:00
Peter Steinberger
aa8f87a3bf refactor(plugins): reuse plugin loader logger adapter 2026-02-18 23:48:32 +00:00
Peter Steinberger
a8ebe942aa refactor(cli): share camera clip file writer 2026-02-18 23:48:32 +00:00
Peter Steinberger
e368e74a92 test: dedupe validate-turns identity cases 2026-02-18 23:38:22 +00:00
Peter Steinberger
002f158da6 test: merge empty-id sanitize mode checks 2026-02-18 23:37:03 +00:00
Peter Steinberger
595246b58b test: merge context-window overflow variants 2026-02-18 23:35:51 +00:00
Peter Steinberger
cea586ba5a test: merge skills-cli json output cases 2026-02-18 23:34:47 +00:00
Peter Steinberger
5d9517767f refactor(config): share media provider request fields 2026-02-18 23:34:15 +00:00
Peter Steinberger
3f621d13ff refactor(cli): dedupe browser debug and download opts 2026-02-18 23:34:15 +00:00
Peter Steinberger
0048af4e2d refactor(commands): dedupe auth-choice model notes 2026-02-18 23:34:15 +00:00
Peter Steinberger
4e62bdf78d refactor(signal): reuse shared reaction types 2026-02-18 23:34:15 +00:00
Peter Steinberger
136bd59ba5 refactor(shared): centralize @/# slug normalization 2026-02-18 23:34:15 +00:00
Peter Steinberger
b366279030 refactor(shared): reuse node list parsers across cli and tools 2026-02-18 23:34:15 +00:00
Peter Steinberger
3b7c8fe79a refactor(cli): extract shared node media helpers 2026-02-18 23:34:15 +00:00
Peter Steinberger
65ef7fb4a4 test: dedupe empty-input mmr assertions 2026-02-18 23:33:15 +00:00
Peter Steinberger
317441d09a test: reuse chat-not-found assertion helper 2026-02-18 23:31:56 +00:00
Peter Steinberger
281e9110cc test: table-drive format-time timestamp assertions 2026-02-18 23:30:31 +00:00
Peter Steinberger
20849df702 test: merge media invalid-path scenarios 2026-02-18 23:28:53 +00:00
Peter Steinberger
6f3a6013e3 test: table-drive poll duration clamp cases 2026-02-18 23:27:50 +00:00
Peter Steinberger
5e7e63250a test: merge base64 oversize guard variants 2026-02-18 23:26:41 +00:00
Peter Steinberger
d743332d83 test: table-drive mime mapping assertions 2026-02-18 23:25:30 +00:00
Peter Steinberger
de826a62f9 test: merge telegram reaction scenarios 2026-02-18 23:23:38 +00:00
Peter Steinberger
03241498f9 test: table-drive telegram thread param cases 2026-02-18 23:22:26 +00:00
Peter Steinberger
c25a18493e test: merge direct announce origin variants 2026-02-18 23:21:03 +00:00
Peter Steinberger
1a030a544b test: table-drive sandbox formatter assertions 2026-02-18 23:19:33 +00:00
Peter Steinberger
c8e02329cd test: dedupe subagent announce fallback and thread assertions 2026-02-18 23:15:11 +00:00
Peter Steinberger
d54a4a08b2 refactor(auto-reply): dedupe allowlist path and name helpers 2026-02-18 23:09:09 +00:00
Peter Steinberger
f33ecae0bb refactor(config): dedupe native command setting resolver 2026-02-18 23:09:09 +00:00
Peter Steinberger
8b257703d8 refactor(auto-reply): reuse abort session-entry resolver 2026-02-18 23:09:09 +00:00
Peter Steinberger
c0c10f42e2 refactor(commands): share daemon runtime warning helper 2026-02-18 23:09:09 +00:00
Peter Steinberger
3ce615ff06 refactor(cli): share runtime status color rendering 2026-02-18 23:09:09 +00:00
Peter Steinberger
9a100d520d refactor(gateway): dedupe exec approvals node validation 2026-02-18 23:09:09 +00:00
Peter Steinberger
8e6a7a6343 refactor(models): reuse list format helpers in scan 2026-02-18 23:09:09 +00:00
Peter Steinberger
6eb0964fa6 refactor(auto-reply): share standard set/unset slash parsing 2026-02-18 23:09:09 +00:00
Peter Steinberger
6cbd00a3c6 test: simplify invalid-input fallback assertions in format-time 2026-02-18 22:51:01 +00:00
Peter Steinberger
bdb13d6c4c refactor(cron-cli): share enable-disable command wiring 2026-02-18 22:49:39 +00:00
Peter Steinberger
8369913c7a refactor(models): reuse validated config snapshot loader 2026-02-18 22:49:39 +00:00
Peter Steinberger
61c0c147ad refactor(update-cli): share timeout option validation 2026-02-18 22:49:39 +00:00
Peter Steinberger
b704bad8f3 test: merge telegram thread id normalization assertions 2026-02-18 22:47:28 +00:00
Peter Steinberger
c0e0d4c63d test: dedupe empty-array counter checks in sandbox formatters 2026-02-18 22:46:10 +00:00
Peter Steinberger
e9a37d7af2 test: merge telegram probe success retry variants 2026-02-18 22:44:37 +00:00
Peter Steinberger
3128bd2854 test: dedupe non-matching unhandled rejection cases 2026-02-18 22:42:39 +00:00
Peter Steinberger
3b481001d1 test: merge duplicate line carousel column-limit cases 2026-02-18 22:41:25 +00:00
Peter Steinberger
2157385ff6 refactor(auto-reply): share unique model catalog insertion 2026-02-18 22:40:26 +00:00
Peter Steinberger
c7458782b8 refactor(cli): dedupe service-load and command-removal loops 2026-02-18 22:40:26 +00:00
Peter Steinberger
5e76cefc70 refactor(gateway): share session store lookup map builder 2026-02-18 22:40:26 +00:00
Peter Steinberger
b4cba304e2 refactor(outbound): reuse required channel/plugin resolution 2026-02-18 22:40:26 +00:00
Peter Steinberger
a117e9fed6 refactor(outbound): share plugin send/poll dispatch path 2026-02-18 22:40:25 +00:00
Peter Steinberger
fc5bcebd0a perf(test): reduce channel health monitor check slack 2026-02-18 22:39:57 +00:00
Peter Steinberger
7e243d80fe test: dedupe line rich menu label truncation checks 2026-02-18 22:38:49 +00:00
Peter Steinberger
8a6b55e715 perf(test): tighten channel health monitor timer windows 2026-02-18 22:36:44 +00:00
Peter Steinberger
65002b2b4b perf(test): tighten subagent announce retry give-up wait 2026-02-18 22:33:38 +00:00
Peter Steinberger
bc38d9b844 refactor(tui): share select list theme styles 2026-02-18 22:31:45 +00:00
Peter Steinberger
f054cd6709 refactor(gateway): dedupe cron protocol param schemas 2026-02-18 22:31:45 +00:00
Peter Steinberger
bb0516655c perf(test): align node wake test waits with reconnect timeout 2026-02-18 22:31:19 +00:00
Peter Steinberger
7ebd213acf perf(test): dedupe telegram thread cases and tighten PTY timer 2026-02-18 22:29:31 +00:00
Peter Steinberger
6dd868f07e perf(test): trim bonjour watchdog post-stop timer advance 2026-02-18 22:26:27 +00:00
Peter Steinberger
9092d783a4 perf(test): tighten discord stall reaction test timing 2026-02-18 22:25:19 +00:00
Peter Steinberger
be1f2a1348 perf(test): drop timeout wrapper in async memory search test 2026-02-18 22:22:36 +00:00
Peter Steinberger
dfdeeaf4b9 perf(test): speed up telegram media retry tests 2026-02-18 22:21:05 +00:00
Peter Steinberger
ac4ae9ed61 refactor(browser): dedupe storage and download route parsing 2026-02-18 22:18:48 +00:00
Peter Steinberger
bb00eb2031 refactor(browser): reuse shared tab context in snapshot routes 2026-02-18 22:18:48 +00:00
Peter Steinberger
42f34af776 refactor(browser): share basic and tabs route helpers 2026-02-18 22:18:48 +00:00
Peter Steinberger
ba49b970df perf(test): reduce discord stall timer advance window 2026-02-18 22:16:23 +00:00
Peter Steinberger
cb488df572 perf(test): tighten fake timer windows in channel restart tests 2026-02-18 22:11:56 +00:00
Peter Steinberger
8b4d449dbc perf(test): use setImmediate for node invoke bypass yields 2026-02-18 22:09:48 +00:00
Peter Steinberger
671560616a perf(test): use expect.poll in browserless live test 2026-02-18 22:06:44 +00:00
Peter Steinberger
8b09694882 perf(test): simplify shutdown rejection tick wait 2026-02-18 22:05:40 +00:00
Peter Steinberger
06d2752a0f refactor(browser): dedupe tab route profile and error handling 2026-02-18 22:05:11 +00:00
Peter Steinberger
66c1b8b4f1 perf(test): batch channel health monitor timer advances 2026-02-18 22:01:46 +00:00
Peter Steinberger
b30e3467ee refactor(browser): reuse shared route context in agent act routes 2026-02-18 22:01:28 +00:00
Peter Steinberger
b76e19ceb7 test(browser): cover shared and storage route parsing helpers 2026-02-18 21:58:08 +00:00
Peter Steinberger
5d98c2ae7e refactor(browser): share playwright route context for debug/storage routes 2026-02-18 21:58:08 +00:00
Peter Steinberger
c4eaf7d0c2 perf(test): batch retry timer advances in telegram probe tests 2026-02-18 21:57:47 +00:00
Peter Steinberger
d071f49676 perf(test): batch fake-timer advance in discord process test 2026-02-18 21:55:33 +00:00
Peter Steinberger
a011361784 perf(test): remove timer callbacks in command queue tests 2026-02-18 21:53:57 +00:00
Peter Steinberger
f3b7b51132 perf(test): remove fixed waits in node invoke bypass e2e 2026-02-18 21:52:55 +00:00
Peter Steinberger
48b0b55fa4 test: make shell-env cache assertions windows-safe 2026-02-18 21:51:08 +00:00
Xinhe Hu
b62bd290cb fix: remove hardcoded disableBlockStreaming to honor agent config for TUI (#19693)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 710d449080
Co-authored-by: neipor <191749196+neipor@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-18 16:25:59 -05:00
Nimrod Gutman
dd28a77df0 fix(ios): refactor screen webview lifecycle handling (#20366)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7beb794a06
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-02-19 05:05:40 +08:00
Mariano
e67da1538c iOS/Gateway: wake disconnected iOS nodes via APNs before invoke (#20332)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7751f9c531
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 21:00:17 +00:00
Mariano
750276fa36 fix(protocol): regenerate Swift models for push.test (#20325)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 9281e7ad03
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 20:04:03 +00:00
Mariano
264131eb9f Canvas: improve A2UI asset resolution and empty state (#20312)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: adce485695
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 19:44:55 +00:00
Mariano
fe3f0759b5 Chat UI: accept canonical main session key alias (#20311)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: a4ed5235bc
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 19:42:18 +00:00
Mariano
6e7f1a6a1b iOS onboarding: prevent pairing flicker during auto-resume (#20310)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 691808b747
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 19:39:41 +00:00
Mariano
c2d12b7e31 iOS: add APNs registration and notification signing config (#20308)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 614180020e
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 19:37:03 +00:00
Mariano
99d099aa84 Gateway: add APNs push test pipeline (#20307)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 6a1c442207
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 19:32:42 +00:00
Peter Steinberger
1f5cd65d60 refactor(channels): share case-insensitive account lookup in dock 2026-02-18 19:04:57 +00:00
Peter Steinberger
d7a6a0a0b9 refactor(reply): share embedded run fallback/context builders 2026-02-18 19:02:25 +00:00
Peter Steinberger
32a704f630 refactor(auth): share resolve profile params type 2026-02-18 19:02:19 +00:00
Peter Steinberger
f830261c40 test(daemon): dedupe schtasks fixtures and cover state-dir override 2026-02-18 18:54:51 +00:00
Peter Steinberger
9a77268242 refactor(media): share provider auth resolution for entry runs 2026-02-18 18:54:46 +00:00
Peter Steinberger
79cc4aec80 refactor(auth): share oauth result builders and token expiry checks 2026-02-18 18:54:40 +00:00
Peter Steinberger
8d4ffe350e refactor(agents): share discord role mutation parsing 2026-02-18 18:54:34 +00:00
Peter Steinberger
9362e0f9a9 refactor(browser): share download request helper 2026-02-18 18:54:27 +00:00
Peter Steinberger
2863661bcc refactor(gateway): share openai response text extraction 2026-02-18 18:54:22 +00:00
Peter Steinberger
e1419f3a02 refactor(agents): reuse embedded block flush helper 2026-02-18 18:54:15 +00:00
Peter Steinberger
fa5902f210 refactor(browser): share storage mutation route parsing 2026-02-18 18:42:26 +00:00
Peter Steinberger
7b9db18d5e refactor(cli): share directory list command flow 2026-02-18 18:38:58 +00:00
Peter Steinberger
a848e9a1cd fix(types): narrow snapshot refs mode type 2026-02-18 18:38:51 +00:00
Peter Steinberger
4c096020a2 refactor(commands): share configure wizard channel/daemon steps 2026-02-18 18:37:17 +00:00
Peter Steinberger
079bf25fee refactor(gateway): share transcript path/fd helpers 2026-02-18 18:35:04 +00:00
Peter Steinberger
37143cf70c refactor(slack): share markdown render options 2026-02-18 18:33:48 +00:00
Peter Steinberger
86f504e256 refactor(browser): share checked fetch helper for cdp 2026-02-18 18:33:40 +00:00
Peter Steinberger
f50c38ec1a refactor(browser): reuse role snapshot args in route 2026-02-18 18:33:35 +00:00
Peter Steinberger
2789eb7512 refactor(line): share rich menu user batching 2026-02-18 18:30:23 +00:00
Peter Steinberger
4f36c813a7 refactor(commands): share custom api verification request flow 2026-02-18 18:30:13 +00:00
Peter Steinberger
307719abe9 fix(types): align restart sentinel and typing test mocks 2026-02-18 18:25:25 +00:00
Peter Steinberger
0def1ac1d2 refactor(commands): share session entry persistence 2026-02-18 18:25:25 +00:00
Peter Steinberger
e103323014 refactor(browser): share playwright download wait/save flow 2026-02-18 18:25:25 +00:00
Peter Steinberger
7bf9b6e52f refactor(line): share account config base type 2026-02-18 18:25:25 +00:00
Peter Steinberger
9fd810e3a6 refactor(daemon): share systemd service action flow 2026-02-18 18:25:25 +00:00
Peter Steinberger
63403d47d9 refactor(auth): share oauth profile config checks 2026-02-18 18:25:25 +00:00
Peter Steinberger
06b2df9fc7 refactor(reply): share verbose gate helpers 2026-02-18 18:25:25 +00:00
Peter Steinberger
efd6ed9a56 refactor(subagents): dedupe list line rendering 2026-02-18 18:25:25 +00:00
Peter Steinberger
bec94449eb refactor(subagents): share run target resolution 2026-02-18 18:25:25 +00:00
Peter Steinberger
4e7182c4af refactor(media): share image resize side grid and quality steps 2026-02-18 18:25:25 +00:00
Peter Steinberger
85ebdf88b0 refactor(agents): share text block extraction helper 2026-02-18 18:25:25 +00:00
Peter Steinberger
2d55cc446a refactor(config): share install record schema shape 2026-02-18 18:25:25 +00:00
Peter Steinberger
0dc004fd21 refactor(sessions): share session thread/topic parsing 2026-02-18 18:25:25 +00:00
Peter Steinberger
1aa4d3a6f0 refactor(queue): share runtime settings and summary helpers 2026-02-18 18:25:25 +00:00
Peter Steinberger
84841aebe5 perf(test): replace telegram media flush sleeps 2026-02-18 18:10:32 +00:00
Peter Steinberger
e47df9ed76 perf(test): tighten background-abort e2e wait 2026-02-18 18:08:28 +00:00
Peter Steinberger
b7c75f3918 perf(test): speed up subagent persistence e2e flushes 2026-02-18 18:06:56 +00:00
Peter Steinberger
fae5ba637c perf(test): replace bash-tools polling loops 2026-02-18 18:03:18 +00:00
Peter Steinberger
e583e716f2 perf(test): use expect.poll for background abort completion 2026-02-18 18:00:07 +00:00
Peter Steinberger
6f273d5e2a perf(test): replace send-keys session polling loop 2026-02-18 17:57:48 +00:00
Peter Steinberger
cd8eb079e3 perf(test): replace subagent lifecycle polling helper 2026-02-18 17:53:33 +00:00
Peter Steinberger
c68d1073b5 perf(test): replace claude runner call polling loop 2026-02-18 17:51:38 +00:00
Peter Steinberger
a82ceb81d2 perf(test): replace sessions e2e yield loops with waitFor 2026-02-18 17:48:51 +00:00
Peter Steinberger
95aa5480a0 fix(telegram): correct onboarding import for chat lookup helper 2026-02-18 17:48:02 +00:00
Peter Steinberger
d67942af1e refactor(telegram): share getChat id lookup helper 2026-02-18 17:48:02 +00:00
Peter Steinberger
6187e2afbd refactor(gateway): share gmail watcher startup flow 2026-02-18 17:48:02 +00:00
Peter Steinberger
e702a9eb52 refactor(channels): share account action gate resolution 2026-02-18 17:48:02 +00:00
Peter Steinberger
b73a2de9f6 refactor(infra): reuse shared home prefix expansion 2026-02-18 17:48:02 +00:00
Peter Steinberger
b51166e879 refactor(browser): share control lifecycle helpers 2026-02-18 17:48:02 +00:00
Peter Steinberger
005e1d5fd1 refactor(cli): share styled select prompt helper 2026-02-18 17:48:02 +00:00
Peter Steinberger
8b48e0c615 refactor(shared): reuse requirement remote context type 2026-02-18 17:48:02 +00:00
Peter Steinberger
7b2697bd4d refactor(auto-reply): reuse native command spec mapping 2026-02-18 17:48:01 +00:00
Peter Steinberger
f46bcbe16d refactor(auto-reply): share slash set/unset command parsing 2026-02-18 17:48:01 +00:00
Mariano
fedebc245e fix(protocol): align bool-first AnyCodable equality/hash dispatch (#20233)
* fix(protocol): preserve booleans in AnyCodable bridge

* fix(protocol): align AnyCodable bool-first type dispatch
2026-02-18 17:47:13 +00:00
Peter Steinberger
8f079afb38 perf(test): remove timer usage in command queue ordering test 2026-02-18 17:46:39 +00:00
Peter Steinberger
6d15d01446 perf(test): replace relay list polling loop with expect.poll 2026-02-18 17:44:44 +00:00
Peter Steinberger
5d81c3ead6 perf(test): remove timer sleeps from concurrency test 2026-02-18 17:43:06 +00:00
Mariano
e9b4d86e37 fix(protocol): preserve AnyCodable booleans from JSON bridge (#20220)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 1d86183e3b
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 17:39:54 +00:00
Peter Steinberger
05173ec53a perf(test): use fs.rm retry options in cron teardown 2026-02-18 17:37:26 +00:00
Peter Steinberger
aa3dfe8216 perf(test): replace role-update signal polling with waitFor 2026-02-18 17:35:13 +00:00
Peter Steinberger
d16621f608 fix(test): annotate mock web listener return type 2026-02-18 17:33:25 +00:00
Peter Steinberger
9c125c6c1f perf(test): remove unnecessary qmd export delay 2026-02-18 17:31:59 +00:00
Peter Steinberger
f9e67f3f4c perf(test): replace gateway chat polling loops with waitFor 2026-02-18 17:28:25 +00:00
Peter Steinberger
e8e47ff00e perf(test): replace manual log polling with vi.waitFor 2026-02-18 17:26:05 +00:00
Peter Steinberger
8ab90858ba refactor(auto-reply): share command action arg formatting 2026-02-18 17:23:44 +00:00
Peter Steinberger
0a78331536 refactor(infra): share shell env timeout normalization 2026-02-18 17:23:44 +00:00
Peter Steinberger
5ae4595bb9 refactor(plugins): reuse plugin service runtime context 2026-02-18 17:23:44 +00:00
Peter Steinberger
64a10e64e4 perf(test): replace reconnect polling sleeps with waitFor 2026-02-18 17:22:18 +00:00
Peter Steinberger
0d25b6a317 perf(test): remove fixed sleeps in async test flows 2026-02-18 17:20:35 +00:00
Peter Steinberger
00e32cf04a test(auto-reply): type set/unset action helper expectations 2026-02-18 17:16:36 +00:00
Peter Steinberger
28d49b8d44 refactor(auth-profiles): reuse cooldown timestamp resolver 2026-02-18 17:13:47 +00:00
Peter Steinberger
818419b4c4 refactor(auto-reply): share set/unset command action parsing 2026-02-18 17:13:40 +00:00
Peter Steinberger
288015a9fc refactor(auth): share api key masking utility 2026-02-18 17:13:35 +00:00
Peter Steinberger
3138dbaf75 test(auto-reply): share elevated-off status assertion 2026-02-18 17:01:22 +00:00
Peter Steinberger
50e5413c19 refactor(cron-test): share running-state fixture 2026-02-18 17:01:22 +00:00
Peter Steinberger
c7831fdf1e refactor(gateway-test): share preview transcript fixture 2026-02-18 17:01:22 +00:00
Peter Steinberger
e9f6a2ce52 refactor(web-test): share mock listener harness 2026-02-18 17:01:22 +00:00
Peter Steinberger
f05395ae00 refactor(test): share internal hook and npm pack assertions 2026-02-18 17:01:22 +00:00
Peter Steinberger
72a4d83334 perf(test): use microtask wait in fetch rejection test 2026-02-18 16:50:05 +00:00
Peter Steinberger
c0a6ff08a7 test(auto-reply): reuse shared directive and home test harnesses 2026-02-18 16:48:35 +00:00
Peter Steinberger
82cb185881 refactor(core): unify bounded concurrency runner 2026-02-18 16:48:35 +00:00
Peter Steinberger
2b8f1bade0 refactor(archive): share archive path safety helpers 2026-02-18 16:48:35 +00:00
Peter Steinberger
36996194cd perf(test): remove timer waits in hooks and discord monitor tests 2026-02-18 16:45:48 +00:00
Peter Steinberger
4605dfd2ae test(channels): add slack group-mention and onboarding helper coverage 2026-02-18 16:35:25 +00:00
Peter Steinberger
f3b75730de refactor(channels): share slack matching and allowlist prompt flow 2026-02-18 16:35:25 +00:00
Peter Steinberger
c0cd53e104 perf(test): trim sandbox registry cleanup churn 2026-02-18 16:28:00 +00:00
Peter Steinberger
a661eec0bf test(channels): cover query+limit filtering in directory config 2026-02-18 16:26:52 +00:00
Peter Steinberger
68be4611dd refactor(channels): dedupe directory query/limit pipelines 2026-02-18 16:26:52 +00:00
Peter Steinberger
d77dcebcb1 perf(test): replace timeout ticks with microtask waits 2026-02-18 16:23:55 +00:00
Peter Steinberger
983a68c23e test(matrix): cover directory context and group exact-match resolution 2026-02-18 16:22:20 +00:00
Peter Steinberger
eb4f1e765c refactor(matrix): dedupe directory/target match helpers 2026-02-18 16:22:20 +00:00
Peter Steinberger
e5f13db13d perf(test): remove polling loop from announce queue tests 2026-02-18 16:22:00 +00:00
Peter Steinberger
98fac87a9e test(matrix): add coverage for deduped action helpers 2026-02-18 16:18:01 +00:00
Peter Steinberger
f5c3702191 refactor(matrix): dedupe action limit and pin/reaction helpers 2026-02-18 16:18:01 +00:00
Peter Steinberger
7648f6bb00 perf(test): fake abort timer and dedupe slack thread cases 2026-02-18 16:14:07 +00:00
Peter Steinberger
29d3bb278f refactor(device-pair): reduce duplicated gateway parsing 2026-02-18 16:08:38 +00:00
Peter Steinberger
95d52b06d5 refactor(mattermost): dedupe reaction flow and test fixtures 2026-02-18 16:08:38 +00:00
Peter Steinberger
c7bc94436b perf(test): fake queue timers and merge telegram reply-mode checks 2026-02-18 16:01:20 +00:00
Peter Steinberger
797a47c3ce docs: harden coding-agent skill guidance example 2026-02-18 16:55:50 +01:00
Pejman Pour-Moezzi
a0d904dc23 docs(discord): replace quick setup and add recommended guild setup (#20088)
Co-authored-by: Shadow <shadow@openclaw.ai>
2026-02-18 09:39:09 -06:00
Peter Steinberger
6a19654c4a refactor(core): dedupe browser route signatures and cli watchdog schema 2026-02-18 14:15:20 +00:00
Peter Steinberger
1934eebbf0 refactor(agents): dedupe lifecycle send assertions and stable payload stringify 2026-02-18 14:15:14 +00:00
Peter Steinberger
168d24526e chore(protocol): regenerate Swift models for device pair remove params 2026-02-18 14:01:34 +00:00
Peter Steinberger
42025915db test(agents): dedupe sessions_spawn model preference assertions 2026-02-18 14:01:29 +00:00
Peter Steinberger
33b0b38f65 test(agents): dedupe shared bootstrap and tool-id test setup 2026-02-18 14:01:24 +00:00
Peter Steinberger
33f30367e1 fix(cli): include model and thinking fields in cron edit patch type 2026-02-18 13:39:40 +00:00
Peter Steinberger
41e68c31db test(channels): dedupe slack arg-menu and discord reply chunk assertions 2026-02-18 13:39:40 +00:00
Peter Steinberger
c7bfa818ea test(cli): dedupe cron add/edit assertion harness 2026-02-18 13:39:40 +00:00
Mariano
57083e4220 iOS: add Apple Watch companion message MVP (#20054)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 720791ae6b
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 13:37:41 +00:00
Peter Steinberger
e71e9a55ab fix(cli): align runtime capture helper with RuntimeEnv signature 2026-02-18 13:34:03 +00:00
Peter Steinberger
277d524fa3 test(agents): restore stable cron tool gateway mocks 2026-02-18 13:34:03 +00:00
Peter Steinberger
a18f411fb6 test(agents): dedupe cron tool mock wiring 2026-02-18 13:34:03 +00:00
Peter Steinberger
8f866d51c4 test(cli): dedupe runtime capture fixtures across command specs 2026-02-18 13:34:03 +00:00
Peter Steinberger
3af9f704c8 test(cli): dedupe repeated gateway node and slack pairing setup 2026-02-18 13:34:03 +00:00
Peter Steinberger
2d0ce40ed6 test(agents): dedupe tool-result overflow and telegram account helpers 2026-02-18 13:34:03 +00:00
Mariano
1437ed76a0 Gateway/CLI: add paired-device remove and clear flows (#20057)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 26523f8a38
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 13:27:31 +00:00
Mariano
fc65f70a9b iOS: stabilize pairing/reconnect loops (#20056)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: b01a482a17
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 13:23:06 +00:00
Peter Steinberger
ff50d3303d test(memory): dedupe model-auth mock setup 2026-02-18 13:17:44 +00:00
Peter Steinberger
28b8101eef fix(browser): handle IPv6 loopback auth and dedupe fetch auth tests 2026-02-18 13:15:00 +00:00
Peter Steinberger
eb775ff24b test(media): dedupe audio provider request assertions 2026-02-18 13:13:43 +00:00
Peter Steinberger
e1b491d961 test(channels): dedupe inbound contract dispatch capture setup 2026-02-18 13:13:43 +00:00
Mariano
39881a318a Browser: reuse extension relay when relay port is already occupied (#20035)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: b310666d39
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-18 13:13:04 +00:00
Peter Steinberger
f4db58a5fd test(media): dedupe auto-audio fixture wiring 2026-02-18 13:06:21 +00:00
Peter Steinberger
d067618600 test(line): dedupe reply chunk fixture setup 2026-02-18 13:06:08 +00:00
Peter Steinberger
53ad08f319 test(slack): type draft stream harness callbacks 2026-02-18 13:02:59 +00:00
Peter Steinberger
7b46f2c17f test(imessage): dedupe send test scaffolding 2026-02-18 13:01:37 +00:00
Peter Steinberger
7f7fc523cf test(cli): dedupe runMessageAction helper specs 2026-02-18 12:59:36 +00:00
Peter Steinberger
c6d6411378 test(media): dedupe redirect request fixtures 2026-02-18 12:58:35 +00:00
Peter Steinberger
7bca5f5400 test(slack): dedupe block and draft stream test fixtures 2026-02-18 12:57:51 +00:00
Peter Steinberger
3daf730fcc test(gateway): fix send target resolution error typing 2026-02-18 12:54:22 +00:00
Peter Steinberger
56ebbf0eed test(gateway): dedupe sessions usage handler fixtures 2026-02-18 12:52:34 +00:00
Peter Steinberger
fc29588329 test(gateway): dedupe send delivery fixtures 2026-02-18 12:52:25 +00:00
Peter Steinberger
3a09d85cd3 test(gateway): fix typed respond helpers in agent tests 2026-02-18 12:49:15 +00:00
Peter Steinberger
00c2308085 test(gateway): dedupe health status scope test setup 2026-02-18 12:48:10 +00:00
Peter Steinberger
c6da37dfb5 test(gateway): dedupe agent handler request fixtures 2026-02-18 12:48:04 +00:00
Peter Steinberger
396ccf9fb1 test(gateway): dedupe agents.files.list assertions 2026-02-18 12:45:14 +00:00
Peter Steinberger
2aec380fb3 test(gateway): dedupe update and chat abort persistence fixtures 2026-02-18 12:43:54 +00:00
Peter Steinberger
bb84452c62 fix(signal): restore mention-gating helper map typing 2026-02-18 12:43:46 +00:00
Peter Steinberger
37b5c92928 test(signal): dedupe mention-gating handler setup 2026-02-18 12:38:44 +00:00
Peter Steinberger
9b68af5f4f test(signal): dedupe receive event fixtures and add mention clamp case 2026-02-18 12:37:38 +00:00
Peter Steinberger
9c2b82362e test(signal): dedupe monitor tool-result test payload fixtures 2026-02-18 12:28:35 +00:00
Peter Steinberger
1e2b367e1e test(hooks): dedupe session-memory handler test setup 2026-02-18 12:28:30 +00:00
Peter Steinberger
c3472f6c54 test(memory): dedupe embeddings provider test fixtures 2026-02-18 12:28:25 +00:00
Peter Steinberger
87ca2a24bd test(gateway): dedupe call gateway test setup 2026-02-18 12:27:21 +00:00
Peter Steinberger
514e318df9 test(config): dedupe io write config test setup 2026-02-18 12:20:56 +00:00
Peter Steinberger
eabf187fa5 test(cron): dedupe migration and regression fixtures 2026-02-18 12:20:48 +00:00
Peter Steinberger
2fd211b705 test(auto-reply): dedupe directive behavior e2e fixtures 2026-02-18 12:20:40 +00:00
Peter Steinberger
3c886ee98b test(infra): dedupe update-runner fixture setup 2026-02-18 12:04:32 +00:00
Peter Steinberger
4750be9d5f test(cli): extract update-cli package-install test helpers 2026-02-18 12:04:32 +00:00
Peter Steinberger
3356aae704 test(cron): dedupe delivery target tests and add coverage 2026-02-18 12:04:32 +00:00
Peter Steinberger
36a34e5959 test(cron): dedupe isolated-agent session test setup 2026-02-18 12:04:32 +00:00
Nimrod Gutman
cb34e80f98 fix(ios): restore auto-selected team for local signing (#19993)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 6f375238f0
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-02-18 19:38:23 +08:00
Taras Lukavyi
d833dcd731 fix(telegram): cron and heartbeat messages land in wrong chat instead of target topic (#19367)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: bf02bbf9ce
Co-authored-by: Lukavyi <1013690+Lukavyi@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-18 15:31:01 +05:30
the sun gif man
114736ed1a Doctor/Security: fix telegram numeric ID + symlink config permission warnings (#19844)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: e42bf1e48d
Co-authored-by: joshp123 <1497361+joshp123@users.noreply.github.com>
Co-authored-by: joshp123 <1497361+joshp123@users.noreply.github.com>
Reviewed-by: @joshp123
2026-02-18 00:09:51 -08:00
Gustavo Madeira Santana
7ea7b7e7af Infra: unify git root discovery 2026-02-18 00:45:43 -05:00
Peter Steinberger
639d0221ff test: dedupe line and whatsapp target resolution tests 2026-02-18 05:31:13 +00:00
Peter Steinberger
a9cce800df test: dedupe slack missing-thread tests and cover history failures 2026-02-18 05:31:06 +00:00
Peter Steinberger
12ad708ce5 test: dedupe gateway auth and sessions patch coverage 2026-02-18 05:30:59 +00:00
Peter Steinberger
e3292b9af1 test: dedupe sessions command tests and cover active filtering 2026-02-18 05:30:51 +00:00
Peter Steinberger
23f2150190 test: dedupe auth fallback tests and add auth util unit coverage 2026-02-18 05:05:04 +00:00
Peter Steinberger
112f8250fc test: dedupe registry/session tests and add install source coverage 2026-02-18 05:05:04 +00:00
Gustavo Madeira Santana
07fdceb5fd refactor: centralize presence routing and version precedence coverage (#19609)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 10d9df5263
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-18 00:02:51 -05:00
Robby
5c69e625f5 fix(cli): display correct model for sub-agents in sessions list (#18660)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: ba54c5a351
Co-authored-by: robbyczgw-cla <239660374+robbyczgw-cla@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-17 23:59:20 -05:00
Peter Steinberger
a69e7682c1 refactor(test): dedupe channel and monitor action suites 2026-02-18 04:49:22 +00:00
Peter Steinberger
31f83c86b2 refactor(test): dedupe agent harnesses and routing fixtures 2026-02-18 04:49:22 +00:00
Peter Steinberger
8a9fddedc9 refactor: extract shared install and embedding utilities 2026-02-18 04:49:22 +00:00
Gustavo Madeira Santana
4d3403b7ac chore: fix CI errors 2026-02-17 23:46:40 -05:00
Peter Steinberger
308e09c876 perf(test): shorten process timeout fixtures 2026-02-18 04:27:01 +00:00
Peter Steinberger
46278e22cf perf(test): trim telegram duplicates and queue wait delays 2026-02-18 04:22:59 +00:00
Peter Steinberger
fa4772b4ce perf(test): dedupe telegram allowlist and speed twitch probe 2026-02-18 04:16:36 +00:00
Peter Steinberger
fdc6768227 perf(test): stabilize and speed sandbox registry races 2026-02-18 04:10:27 +00:00
Peter Steinberger
5f12334761 refactor: dedupe image, web, and auth profile test fixtures 2026-02-18 04:04:14 +00:00
Peter Steinberger
05b7bd2c22 refactor: dedupe command dispatch and process poll tests 2026-02-18 04:04:14 +00:00
Peter Steinberger
adac9cb67f refactor: dedupe gateway and scheduler test scaffolding 2026-02-18 04:04:14 +00:00
Peter Steinberger
262472ba20 test: remove duplicated scenario scaffolding across runtime tests 2026-02-18 04:04:14 +00:00
Peter Steinberger
e57628165a test: dedupe shared setup in channel and doctor config tests 2026-02-18 04:04:14 +00:00
Peter Steinberger
d1ab852972 test: extract shared e2e helpers for trigger handling and skills 2026-02-18 04:04:14 +00:00
Peter Steinberger
b099171db5 perf(test): dedupe slow discord monitor cases 2026-02-18 04:04:04 +00:00
Peter Steinberger
ac0db68235 refactor(security): extract safeBins trust resolver 2026-02-18 05:01:31 +01:00
Peter Steinberger
e8154c12e6 refactor(net): table-drive embedded IPv6 decoding and SSRF tests 2026-02-18 04:57:08 +01:00
Peter Steinberger
35016a380c fix(sandbox): serialize registry mutations and lock usage 2026-02-18 04:55:40 +01:00
Peter Steinberger
28bac46c92 fix(security): harden safeBins path trust 2026-02-18 04:55:31 +01:00
Peter Steinberger
42d2a61888 chore(changelog): move SSRF transition fix to 2026.2.18 2026-02-18 04:53:50 +01:00
Peter Steinberger
442fdbf3d8 fix(security): block SSRF IPv6 transition bypasses 2026-02-18 04:53:09 +01:00
Peter Steinberger
50e5553533 fix: align retry backoff semantics and test mock signatures 2026-02-18 04:53:09 +01:00
Gustavo Madeira Santana
0bf1b38cc0 Agents: fix subagent completion thread routing 2026-02-17 22:52:58 -05:00
Peter Steinberger
35851cdaff chore(changelog): move cron SSRF fix into 2026.2.18 2026-02-18 04:52:13 +01:00
Peter Steinberger
516046dba8 fix: avoid doctor token regeneration on invalid repairs 2026-02-18 04:51:25 +01:00
Peter Steinberger
797ea7ed27 perf(test): cut slow monitor/subagent test overhead 2026-02-18 03:50:30 +00:00
Peter Steinberger
99db4d13e5 fix(gateway): guard cron webhook delivery against SSRF 2026-02-18 04:48:08 +01:00
Peter Steinberger
bc00c7d156 refactor: dedupe sandbox registry helpers 2026-02-18 04:46:38 +01:00
Ayaan Zaidi
6a5f887b3d test: harden Telegram command menu sanitization coverage (#19703)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 6a41b11590
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-18 09:16:31 +05:30
Peter Steinberger
cc29be8c9b fix: serialize sandbox registry writes 2026-02-18 04:44:56 +01:00
Peter Steinberger
8278903f0a fix: update deep links handling 2026-02-18 04:40:42 +01:00
Peter Steinberger
4bf3338834 chore: bump version to 2026.2.18 unreleased 2026-02-18 04:40:06 +01:00
Peter Steinberger
f25bbbc37e feat: switch anthropic onboarding defaults to sonnet 2026-02-18 04:37:58 +01:00
Gustavo Madeira Santana
e8816c554f Agents: fix subagent completion delivery to origin channel 2026-02-17 22:36:14 -05:00
Peter Steinberger
ca43efa965 fix(ci): force npm install path in smoke docker tests 2026-02-18 03:25:14 +00:00
Peter Steinberger
91e9684e8c test: add normalization coverage for shared and slack allow-list 2026-02-18 03:17:54 +00:00
Peter Steinberger
8407eeb33c refactor: extract shared string normalization helpers 2026-02-18 03:17:54 +00:00
Peter Steinberger
8984f31876 fix(agents): correct completion announce retry backoff schedule 2026-02-18 03:07:47 +00:00
Peter Steinberger
a420fa0417 fix(test): align subagent announce chat history mock typing 2026-02-18 03:02:20 +00:00
Peter Steinberger
289f215b31 fix(agents): make manual subagent completion announce deterministic 2026-02-18 03:00:27 +00:00
sebslight
d30492823c chore(auto-reply): format subagent command files 2026-02-17 21:55:47 -05:00
Peter Steinberger
34851a78b2 fix: route manual subagent spawn replies via OriginatingTo fallback 2026-02-18 03:48:18 +01:00
Peter Steinberger
4134875c31 fix: route discord native subagent announce to channel target 2026-02-18 02:42:52 +00:00
Peter Steinberger
c1928845ac fix: route native subagent spawns to target session 2026-02-18 02:35:58 +00:00
Gustavo Madeira Santana
40a6661597 test(cli): fix option-collision mock typings 2026-02-17 21:32:04 -05:00
Peter Steinberger
c90b09cb02 feat(agents): support Anthropic 1M context beta header 2026-02-18 03:29:48 +01:00
Peter Steinberger
d1c00dbb7c fix: harden include confinement edge cases (#18652) (thanks @aether-ai-agent) 2026-02-18 03:27:16 +01:00
aether-ai-agent
b5f551d716 fix(security): OC-06 prevent path traversal in config includes
Fixed CWE-22 path traversal vulnerability allowing arbitrary file reads
through the $include directive in OpenClaw configuration files.

Security Impact:
- CVSS 8.6 (High) - Arbitrary file read vulnerability
- Attack vector: Malicious config files with path traversal sequences
- Impact: Exposure of /etc/passwd, SSH keys, cloud credentials, secrets

Implementation:
- Added path boundary validation in resolvePath() (lines 169-198)
- Implemented symlink resolution to prevent bypass attacks
- Restrict includes to config directory only
- Throw ConfigIncludeError for escaping paths

Testing:
- Added 23 comprehensive security tests
- 48/48 includes.test.ts tests passing
- 5,063/5,063 full suite tests passing
- 95.55% coverage on includes.ts
- Zero regressions, zero breaking changes

Attack Vectors Blocked:
✓ Absolute paths (/etc/passwd, /etc/shadow)
✓ Relative traversal (../../etc/passwd)
✓ Symlink bypass attempts
✓ Home directory access (~/.ssh/id_rsa)

Legitimate Use Cases Preserved:
✓ Same directory includes (./config.json)
✓ Subdirectory includes (./clients/config.json)
✓ Deep nesting (./a/b/c/config.json)

Aether AI Agent Security Research
2026-02-18 03:27:16 +01:00
Peter Steinberger
ae3637b23b test: expand subagent announce completion coverage 2026-02-18 03:21:52 +01:00
Peter Steinberger
edf7d6af61 fix: harden subagent completion announce retries 2026-02-18 03:19:50 +01:00
Peter Steinberger
d7c6136c1f test: add sonnet 4.6 and opus 4.6 setup-token model tests 2026-02-18 03:12:32 +01:00
Gustavo Madeira Santana
5a31da8eec chore: format imports in gateway and session tools 2026-02-17 21:10:38 -05:00
Peter Steinberger
81db059627 fix(subagents): always read latest assistant/tool output on subagent completion 2026-02-18 02:59:40 +01:00
Peter Steinberger
0dd97feb41 fix(subagents): include tool role in subagent completion output 2026-02-18 02:57:33 +01:00
Gustavo Madeira Santana
985ec71c55 CLI: resolve parent/subcommand option collisions (#18725)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: b7e51cf909
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-17 20:57:09 -05:00
Peter Steinberger
fa4f66255c fix(subagents): return completion message for manual session spawns 2026-02-18 02:52:35 +01:00
Peter Steinberger
f6f5cda6ca style: format subagent command files 2026-02-18 01:50:11 +00:00
Peter Steinberger
e2dd827ca4 fix: guarantee manual subagent spawn sends completion message 2026-02-18 02:45:05 +01:00
Peter Steinberger
5bd95bef5a fix(protocol): regenerate swift gateway models 2026-02-18 01:37:34 +00:00
Peter Steinberger
b8b43175c5 style: align formatting with oxfmt 0.33 2026-02-18 01:34:35 +00:00
Peter Steinberger
31f9be126c style: run oxfmt and fix gate failures 2026-02-18 01:29:02 +00:00
Peter Steinberger
638853c6d2 fix(security): sanitize sandbox env vars before docker launch 2026-02-18 02:18:05 +01:00
Peter Steinberger
5487c9adeb feat(security): add sandbox env sanitization helpers + tests 2026-02-18 02:18:02 +01:00
Peter Steinberger
71ad357bbe test: remove obsolete mesh test file 2026-02-18 02:18:02 +01:00
Peter Steinberger
972d1b74d0 Revert "Add mesh orchestration gateway methods with DAG execution and retry"
This reverts commit 83990ed542.
2026-02-18 02:18:02 +01:00
Peter Steinberger
01672a8f25 Revert "Add mesh auto-planning with chat command UX and hardened auth/session behavior"
This reverts commit 16e59b26a6.

# Conflicts:
#	src/auto-reply/reply/commands-mesh.ts
#	src/gateway/server-methods/mesh.ts
#	src/gateway/server-methods/server-methods.test.ts
2026-02-18 02:18:02 +01:00
Peter Steinberger
6dcc052bb4 fix: stabilize model catalog and pi discovery auth storage compatibility 2026-02-18 02:09:40 +01:00
Peter Steinberger
653add918b chore: bump workspace dependencies 2026-02-18 01:59:08 +01:00
Peter Steinberger
414b996b0c fix(agents): make image resize logs single-line with size 2026-02-18 01:58:33 +01:00
Peter Steinberger
3459200444 docs: reorder unreleased changelog by user-impact highlights 2026-02-18 01:51:28 +01:00
Nick Lamb
f42e13c17c feat(telegram): add forum topic creation support (#17035)
* Revert "fix(gateway): set explicit chat timeouts for mesh gateway calls"

This reverts commit c529e6005a.

* Revert "fix: capture init script exit codes instead of swallowing via pipe"

This reverts commit 8b14052ebe.

* Revert "feat(docker): add init script support via /openclaw-init.d/"

This reverts commit 53af9f7437.

* Revert "Agents: improve Windows scaffold helpers for venture studio"

This reverts commit b6d934c2c7.

* chore: Fix types in tests 1/N.

* chore: Fix types in tests 2/N.

* Revert "fix: remove stderr suppression so install failures are visible in build logs"

This reverts commit 717caa97fb.

* Revert "fix(docker): ensure memory-lancedb deps installed in Docker image"

This reverts commit 2ab6313d99.

* Revert "fix: add windowsHide: true to spawn in runCommandWithTimeout"

This reverts commit 32c66aff49.

* Revert "Onboarding: fix webchat URL loopback and canonical session"

This reverts commit 59e0e7e4ff.

* Revert "feat(linq): add interactive onboarding adapter"

This reverts commit b91e43714b.

* Revert "feat: add Linq channel — real iMessage via API, no Mac required"

This reverts commit d4a142fd8f.

* docs: clarify discord proxy scope for startup REST calls

* Revert "fix: flatten remaining anyOf/oneOf in Gemini schema cleaning"

This reverts commit 06b961b037.

* Revert "fix: session-memory hook finds previous session file after /new/reset"

This reverts commit d6acd71576.

* Revert "fix: respect OPENCLAW_HOME for isolated gateway instances"

This reverts commit 34b18ea9db.

* fix(process): harden graceful kill-tree cancellation semantics

* fix(slack): scope attachment extraction to forwarded shares

* docs(changelog): note process kill-tree hotfix

* docs(changelog): note slack forwarded attachment hotfix

* fix(session-memory): harden reset transcript recovery

* revert(telegram): undo accidental merge of PR #18601

* fix(ui): preserve locale bootstrap and trusted-proxy overview behavior

* fix(scripts): harden Windows UI spawn behavior

* fix(slack): validate interaction payloads and handle malformed actions

* fix(mattermost): harden react remove flag parsing

* docs(changelog): record PR 18608 fixups

* fix(heartbeat): bound responsePrefix strip for ack detection

* chore: Fix types in tests 3/N.

* chore: chore: Fix types in tests 4/N.

* chore: Fix types in tests 5/N.

* chore: Fix types in tests 6/N.

* chore: Format files.

* chore: Fix types that were broken due to reverts.

* chore: Cleanup unused vars that were leftover from the reverts.

* fix(actions): layer per-account gate fallback

* fix(subagents): pass group context in /subagents spawn

* fix(failover): align abort timeout detection and regressions

* fix(models): sync auth-profiles before availability checks

* fix(ui): correct usage range totals and muted styles

* Revert "feat: show transcript file size in session status"

This reverts commit 15dd2cda20.

* revert(doctor): undo accidental merge of PR #18591

* fix(agents): align session lock hold budget with run timeouts

* Revert "fix: resolve #12770 - update Antigravity default model and trim leading whitespace in BlueBubbles replies"

This reverts commit e179d453c7.

* revert(tools): undo accidental merge of PR #18584

* revert(tools): finish rollback of PR #18584

* chore: Fix Slack test.

* revert: remove accidentally merged video-quote-finder skill (#18550)

* revert: accidental merge of OC-09 sandbox env sanitization change

* fix(doctor): move forced exit to top-level command

* chore: Fix types in tests 7/N.

* chore: Fix types in tests 8/N.

* chore: Fix types in tests 9/N.

* chore: Fix types in tests 10/N.

* chore: Fix types in tests 11/N.

* chore: chore: Fix types in tests 12/N.

* chore: Fix type errors from reverts.

* fix(gateway): remove watch-mode build/start race (#18782)

* fix(doctor): repair googlechat open dm wildcard auto-fix

* test(extensions): cast fetch mocks to satisfy tsgo

* fix(gateway): harden channel health monitor recovery

* fix(reply): track messaging media aliases for dedupe

* refactor(plugins): split before-agent hooks by model and prompt phases

* revert(telegram): undo accidental merge of PR #18564

* fix(agents): restore multi-image image tool schema contract

* chore: Format files.

* fix(ui): gate sessions refresh on successful delete

* revert(docs): undo accidental merge of #18516

* revert(exec): undo accidental merge of PR #18521

* docs(cron): clarify webhook posting summary condition

* fix(gateway): preserve chat.history context under hard caps

* chore: Fix types in tests 13/N.

* chore: Fix types in tests 14/N.

* chore: Fix types in tests 15/N.

* chore: Fix types in tests 16/N.

* chore: Fix types in tests 17/N.

* chore: Fix types in tests 18/N.

* chore: Format files.

* revert(sandbox): revert SHA-1 slug restoration

* test(session): cover stale threadId fallback

* test(status): cover token summary variants

* test(telegram): cover getFile file-too-big errors

* test(voice-call): cover stream disconnect auto-end

* chore(format): fix test import order

* test(agents): cover tool result media placeholders

* chore: chore: Fix types in tests 19/N.

* chore: Fix types in tests 20/N.

* chore: Fix types in tests 21/N.

* chore: Fix types in tests 22/N.

* chore: Fix types in tests 23/N.

* docs(voice-call): document stale call reaper config

* fix(doctor): audit env-only gateway tokens

* fix(sessions): purge deleted transcript archives

* test(docker): cover browser install build arg

* revert(gateway): restore loopback auth setup

* revert(voice-call): undo cached greeting note

* revert(voice-call): undo oxfmt formatting

* revert(voice-call): undo oxfmt formatting pass

* revert(voice-call): remove cached inbound greeting

* test: stabilize infra tests

* fix(subagents): harden announce retry guards

* Revert "fix(whatsapp): allow per-message link preview override\n\nWhatsApp messages default to enabling link previews for URLs. This adds\nsupport for overriding this behavior per-message via the \nparameter (e.g. from  tool options), consistent with Telegram.\n\nFix: Updated internal WhatsApp Web API layers to pass  option\ndown to Baileys ."

This reverts commit 1bef2fc68b.

* fix(telegram): clear offsets on token change

* test(agents): cover exec non-zero exits

* CI: use self-hosted for labeler/automation

* Revert "channels: migrate extension account listing to factory"

This reverts commit d24340d75b.

* chore(format)

* chore: wtf.

* chore: Fix types.

* chore: Fix types in tests 24/N.

* chore: Fix types in tests 25/N.

* chore: Fix types in tests 26/N.

* chore: Fix types in tests 27/N.

* chore: Fix types in tests 28/N.

* chore: Fix types in tests 29/N.

* chore: Fix types in tests 30/N.

* chore: Fix types in tests 31/N.

* chore: Fix types in tests 32/N.

* fix(telegram): add initial message debounce for better push notifications (#18147)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 5e2285b6a0
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus

* style(telegram): format dispatch files

* chore: Fix types in tests 33/N.

* chore: Fix types in tests 34/N.

* chore: Fix types in tests 35/N.

* chore: Fix types in tests 36/N.

* chore: Fix types in tests 37/N.

* chore: Fix types in tests 38/N.

* chore: Fix types in tests 39/N.

* chore: Fix types in tests 40/N.

* chore: Fix types in tests 41/N.

* chore: Fix types in tests 42/N.

* chore: Fix types in tests 43/N.

* chore: Fix types in tests 44/N.

* chore: Fix types in tests 45/N.

* chore: Typecheck tests.

* chore: Fix broken test.

* chore: Fix hanging test.

* fix(telegram): avoid duplicate preview bubbles in partial stream mode (#18956)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: cf4eca71d4
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus

* fix: before_tool_call hook double-fires with abort signal (#16852)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 6269d617f3
Co-authored-by: sreuter <550246+sreuter@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus

* Revert "Default Telegram polls to public"

This reverts commit c43e95e011.

* Revert "Fix Telegram poll action wiring"

This reverts commit 556b531a14.

* Revert "Add Telegram polls action to config typing"

This reverts commit 5cbfaf5cc7.

* Revert "fix(telegram): wire sendPollTelegram into channel action handler (#16977)"

This reverts commit 7bb9a7dcfc.

* CI: remove formal models conformance workflow (#19007)

* fix: preserve telegram dm topic thread ids

* style: drop aidev-note prefix in telegram comments

* test: pass extensionContext in abort dedupe e2e

* fix: align tool execute arg parsing for hooks

* test: type telegram action mock passthrough args

* Configure: make model picker allowlist searchable

* Configure: improve searchable model picker token matching

* Docs: add screenshot showing model picker usability issue

* fix: searchable model picker in configure (#19010) (thanks @bjesuiter)

* fix(extensions): revert openai codex auth plugin (PR #18009)

* feat(telegram): add channel_post support for bot-to-bot communication (#17857)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 27a343cd4d
Co-authored-by: theSamPadilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus

* Revert "fix: handle forum/topics in Telegram DM thread routing (#17980)"

This reverts commit e20b87f1ba.

* Revert: undo #17974 README change

* voice-call: harden closed-loop turn loop and transcript routing (#19140)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 14a3edb005
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky

* iOS onboarding: stop auth step-3 retry loop churn (#19153)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: a38ec42bdd
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky

* Revert: fully roll back #17974 zh-cn UI README

* chore(subagents): add regression coverage and changelog

* fix(daemon): scope token drift warnings

* test(web): fix baileys mock typing

* test(cron): cover webhook session rollover overrides

* docs(changelog): note webhook session reuse fix

* fix(discord): normalize command allowFrom prefixes

* fix(cli): honor update restart overrides

* fix(cron): add spin-loop regression coverage

* test(gateway): cover trusted proxy trimming

* test(discord): cover audioAsVoice replies

* test(feishu): cover post mentions for other users

* fix(discord): preserve DM lastRoute user target

* Revert "fix(browser): track original port mapping for EADDRINUSE fallback"

This reverts commit 8e55503d77.

* Revert "fix(browser): handle EADDRINUSE with automatic port fallback"

This reverts commit 0e6daa2e6e.

* test(discord): fix mock call arg typing

* Revert: fully roll back #17986 templates

* test: add fetch mock helper and reaction coverage

* CLI: approve latest pending device request

* docs(readme): remove Android install link

* revert(agents): remove llms.txt discovery prompt (#19192)

* fix(ui): revert PR #18093 directive tags (#19188)

* test(discord): cover auto-thread skip types

* test(update): cover restart gating

* docs(zai): document tool_stream defaults

* revert: per-model thinkingDefault override (#19195)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: fe2c59e222
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight

* fix(gateway): make stale token cleanup non-fatal

* Agents: add before_message_write persistence regression tests

* fix(mattermost): surface reactions support

* Tests: fix fetch mock typings for type-aware checks

* revert: fix models set catalog validation (#19194)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7e3b2ff7af
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight

* test: cover cron telemetry and typed fetch mocks

* revert(agents): revert base64 image validation (#19221)

* docs(cli): add components send example

* test(sessions): add delivery info regression coverage

* fix(daemon): guard preferred node selection

* test(auto-reply): cover sender_id metadata

* revert: PR 18288 accidental merge (#19224)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 3cda31578c
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight

* test(telegram): cover autoSelectFamily env precedence

* test(cron): add model fallback regression coverage

* test(release): add appcast regression coverage

* docs(changelog): remove revert entries

* docs: add maintainer application section

* docs: refine maintainer application guidance

* docs: add vision doc and link from README

* docs: add community plugins guide

* Update auto-response message for third-party extensions

* update my contributing list

* iOS: use operator session for ChatSheet RPCs (#19320)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 0753b3a1a2
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky

* fix: sanitize native command names for Telegram API (#19257)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: b608be3488
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus

* docs(slack): add assistant:write requirement for typing status

* chore: document sessions_spawn response note and subagent context prefix

* feat(ios): auto-select local signing team (#18421)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: bbb9c3aa48
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman

* fix(bluebubbles): recover outbound message IDs and include sender metadata

* fix cron announce routing and timeout handling

* changelog: add @tyler6204 credit for today's entries

* feat: share to openclaw ios app (#19424)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 0a7ab8589a
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky

* Docs: expand multi-agent routing

* docs(changelog): add missing 2026.2.16 entries and reorder by user impact

* chore(release): bump version to 2026.2.17

* fix(signal): canonicalize message targets in tool and inbound flows

* docs: tighten contribution guidance and vision links

* docs: tighten PR scope and review-size policy in vision

* fix(gateway): block cross-session fallback in node event delivery

* fix(gateway): make health monitor checks single-flight

* fix(ios): harden share relay routing and delivery guards

* fix(telegram): normalize topic-create targets and add regression tests

* feat(cron): add default stagger controls for scheduled jobs

* fix(cron): retry next-second schedule compute on undefined

* docs(security): harden gateway security guidance

* feat(models): support anthropic sonnet 4.6

* fix: wire agents.defaults.imageModel into media understanding auto-discovery

resolveAutoEntries only checked a hardcoded list of providers
(openai, anthropic, google, minimax) when looking for an image model.
agents.defaults.imageModel was never consulted by the media understanding
pipeline — it was only wired into the explicit `image` tool.

Add resolveImageModelFromAgentDefaults that reads the imageModel config
(primary + fallbacks) and inserts it into the auto-discovery chain before
the hardcoded provider list.  runProviderEntry already falls back to
describeImageWithModel (via pi-ai) for providers not in the media
understanding registry, so no additional provider registration is needed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
(cherry picked from commit b381029ede)

* docs: update AGENTS instructions

* fix(subagent): harden read-tool overflow guards and sticky reply threading (#19508)

* fix(gateway): avoid premature agent.wait completion on transient errors

* fix(agent): preemptively guard tool results against context overflow

* fix: harden tool-result context guard and add message_id metadata

* fix: use importOriginal in session-key mock to include DEFAULT_ACCOUNT_ID

The run.skill-filter test was mocking ../../routing/session-key.js with only
buildAgentMainSessionKey and normalizeAgentId, but the module also exports
DEFAULT_ACCOUNT_ID which is required transitively by src/web/auth-store.ts.

Switch to importOriginal pattern so all real exports are preserved alongside
the mocked functions.

* pi-runner: guard accumulated tool-result overflow in transformContext

* PI runner: compact overflowing tool-result context

* Subagent: harden tool-result context recovery

* Enhance tool-result context handling by adding support for legacy tool outputs and improving character estimation for message truncation. This includes a new function to create legacy tool results and updates to existing functions to better manage context overflow scenarios.

* Enhance iMessage handling by adding reply tag support in send functions and tests. This includes modifications to prepend or rewrite reply tags based on provided replyToId, ensuring proper message formatting for replies.

* Enhance message delivery across multiple channels by implementing sticky reply context for chunked messages. This includes preserving reply references in Discord, Telegram, and iMessage, ensuring that follow-up messages maintain their intended reply targets. Additionally, improve handling of reply tags in system prompts and tests to support consistent reply behavior.

* Enhance read tool functionality by implementing auto-paging across chunks when no explicit limit is provided, scaling output budget based on model context window. Additionally, add tests for adaptive reading behavior and capped continuation guidance for large outputs. Update related functions to support these features.

* Refine tool-result context management by stripping oversized read-tool details payloads during compaction, ensuring repeated read calls do not bypass context limits. Introduce new utility functions for handling truncation content and enhance character estimation for tool results. Add tests to validate the removal of excessive details in context overflow scenarios.

* Refine message delivery logic in Matrix and Telegram by introducing a flag to track if a text chunk was sent. This ensures that replies are only marked as delivered when a text chunk has been successfully sent, improving the accuracy of reply handling in both channels.

* fix: tighten reply threading coverage and prep fixes (#19508) (thanks @tyler6204)

* fix(hooks): backport internal message hook bridge with safe delivery semantics

* fix(subagent): update SUBAGENT_SPAWN_ACCEPTED_NOTE for clarity on auto-announcement behavior

* fix: follow-up slack streaming routing/tests (#9972) (thanks @natedenh)

* fix: reduce default image dimension from 2000px to 1200px

Large images (2000px) consume excessive context tokens when sent to LLMs.
1200px provides sufficient detail for most use cases while significantly
reducing token usage.

The 5MB byte limit remains unchanged as JPEG compression at 1200px
naturally produces smaller files.

(cherry picked from commit 40182123dd)

* fix(agents): make image sanitization dimension configurable

* docs(tokens): document image dimension token tradeoffs

* Whatsapp/add resolve outbound target tests (#19345)

* test(whatsapp): add resolveWhatsAppOutboundTarget test suite

* style: auto-format files

* fix(test): correct mock order for invalid allowList entry test

* feat(skills): Add 'Use when / Don't use when' routing blocks (#14521)

* feat(skills): add 'Use when / Don't use when' blocks to skill descriptions

Based on OpenAI's Shell + Skills + Compaction best practices article.

Key changes:
- Added clear routing logic to skill descriptions
- Added negative examples to prevent misfires
- Added templates/examples to github skill
- Included Blake's specific setup notes for openhue

Skills updated:
- apple-reminders: Clarify vs Clawdbot cron
- github: Clarify vs local git operations
- imsg: Clarify vs other messaging channels
- openhue: Add device inventory, room layout
- tmux: Clarify vs exec tool
- weather: Add location defaults, format codes

Reference: https://developers.openai.com/blog/skills-shell-tips

* fix(skills): restore metadata and generic CLI examples

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* feat(agents): add generic provider api key rotation (#19587)

* feat(skills): improve descriptions with routing logic (#14577)

* feat(skills): improve descriptions with routing logic

Apply OpenAI's recommended pattern for skill descriptions:
- Add 'Use when' conditions for clear triggering
- Add 'NOT for' negative examples to reduce misfires
- Make descriptions act as routing logic, not marketing copy

Based on: https://developers.openai.com/blog/skills-shell-tips/

Skills updated:
- coding-agent: clarify when to delegate vs direct edit
- github: add boundaries vs browser/scripting
- weather: add scope limitations

Glean reported 20% drop in skill triggering without negative
examples, recovering after adding them. This change brings
Clawdbot skills in line with that pattern.

* docs(skills): clarify routing boundaries (openclaw#14577) (thanks @DylanWoodAkers)

* docs(changelog): add PR 14577 release note (openclaw#14577) (thanks @DylanWoodAkers)

---------

Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>

* Add frontend-design skill

* feat(telegram): add forum topic creation support (#10427)

Add `topic-create` action to the Telegram message adapter, enabling
programmatic creation of forum topics in supergroups.

Changes:
- Add `createForumTopicTelegram()` to `src/telegram/send.ts`
- Add `createForumTopic` handler in `telegram-actions.ts`
- Wire `topic-create` action in Telegram adapter
- Register `topic-create` in message action names and spec

The bot requires `can_manage_topics` permission in the target group.
Supports optional `iconColor` and `iconCustomEmojiId` parameters.

Closes #10427

* chore: fix formatting in frontend-design SKILL.md

* fix: add action gate check and config type for createForumTopic

Address review feedback:
- Add isActionEnabled() gate in telegram-actions.ts
- Add gate() check in telegram adapter listActions
- Add createForumTopic to TelegramActionConfig type

* fix(telegram): normalize topic-create targets and add regression tests

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
Co-authored-by: cpojer <christoph.pojer@gmail.com>
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
Co-authored-by: Josh Avant <830519+joshavant@users.noreply.github.com>
Co-authored-by: Shadow <hi@shadowing.dev>
Co-authored-by: Hongwei Ma <Marvae@users.noreply.github.com>
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <zaidi@uplause.io>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Sascha Reuter <s.reuter@geek-it.de>
Co-authored-by: sreuter <550246+sreuter@users.noreply.github.com>
Co-authored-by: Nimrod Gutman <nimrod.g@singular.net>
Co-authored-by: Vignesh <mailvgnsh@gmail.com>
Co-authored-by: Benjamin Jesuiter <bjesuiter@gmail.com>
Co-authored-by: Sam Padilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: Muhammed Mukhthar CM <mukhtharcm@gmail.com>
Co-authored-by: Mariano <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
Co-authored-by: Mariano Belinky <mbelinky@gmail.com>
Co-authored-by: Shadow <shadow@openclaw.ai>
Co-authored-by: Sk Akram <skcodewizard786@gmail.com>
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: Onur <onur@textcortex.com>
Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: Pablo Nunez <pnunfe@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Tyler Yust <64381258+tyler6204@users.noreply.github.com>
Co-authored-by: Han Xiao <han.xiao@jina.ai>
Co-authored-by: Verite Igiraneza <69280208+VeriteIgiraneza@users.noreply.github.com>
Co-authored-by: Blakeshannon <blake@blakeshannon.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: DylanWoodAkers <dylan@lec.com>
Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Claw <claw@openclaw.ai>
2026-02-18 01:38:44 +01:00
Peter Steinberger
76949001ea fix: compact skill paths in prompt (#14776) (thanks @bitfish3) 2026-02-18 01:35:37 +01:00
mac26ai
4f2c57eb4e feat(skills): compact skill paths with ~ to reduce prompt tokens
Replace absolute home directory prefix with ~ in skill <location> tags
injected into the system prompt. Models understand ~ expansion and the
read tool resolves it, so this is a safe, backward-compatible change.

Saves ~5-6 tokens per skill path. For a workspace with 90+ skills,
this reduces system prompt size by ~400-600 tokens.

Changes:
- Add compactSkillPaths() helper in workspace.ts
- Apply in buildWorkspaceSkillSnapshot and buildWorkspaceSkillsPrompt
- Add test for path compaction behavior

Before: /Users/alice/.bun/install/global/node_modules/openclaw/skills/github/SKILL.md
After:  ~/.bun/install/global/node_modules/openclaw/skills/github/SKILL.md
2026-02-18 01:35:37 +01:00
DylanWoodAkers
cfd384ead2 feat(skills): improve descriptions with routing logic (#14577)
* feat(skills): improve descriptions with routing logic

Apply OpenAI's recommended pattern for skill descriptions:
- Add 'Use when' conditions for clear triggering
- Add 'NOT for' negative examples to reduce misfires
- Make descriptions act as routing logic, not marketing copy

Based on: https://developers.openai.com/blog/skills-shell-tips/

Skills updated:
- coding-agent: clarify when to delegate vs direct edit
- github: add boundaries vs browser/scripting
- weather: add scope limitations

Glean reported 20% drop in skill triggering without negative
examples, recovering after adding them. This change brings
Clawdbot skills in line with that pattern.

* docs(skills): clarify routing boundaries (openclaw#14577) (thanks @DylanWoodAkers)

* docs(changelog): add PR 14577 release note (openclaw#14577) (thanks @DylanWoodAkers)

---------

Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-18 01:31:28 +01:00
Peter Steinberger
2e91552f09 feat(agents): add generic provider api key rotation (#19587) 2026-02-18 01:31:11 +01:00
Blakeshannon
9cce40d123 feat(skills): Add 'Use when / Don't use when' routing blocks (#14521)
* feat(skills): add 'Use when / Don't use when' blocks to skill descriptions

Based on OpenAI's Shell + Skills + Compaction best practices article.

Key changes:
- Added clear routing logic to skill descriptions
- Added negative examples to prevent misfires
- Added templates/examples to github skill
- Included Blake's specific setup notes for openhue

Skills updated:
- apple-reminders: Clarify vs Clawdbot cron
- github: Clarify vs local git operations
- imsg: Clarify vs other messaging channels
- openhue: Add device inventory, room layout
- tmux: Clarify vs exec tool
- weather: Add location defaults, format codes

Reference: https://developers.openai.com/blog/skills-shell-tips

* fix(skills): restore metadata and generic CLI examples

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-18 01:28:20 +01:00
Verite Igiraneza
6b5199ba2a Whatsapp/add resolve outbound target tests (#19345)
* test(whatsapp): add resolveWhatsAppOutboundTarget test suite

* style: auto-format files

* fix(test): correct mock order for invalid allowList entry test
2026-02-18 01:05:36 +01:00
Peter Steinberger
4c569ce246 docs(tokens): document image dimension token tradeoffs 2026-02-18 00:56:57 +01:00
Peter Steinberger
b05e89e5e6 fix(agents): make image sanitization dimension configurable 2026-02-18 00:54:20 +01:00
Han Xiao
5ee79f80eb fix: reduce default image dimension from 2000px to 1200px
Large images (2000px) consume excessive context tokens when sent to LLMs.
1200px provides sufficient detail for most use cases while significantly
reducing token usage.

The 5MB byte limit remains unchanged as JPEG compression at 1200px
naturally produces smaller files.

(cherry picked from commit 40182123dd)
2026-02-18 00:52:52 +01:00
Peter Steinberger
5b3ecadec3 Merge remote-tracking branch 'origin/main' 2026-02-18 00:51:04 +01:00
Peter Steinberger
1d23934c09 fix: follow-up slack streaming routing/tests (#9972) (thanks @natedenh) 2026-02-18 00:50:22 +01:00
Peter Steinberger
bb9a539d1d Merge remote-tracking branch 'prhead/feat/slack-text-streaming'
# Conflicts:
#	docs/channels/slack.md
#	src/config/types.slack.ts
#	src/slack/monitor/message-handler/dispatch.ts
2026-02-18 00:49:30 +01:00
Tyler Yust
b2acfd606a fix(subagent): update SUBAGENT_SPAWN_ACCEPTED_NOTE for clarity on auto-announcement behavior 2026-02-17 15:49:22 -08:00
Peter Steinberger
f07bb8e8fc fix(hooks): backport internal message hook bridge with safe delivery semantics 2026-02-18 00:35:41 +01:00
Tyler Yust
087dca8fa9 fix(subagent): harden read-tool overflow guards and sticky reply threading (#19508)
* fix(gateway): avoid premature agent.wait completion on transient errors

* fix(agent): preemptively guard tool results against context overflow

* fix: harden tool-result context guard and add message_id metadata

* fix: use importOriginal in session-key mock to include DEFAULT_ACCOUNT_ID

The run.skill-filter test was mocking ../../routing/session-key.js with only
buildAgentMainSessionKey and normalizeAgentId, but the module also exports
DEFAULT_ACCOUNT_ID which is required transitively by src/web/auth-store.ts.

Switch to importOriginal pattern so all real exports are preserved alongside
the mocked functions.

* pi-runner: guard accumulated tool-result overflow in transformContext

* PI runner: compact overflowing tool-result context

* Subagent: harden tool-result context recovery

* Enhance tool-result context handling by adding support for legacy tool outputs and improving character estimation for message truncation. This includes a new function to create legacy tool results and updates to existing functions to better manage context overflow scenarios.

* Enhance iMessage handling by adding reply tag support in send functions and tests. This includes modifications to prepend or rewrite reply tags based on provided replyToId, ensuring proper message formatting for replies.

* Enhance message delivery across multiple channels by implementing sticky reply context for chunked messages. This includes preserving reply references in Discord, Telegram, and iMessage, ensuring that follow-up messages maintain their intended reply targets. Additionally, improve handling of reply tags in system prompts and tests to support consistent reply behavior.

* Enhance read tool functionality by implementing auto-paging across chunks when no explicit limit is provided, scaling output budget based on model context window. Additionally, add tests for adaptive reading behavior and capped continuation guidance for large outputs. Update related functions to support these features.

* Refine tool-result context management by stripping oversized read-tool details payloads during compaction, ensuring repeated read calls do not bypass context limits. Introduce new utility functions for handling truncation content and enhance character estimation for tool results. Add tests to validate the removal of excessive details in context overflow scenarios.

* Refine message delivery logic in Matrix and Telegram by introducing a flag to track if a text chunk was sent. This ensures that replies are only marked as delivered when a text chunk has been successfully sent, improving the accuracy of reply handling in both channels.

* fix: tighten reply threading coverage and prep fixes (#19508) (thanks @tyler6204)
2026-02-17 15:32:52 -08:00
Peter Steinberger
75e11fed5d docs: update AGENTS instructions 2026-02-18 00:16:36 +01:00
Pablo Nunez
5acec7f79b fix: wire agents.defaults.imageModel into media understanding auto-discovery
resolveAutoEntries only checked a hardcoded list of providers
(openai, anthropic, google, minimax) when looking for an image model.
agents.defaults.imageModel was never consulted by the media understanding
pipeline — it was only wired into the explicit `image` tool.

Add resolveImageModelFromAgentDefaults that reads the imageModel config
(primary + fallbacks) and inserts it into the auto-discovery chain before
the hardcoded provider list.  runProviderEntry already falls back to
describeImageWithModel (via pi-ai) for providers not in the media
understanding registry, so no additional provider registration is needed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
(cherry picked from commit b381029ede)
2026-02-18 00:08:27 +01:00
Peter Steinberger
ae2c8f2cf0 feat(models): support anthropic sonnet 4.6 2026-02-18 00:00:31 +01:00
Peter Steinberger
a333d92013 docs(security): harden gateway security guidance 2026-02-17 23:48:49 +01:00
Peter Steinberger
dd4eb8bf63 fix(cron): retry next-second schedule compute on undefined 2026-02-17 23:48:14 +01:00
Peter Steinberger
c26cf6aa83 feat(cron): add default stagger controls for scheduled jobs 2026-02-17 23:48:14 +01:00
Peter Steinberger
b98b113b88 fix(ios): harden share relay routing and delivery guards 2026-02-17 23:47:34 +01:00
Peter Steinberger
442b45e54e fix(gateway): make health monitor checks single-flight 2026-02-17 23:47:29 +01:00
Peter Steinberger
96f7d35dd7 fix(gateway): block cross-session fallback in node event delivery 2026-02-17 23:47:24 +01:00
Peter Steinberger
4bd6a2b0d4 docs: tighten PR scope and review-size policy in vision 2026-02-17 23:40:09 +01:00
Peter Steinberger
3aa33f29e5 docs: tighten contribution guidance and vision links 2026-02-17 23:21:03 +01:00
Josh Avant
b20339a232 fix(signal): canonicalize message targets in tool and inbound flows 2026-02-17 14:17:22 -08:00
Peter Steinberger
9a2c39419e chore(release): bump version to 2026.2.17 2026-02-17 23:08:55 +01:00
Peter Steinberger
25a9e7ed97 docs(changelog): add missing 2026.2.16 entries and reorder by user impact 2026-02-17 23:05:08 +01:00
Shadow
2cf82c357e Docs: expand multi-agent routing 2026-02-17 14:28:08 -06:00
Mariano
bfc9736366 feat: share to openclaw ios app (#19424)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 0a7ab8589a
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-17 20:08:50 +00:00
Tyler Yust
81c5c02e53 changelog: add @tyler6204 credit for today's entries 2026-02-17 11:40:20 -08:00
Tyler Yust
75001a0490 fix cron announce routing and timeout handling 2026-02-17 11:40:04 -08:00
Tyler Yust
e1015a5197 fix(bluebubbles): recover outbound message IDs and include sender metadata 2026-02-17 11:39:58 -08:00
Nimrod Gutman
98962ed81d feat(ios): auto-select local signing team (#18421)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: bbb9c3aa48
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-02-18 03:16:10 +08:00
Tyler Yust
2362aac3db chore: document sessions_spawn response note and subagent context prefix 2026-02-17 11:05:37 -08:00
Onur
ab94295541 docs(slack): add assistant:write requirement for typing status 2026-02-18 02:22:54 +08:00
Sk Akram
c4e9bb3b99 fix: sanitize native command names for Telegram API (#19257)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: b608be3488
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-17 23:20:36 +05:30
Mariano
20a561224c iOS: use operator session for ChatSheet RPCs (#19320)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 0753b3a1a2
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-17 17:42:47 +00:00
Shadow
b251533e03 update my contributing list 2026-02-17 11:07:38 -06:00
Shadow
2e3219ff66 Update auto-response message for third-party extensions 2026-02-17 10:47:22 -06:00
Peter Steinberger
0978d63edd docs: add community plugins guide 2026-02-17 17:42:37 +01:00
Peter Steinberger
5923d3ff8a docs: add vision doc and link from README 2026-02-17 17:38:13 +01:00
Peter Steinberger
d85f0fc0c3 docs: refine maintainer application guidance 2026-02-17 17:38:13 +01:00
Peter Steinberger
dbda60d99b docs: add maintainer application section 2026-02-17 17:38:13 +01:00
Sebastian
2caf7e7612 docs(changelog): remove revert entries 2026-02-17 10:46:54 -05:00
Sebastian
e0e2184b90 test(release): add appcast regression coverage 2026-02-17 10:43:39 -05:00
Sebastian
19a8f8bbf6 test(cron): add model fallback regression coverage 2026-02-17 10:40:25 -05:00
Sebastian
e7c19cb52d test(telegram): cover autoSelectFamily env precedence 2026-02-17 10:10:32 -05:00
Seb Slight
9f261f592d revert: PR 18288 accidental merge (#19224)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 3cda31578c
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-17 10:05:29 -05:00
Sebastian
21978303a9 test(auto-reply): cover sender_id metadata 2026-02-17 10:02:26 -05:00
Sebastian
11fcbadec8 fix(daemon): guard preferred node selection 2026-02-17 10:01:54 -05:00
Sebastian
3f66280c3c test(sessions): add delivery info regression coverage 2026-02-17 10:00:08 -05:00
Sebastian
c0072be6a6 docs(cli): add components send example 2026-02-17 09:58:47 -05:00
Seb Slight
4536a6e05f revert(agents): revert base64 image validation (#19221) 2026-02-17 09:58:39 -05:00
Sebastian
bd1e7fadd5 test: cover cron telemetry and typed fetch mocks 2026-02-17 09:47:29 -05:00
Seb Slight
f44e3b2a34 revert: fix models set catalog validation (#19194)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7e3b2ff7af
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-17 09:43:41 -05:00
Shakker
6bb9b0656f Tests: fix fetch mock typings for type-aware checks 2026-02-17 14:34:41 +00:00
Sebastian
dd0b789669 fix(mattermost): surface reactions support 2026-02-17 09:30:50 -05:00
Shakker
2547b782d7 Agents: add before_message_write persistence regression tests 2026-02-17 14:29:41 +00:00
Shakker
ae93bc9f51 fix(gateway): make stale token cleanup non-fatal 2026-02-17 14:29:41 +00:00
Seb Slight
3211280bed revert: per-model thinkingDefault override (#19195)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: fe2c59e222
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-17 09:25:18 -05:00
Sebastian
5d1bcc76cc docs(zai): document tool_stream defaults 2026-02-17 09:22:55 -05:00
Sebastian
7caf874546 test(update): cover restart gating 2026-02-17 09:20:21 -05:00
Sebastian
a19ea7d400 test(discord): cover auto-thread skip types 2026-02-17 09:19:04 -05:00
Seb Slight
afd78133ba fix(ui): revert PR #18093 directive tags (#19188) 2026-02-17 09:16:13 -05:00
Seb Slight
d54e4af4a1 revert(agents): remove llms.txt discovery prompt (#19192) 2026-02-17 09:15:01 -05:00
Sebastian
747403be9b docs(readme): remove Android install link 2026-02-17 09:14:09 -05:00
Mariano Belinky
b114c82701 CLI: approve latest pending device request 2026-02-17 14:08:04 +00:00
Sebastian
cc359d338e test: add fetch mock helper and reaction coverage 2026-02-17 09:02:39 -05:00
Muhammed Mukhthar CM
0e023e300e Revert: fully roll back #17986 templates 2026-02-17 13:57:50 +00:00
Shakker
e2a93db430 test(discord): fix mock call arg typing 2026-02-17 13:56:30 +00:00
Shakker
1ee64d6c72 Revert "fix(browser): handle EADDRINUSE with automatic port fallback"
This reverts commit 0e6daa2e6e.
2026-02-17 13:56:30 +00:00
Shakker
66f5a4c698 Revert "fix(browser): track original port mapping for EADDRINUSE fallback"
This reverts commit 8e55503d77.
2026-02-17 13:56:30 +00:00
Shakker
b0d4c9b721 fix(discord): preserve DM lastRoute user target 2026-02-17 13:56:30 +00:00
Sebastian
7884d65687 test(feishu): cover post mentions for other users 2026-02-17 08:53:25 -05:00
Sebastian
17c4a03e2b test(discord): cover audioAsVoice replies 2026-02-17 08:49:26 -05:00
Sebastian
9772a28f0e test(gateway): cover trusted proxy trimming 2026-02-17 08:49:16 -05:00
Sebastian
e74ec2acd3 fix(cron): add spin-loop regression coverage 2026-02-17 08:48:11 -05:00
Sebastian
366da7569a fix(cli): honor update restart overrides 2026-02-17 08:47:25 -05:00
Sebastian
dff8692613 fix(discord): normalize command allowFrom prefixes 2026-02-17 08:45:41 -05:00
Sebastian
96fb276481 docs(changelog): note webhook session reuse fix 2026-02-17 08:44:42 -05:00
Sebastian
72ab24a157 test(cron): cover webhook session rollover overrides 2026-02-17 08:44:42 -05:00
Sebastian
7fca92ea93 test(web): fix baileys mock typing 2026-02-17 08:44:42 -05:00
Sebastian
111a24d55c fix(daemon): scope token drift warnings 2026-02-17 08:44:24 -05:00
Sebastian
210bc37971 chore(subagents): add regression coverage and changelog 2026-02-17 08:40:36 -05:00
Muhammed Mukhthar CM
85b5ac8520 Revert: fully roll back #17974 zh-cn UI README 2026-02-17 13:31:38 +00:00
Mariano
836e77449c iOS onboarding: stop auth step-3 retry loop churn (#19153)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: a38ec42bdd
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-17 13:12:53 +00:00
Mariano
0c87dbdcfc voice-call: harden closed-loop turn loop and transcript routing (#19140)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 14a3edb005
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-17 13:02:38 +00:00
Muhammed Mukhthar CM
bc4038149c Revert: undo #17974 README change 2026-02-17 12:23:26 +00:00
Nimrod Gutman
9f907320c3 Revert "fix: handle forum/topics in Telegram DM thread routing (#17980)"
This reverts commit e20b87f1ba.
2026-02-17 11:17:30 +02:00
Sam Padilla
32d12fcae9 feat(telegram): add channel_post support for bot-to-bot communication (#17857)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 27a343cd4d
Co-authored-by: theSamPadilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-17 14:44:18 +05:30
Nimrod Gutman
5db95cd8d5 fix(extensions): revert openai codex auth plugin (PR #18009) 2026-02-17 10:40:13 +02:00
Benjamin Jesuiter
19f8b6bf4f fix: searchable model picker in configure (#19010) (thanks @bjesuiter) 2026-02-17 09:15:55 +01:00
Benjamin Jesuiter
ddee6291eb Docs: add screenshot showing model picker usability issue 2026-02-17 09:15:55 +01:00
Benjamin Jesuiter
daef91800c Configure: improve searchable model picker token matching 2026-02-17 09:15:55 +01:00
Benjamin Jesuiter
01fcac0726 Configure: make model picker allowlist searchable 2026-02-17 09:15:55 +01:00
Ayaan Zaidi
900b97e3c7 test: type telegram action mock passthrough args 2026-02-17 13:30:29 +05:30
Ayaan Zaidi
7be63ec74a fix: align tool execute arg parsing for hooks 2026-02-17 13:30:29 +05:30
Ayaan Zaidi
f8b9e26c47 test: pass extensionContext in abort dedupe e2e 2026-02-17 13:30:29 +05:30
Ayaan Zaidi
1903c685c0 style: drop aidev-note prefix in telegram comments 2026-02-17 13:30:29 +05:30
Ayaan Zaidi
9d9630c83a fix: preserve telegram dm topic thread ids 2026-02-17 13:30:29 +05:30
Vignesh
f17b42d2f8 CI: remove formal models conformance workflow (#19007) 2026-02-16 23:52:24 -08:00
Nimrod Gutman
92de4031a3 Revert "fix(telegram): wire sendPollTelegram into channel action handler (#16977)"
This reverts commit 7bb9a7dcfc.
2026-02-17 09:45:08 +02:00
Nimrod Gutman
e727bca2dc Revert "Add Telegram polls action to config typing"
This reverts commit 5cbfaf5cc7.
2026-02-17 09:44:36 +02:00
Nimrod Gutman
33b59441d2 Revert "Fix Telegram poll action wiring"
This reverts commit 556b531a14.
2026-02-17 09:43:57 +02:00
Nimrod Gutman
b2fef5ebc4 Revert "Default Telegram polls to public"
This reverts commit c43e95e011.
2026-02-17 09:38:15 +02:00
Sascha Reuter
60dc3741c0 fix: before_tool_call hook double-fires with abort signal (#16852)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 6269d617f3
Co-authored-by: sreuter <550246+sreuter@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-17 12:53:54 +05:30
Ayaan Zaidi
583844ecf6 fix(telegram): avoid duplicate preview bubbles in partial stream mode (#18956)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: cf4eca71d4
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-17 12:36:15 +05:30
cpojer
5649e403df chore: Fix hanging test. 2026-02-17 15:56:51 +09:00
cpojer
73668bb963 chore: Fix broken test. 2026-02-17 15:54:17 +09:00
cpojer
bcf862f69f chore: Typecheck tests. 2026-02-17 15:50:07 +09:00
cpojer
048e29ea35 chore: Fix types in tests 45/N. 2026-02-17 15:50:07 +09:00
cpojer
52ad28e097 chore: Fix types in tests 44/N. 2026-02-17 15:50:07 +09:00
cpojer
688f86bf28 chore: Fix types in tests 43/N. 2026-02-17 15:50:07 +09:00
cpojer
7d2ef131c1 chore: Fix types in tests 42/N. 2026-02-17 15:50:07 +09:00
cpojer
6264c5e842 chore: Fix types in tests 41/N. 2026-02-17 15:50:07 +09:00
cpojer
3dc8d5656d chore: Fix types in tests 40/N. 2026-02-17 15:50:07 +09:00
cpojer
c4bd82d81d chore: Fix types in tests 39/N. 2026-02-17 15:50:07 +09:00
cpojer
084e39b519 chore: Fix types in tests 38/N. 2026-02-17 15:50:07 +09:00
cpojer
238718c1d8 chore: Fix types in tests 37/N. 2026-02-17 15:50:07 +09:00
cpojer
7b31e8fc59 chore: Fix types in tests 36/N. 2026-02-17 15:50:07 +09:00
cpojer
2a4ca7671e chore: Fix types in tests 35/N. 2026-02-17 15:50:07 +09:00
cpojer
ed75d30ad3 chore: Fix types in tests 34/N. 2026-02-17 15:50:07 +09:00
cpojer
49bd9f75f4 chore: Fix types in tests 33/N. 2026-02-17 15:50:07 +09:00
Ayaan Zaidi
f44b58fd58 style(telegram): format dispatch files 2026-02-17 11:26:14 +05:30
Hongwei Ma
7ffc8f9f7c fix(telegram): add initial message debounce for better push notifications (#18147)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 5e2285b6a0
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-17 11:21:49 +05:30
cpojer
2e375a5498 chore: Fix types in tests 32/N. 2026-02-17 14:33:38 +09:00
cpojer
116f5afea3 chore: Fix types in tests 31/N. 2026-02-17 14:33:26 +09:00
cpojer
f2f17bafbc chore: Fix types in tests 30/N. 2026-02-17 14:32:57 +09:00
cpojer
ecf1c955a1 chore: Fix types in tests 29/N. 2026-02-17 14:32:43 +09:00
cpojer
03e6acd051 chore: Fix types in tests 28/N. 2026-02-17 14:32:18 +09:00
cpojer
97c8f4999e chore: Fix types in tests 27/N. 2026-02-17 14:31:55 +09:00
cpojer
4235435309 chore: Fix types in tests 26/N. 2026-02-17 14:31:40 +09:00
cpojer
6e5df1dc0f chore: Fix types in tests 25/N. 2026-02-17 14:31:02 +09:00
cpojer
600022cdcc chore: Fix types in tests 24/N. 2026-02-17 14:30:36 +09:00
cpojer
be5b28cd6b chore: Fix types. 2026-02-17 13:40:17 +09:00
cpojer
d0cb8c19b2 chore: wtf. 2026-02-17 13:36:48 +09:00
Sebastian
ed11e93cf2 chore(format) 2026-02-16 23:20:16 -05:00
Sebastian
ca19745fa2 Revert "channels: migrate extension account listing to factory"
This reverts commit d24340d75b.
2026-02-16 23:17:13 -05:00
Shadow
e391827ea9 CI: use self-hosted for labeler/automation 2026-02-16 22:16:20 -06:00
Sebastian
f8adfcf60e test(agents): cover exec non-zero exits 2026-02-16 23:12:06 -05:00
Sebastian
4b40bdb98e fix(telegram): clear offsets on token change 2026-02-16 23:07:26 -05:00
Sebastian
65fa529e03 Revert "fix(whatsapp): allow per-message link preview override\n\nWhatsApp messages default to enabling link previews for URLs. This adds\nsupport for overriding this behavior per-message via the \nparameter (e.g. from tool options), consistent with Telegram.\n\nFix: Updated internal WhatsApp Web API layers to pass option\ndown to Baileys ."
This reverts commit 1bef2fc68b.
2026-02-16 22:59:37 -05:00
Sebastian
67014228cf fix(subagents): harden announce retry guards 2026-02-16 22:57:15 -05:00
Sebastian
f7d2e15a2e test: stabilize infra tests 2026-02-16 22:37:34 -05:00
Sebastian
759c7fc18e revert(voice-call): remove cached inbound greeting 2026-02-16 22:35:28 -05:00
Sebastian
950f36feff revert(voice-call): undo oxfmt formatting pass 2026-02-16 22:35:28 -05:00
Sebastian
833c646ec7 revert(voice-call): undo oxfmt formatting 2026-02-16 22:35:28 -05:00
Sebastian
ffe1ba68b9 revert(voice-call): undo cached greeting note 2026-02-16 22:35:28 -05:00
Sebastian
1486eb66fd revert(gateway): restore loopback auth setup 2026-02-16 22:35:27 -05:00
Sebastian
b7cf28f407 test(docker): cover browser install build arg 2026-02-16 22:35:27 -05:00
Sebastian
826e62a3bc fix(sessions): purge deleted transcript archives 2026-02-16 22:35:27 -05:00
Sebastian
52b624ccae fix(doctor): audit env-only gateway tokens 2026-02-16 22:35:27 -05:00
Sebastian
df8f7ff1ab docs(voice-call): document stale call reaper config 2026-02-16 22:26:31 -05:00
cpojer
cf6cdc74d0 chore: Fix types in tests 23/N. 2026-02-17 12:24:03 +09:00
cpojer
8d6e345338 chore: Fix types in tests 22/N. 2026-02-17 12:23:12 +09:00
cpojer
245018fd6b chore: Fix types in tests 21/N. 2026-02-17 12:23:12 +09:00
cpojer
18cc48dfd9 chore: Fix types in tests 20/N. 2026-02-17 12:23:12 +09:00
cpojer
e09643e82c chore: chore: Fix types in tests 19/N. 2026-02-17 12:23:12 +09:00
Sebastian
ecfc5a5ee7 test(agents): cover tool result media placeholders 2026-02-16 22:21:00 -05:00
Sebastian
68634468f5 chore(format): fix test import order 2026-02-16 22:18:03 -05:00
Sebastian
bfaa03981b test(voice-call): cover stream disconnect auto-end 2026-02-16 22:13:08 -05:00
Sebastian
78c3e5166b test(telegram): cover getFile file-too-big errors 2026-02-16 22:10:59 -05:00
Sebastian
d137f33281 test(status): cover token summary variants 2026-02-16 22:10:07 -05:00
Sebastian
def0254169 test(session): cover stale threadId fallback 2026-02-16 22:08:51 -05:00
Sebastian
7a00f056af revert(sandbox): revert SHA-1 slug restoration 2026-02-16 22:03:41 -05:00
cpojer
6b8c0bc697 chore: Format files. 2026-02-17 12:00:38 +09:00
cpojer
8ece8215aa chore: Fix types in tests 18/N. 2026-02-17 12:00:29 +09:00
cpojer
43c97d18aa chore: Fix types in tests 17/N. 2026-02-17 12:00:29 +09:00
cpojer
7bc783cb03 chore: Fix types in tests 16/N. 2026-02-17 12:00:29 +09:00
cpojer
a76a9c375f chore: Fix types in tests 15/N. 2026-02-17 12:00:29 +09:00
cpojer
db3529e924 chore: Fix types in tests 14/N. 2026-02-17 12:00:29 +09:00
cpojer
50fd2a99ba chore: Fix types in tests 13/N. 2026-02-17 12:00:29 +09:00
Sebastian
81fd771cb9 fix(gateway): preserve chat.history context under hard caps 2026-02-16 21:50:01 -05:00
Sebastian
f6e68b917b docs(cron): clarify webhook posting summary condition 2026-02-16 21:48:57 -05:00
Sebastian
6070116382 revert(exec): undo accidental merge of PR #18521 2026-02-16 21:47:18 -05:00
Sebastian
ae82371d8a revert(docs): undo accidental merge of #18516 2026-02-16 21:46:45 -05:00
Sebastian
3df8305cb6 fix(ui): gate sessions refresh on successful delete 2026-02-16 21:46:04 -05:00
cpojer
9c5f08244e chore: Format files. 2026-02-17 11:37:11 +09:00
Sebastian
391796a3fb fix(agents): restore multi-image image tool schema contract 2026-02-16 21:34:27 -05:00
Sebastian
966e5560f8 revert(telegram): undo accidental merge of PR #18564 2026-02-16 21:29:00 -05:00
Peter Steinberger
0c1c34c950 refactor(plugins): split before-agent hooks by model and prompt phases 2026-02-17 03:28:20 +01:00
Peter Steinberger
a75e95be02 fix(reply): track messaging media aliases for dedupe 2026-02-17 03:27:23 +01:00
Peter Steinberger
1f850374f6 fix(gateway): harden channel health monitor recovery 2026-02-17 03:26:26 +01:00
Sebastian
4aed4eedb7 test(extensions): cast fetch mocks to satisfy tsgo 2026-02-16 21:25:35 -05:00
Sebastian
f7e75d2c5c fix(doctor): repair googlechat open dm wildcard auto-fix 2026-02-16 21:25:35 -05:00
Josh Avant
81741c37fd fix(gateway): remove watch-mode build/start race (#18782) 2026-02-17 11:24:08 +09:00
cpojer
4b8f53979e chore: Fix type errors from reverts. 2026-02-17 11:22:49 +09:00
cpojer
262b7a157a chore: chore: Fix types in tests 12/N. 2026-02-17 11:22:49 +09:00
cpojer
e02feaff83 chore: Fix types in tests 11/N. 2026-02-17 11:22:49 +09:00
cpojer
058eb85762 chore: Fix types in tests 10/N. 2026-02-17 11:22:49 +09:00
cpojer
95f344e433 chore: Fix types in tests 9/N. 2026-02-17 11:22:49 +09:00
cpojer
5dc8983954 chore: Fix types in tests 8/N. 2026-02-17 11:22:49 +09:00
cpojer
ac38d51290 chore: Fix types in tests 7/N. 2026-02-17 11:22:49 +09:00
Sebastian
0aa28c71ca fix(doctor): move forced exit to top-level command 2026-02-16 21:20:05 -05:00
Peter Steinberger
901d4cb310 revert: accidental merge of OC-09 sandbox env sanitization change 2026-02-17 03:19:42 +01:00
Sebastian
f79cf3a01d revert: remove accidentally merged video-quote-finder skill (#18550) 2026-02-16 21:16:29 -05:00
cpojer
a78839e60c chore: Fix Slack test. 2026-02-17 11:15:15 +09:00
Sebastian
bb8df6ab8d revert(tools): finish rollback of PR #18584 2026-02-16 21:13:48 -05:00
Sebastian
f924ab40d8 revert(tools): undo accidental merge of PR #18584 2026-02-16 21:13:48 -05:00
Sebastian
0158e41298 Revert "fix: resolve #12770 - update Antigravity default model and trim leading whitespace in BlueBubbles replies"
This reverts commit e179d453c7.
2026-02-16 21:11:53 -05:00
Peter Steinberger
fb6e415d0c fix(agents): align session lock hold budget with run timeouts 2026-02-17 03:10:36 +01:00
Sebastian
ce4b4d947c revert(doctor): undo accidental merge of PR #18591 2026-02-16 21:09:49 -05:00
Sebastian
4147545469 Revert "feat: show transcript file size in session status"
This reverts commit 15dd2cda20.
2026-02-16 21:04:29 -05:00
Peter Steinberger
9789dfd95b fix(ui): correct usage range totals and muted styles 2026-02-17 03:04:00 +01:00
Sebastian
4ca75bed56 fix(models): sync auth-profiles before availability checks 2026-02-16 21:00:59 -05:00
Sebastian
fbda9a93fd fix(failover): align abort timeout detection and regressions 2026-02-16 21:00:27 -05:00
Peter Steinberger
f242246839 fix(subagents): pass group context in /subagents spawn 2026-02-17 03:00:01 +01:00
Sebastian
2b3ecee7c5 fix(actions): layer per-account gate fallback 2026-02-16 20:59:30 -05:00
cpojer
616c0bd4c7 chore: Cleanup unused vars that were leftover from the reverts. 2026-02-17 10:57:31 +09:00
cpojer
b3d9ecf4e4 chore: Fix types that were broken due to reverts. 2026-02-17 10:57:31 +09:00
cpojer
01ea808876 chore: Format files. 2026-02-17 10:57:31 +09:00
cpojer
003d6c45d6 chore: Fix types in tests 6/N. 2026-02-17 10:57:31 +09:00
cpojer
b6d4f7c00e chore: Fix types in tests 5/N. 2026-02-17 10:57:31 +09:00
cpojer
c49234cbfb chore: chore: Fix types in tests 4/N. 2026-02-17 10:57:31 +09:00
cpojer
1406b28469 chore: Fix types in tests 3/N. 2026-02-17 10:57:31 +09:00
Sebastian
3518554e23 fix(heartbeat): bound responsePrefix strip for ack detection 2026-02-16 20:56:55 -05:00
Peter Steinberger
c219c85df3 docs(changelog): record PR 18608 fixups 2026-02-17 02:56:45 +01:00
Peter Steinberger
afa5533253 fix(mattermost): harden react remove flag parsing 2026-02-17 02:55:46 +01:00
Peter Steinberger
d6226355e6 fix(slack): validate interaction payloads and handle malformed actions 2026-02-17 02:51:00 +01:00
Sebastian
bbb5fbc71f fix(scripts): harden Windows UI spawn behavior 2026-02-16 20:49:09 -05:00
Peter Steinberger
742e6543c7 fix(ui): preserve locale bootstrap and trusted-proxy overview behavior 2026-02-17 02:46:24 +01:00
Sebastian
accb673490 revert(telegram): undo accidental merge of PR #18601 2026-02-16 20:46:05 -05:00
Sebastian
3fff266d52 fix(session-memory): harden reset transcript recovery 2026-02-16 20:39:06 -05:00
Sebastian
f818de7bef docs(changelog): note slack forwarded attachment hotfix 2026-02-16 20:38:03 -05:00
Sebastian
3793424f5f docs(changelog): note process kill-tree hotfix 2026-02-16 20:37:22 -05:00
Sebastian
67250f059a fix(slack): scope attachment extraction to forwarded shares 2026-02-16 20:37:08 -05:00
Sebastian
fb996031bc fix(process): harden graceful kill-tree cancellation semantics 2026-02-16 20:37:08 -05:00
Gustavo Madeira Santana
7b172d61cd Revert "fix: respect OPENCLAW_HOME for isolated gateway instances"
This reverts commit 34b18ea9db.
2026-02-16 20:36:01 -05:00
Peter Steinberger
014a46d3fc Revert "fix: session-memory hook finds previous session file after /new/reset"
This reverts commit d6acd71576.
2026-02-17 02:34:09 +01:00
Gustavo Madeira Santana
a1538ea637 Revert "fix: flatten remaining anyOf/oneOf in Gemini schema cleaning"
This reverts commit 06b961b037.
2026-02-16 20:33:58 -05:00
Peter Steinberger
c0c367fde7 docs: clarify discord proxy scope for startup REST calls 2026-02-17 02:30:55 +01:00
Peter Steinberger
2992639f88 Revert "feat: add Linq channel — real iMessage via API, no Mac required"
This reverts commit d4a142fd8f.
2026-02-17 02:30:55 +01:00
Peter Steinberger
a36782e342 Revert "feat(linq): add interactive onboarding adapter"
This reverts commit b91e43714b.
2026-02-17 02:30:55 +01:00
Gustavo Madeira Santana
0d1eceb9cf Revert "Onboarding: fix webchat URL loopback and canonical session"
This reverts commit 59e0e7e4ff.
2026-02-16 20:30:03 -05:00
Sebastian
726ad45c75 Revert "fix: add windowsHide: true to spawn in runCommandWithTimeout"
This reverts commit 32c66aff49.
2026-02-16 20:27:32 -05:00
Gustavo Madeira Santana
22b2a77b30 Revert "fix(docker): ensure memory-lancedb deps installed in Docker image"
This reverts commit 2ab6313d99.
2026-02-16 20:27:19 -05:00
Gustavo Madeira Santana
63aa5c5a45 Revert "fix: remove stderr suppression so install failures are visible in build logs"
This reverts commit 717caa97fb.
2026-02-16 20:27:19 -05:00
cpojer
950d5a46b2 chore: Fix types in tests 2/N. 2026-02-17 10:26:49 +09:00
cpojer
0cf443afe8 chore: Fix types in tests 1/N. 2026-02-17 10:26:49 +09:00
Peter Steinberger
25126d75c3 Revert "Agents: improve Windows scaffold helpers for venture studio"
This reverts commit b6d934c2c7.
2026-02-17 02:26:36 +01:00
Gustavo Madeira Santana
37064e5cc6 Revert "feat(docker): add init script support via /openclaw-init.d/"
This reverts commit 53af9f7437.
2026-02-16 20:25:46 -05:00
Gustavo Madeira Santana
09c82a1fbf Revert "fix: capture init script exit codes instead of swallowing via pipe"
This reverts commit 8b14052ebe.
2026-02-16 20:25:46 -05:00
Peter Steinberger
83392d3927 Revert "fix(gateway): set explicit chat timeouts for mesh gateway calls"
This reverts commit c529e6005a.
2026-02-17 02:25:31 +01:00
Peter Steinberger
563df56389 Revert "config: align memory hybrid UI metadata with schema labels/help"
This reverts commit 7d8d8c338b.
2026-02-17 02:24:48 +01:00
Peter Steinberger
4fa35d3fd9 Revert "fix: use resolveUserPath utility for tilde expansion"
This reverts commit f82a3d3e2b.
2026-02-17 02:24:31 +01:00
Peter Steinberger
d4385e67aa chore(docs): drop accidental .DS_Store artifacts 2026-02-17 02:23:41 +01:00
Peter Steinberger
c65b3c2ed9 fix(docs): revert accidental es/pt-BR translation scaffold from #18473 2026-02-17 02:23:41 +01:00
Sebastian
6d451c8205 test(ollama): add reasoning fallback regression coverage 2026-02-16 20:20:47 -05:00
sebslight
83b1ae895e fix(transcript): always drop orphaned OpenAI reasoning blocks 2026-02-16 20:20:32 -05:00
cpojer
6229814af2 chore: Remove invalid tsconfig paths reference. 2026-02-17 10:15:10 +09:00
cpojer
ff8316e04e chore: Fix formatting. 2026-02-17 10:14:13 +09:00
cpojer
d3a36cc3b0 chore: Fix remaining extension test types, enable type checking for extension tests. 2026-02-17 10:14:01 +09:00
cpojer
a741985574 chore: Fix more extension test types, 2/N. 2026-02-17 10:14:01 +09:00
cpojer
72f00df95a chore: Fix more extension test type 1/N. 2026-02-17 10:14:01 +09:00
cpojer
0f8d1f175a chore: Fix type errors in extensions/twitch tests. 2026-02-17 10:14:00 +09:00
cpojer
889f221ed1 chore: Fix type errors in extensions/bluebubbles tests. 2026-02-17 10:14:00 +09:00
Peter Steinberger
6244ef9ea8 fix: handle Windows and UNC bind mount parsing 2026-02-17 02:08:56 +01:00
Peter Steinberger
13ae1ae056 fix(memory): tighten embedding manager inheritance types 2026-02-17 00:59:54 +00:00
Peter Steinberger
5115f6fdf3 style: normalize imports for oxfmt 0.33 2026-02-17 00:59:54 +00:00
Peter Steinberger
dcdbbd8b3b test: replace ui prototype method patches with instance stubs 2026-02-17 01:57:51 +01:00
Peter Steinberger
c20ef582cb fix: align cron session key routing (#18637) (thanks @vignesh07) 2026-02-17 01:54:59 +01:00
Vignesh Natarajan
064a3079cb Heartbeat: queue pending wakes per target 2026-02-17 01:54:59 +01:00
Vignesh Natarajan
a7c25f203a Protocol: regenerate cron Swift models 2026-02-17 01:54:59 +01:00
Vignesh Natarajan
a258503590 Cron: dedupe gateway wake target resolution 2026-02-17 01:54:59 +01:00
Vignesh Natarajan
f988abf202 Cron: route reminders by session namespace 2026-02-17 01:54:59 +01:00
Peter Steinberger
f452a7a60b refactor(shared): reuse chat content extractor for assistant text 2026-02-17 00:53:44 +00:00
Peter Steinberger
ddef3cadba refactor: replace memory manager prototype mixing 2026-02-17 01:50:04 +01:00
Peter Steinberger
7649f9cba4 refactor(test): share heartbeat sandbox fixtures 2026-02-17 00:49:42 +00:00
Peter Steinberger
b9e7299a70 refactor(test): share embedded runner overflow mocks 2026-02-17 00:49:37 +00:00
Peter Steinberger
9032a50981 refactor: reuse sandbox path expansion in apply-patch 2026-02-17 00:45:02 +00:00
Peter Steinberger
7687f6cfcd refactor: reuse runtime requires evaluation 2026-02-17 00:45:02 +00:00
Peter Steinberger
5195179150 refactor: centralize plugin allowlist mutation 2026-02-17 00:45:02 +00:00
Peter Steinberger
7147cd9cc0 refactor: dedupe process-scoped lock maps 2026-02-17 00:45:02 +00:00
cpojer
c70597daeb chore: Fix formatting. 2026-02-17 09:40:00 +09:00
cpojer
194608d0dd chore: Remove leftover file. 2026-02-17 09:33:26 +09:00
Peter Steinberger
dee0134269 style: reformat dedupe-touched files 2026-02-17 00:32:34 +00:00
Peter Steinberger
817b5812e1 refactor(agents): share queued JSONL file writer 2026-02-17 00:32:34 +00:00
Peter Steinberger
80c7d04ad2 refactor(cron): reuse shared run outcome telemetry types 2026-02-17 00:32:34 +00:00
Peter Steinberger
a6466f2576 refactor(web-tools): share URL allowlist resolver 2026-02-17 00:32:34 +00:00
Peter Steinberger
64fc82844e refactor(channels): share prefixed target parsing 2026-02-17 00:32:34 +00:00
Peter Steinberger
10b060dbd3 refactor(agent-tools): reuse gateway option parsing 2026-02-17 00:32:34 +00:00
Peter Steinberger
37c97964af refactor(media): centralize input file limit resolution 2026-02-17 00:32:34 +00:00
Peter Steinberger
ed74f48bd5 refactor(status): share update channel display + one-liner 2026-02-17 00:32:34 +00:00
cpojer
1dc9bb8d62 chore: Fix more type issues. 2026-02-17 09:29:47 +09:00
Peter Steinberger
8ae93cce53 docs: add ts-suppression guardrails 2026-02-17 01:26:06 +01:00
cpojer
843acd52b7 chore: Fix up Oxlint/Oxfmt ignore patterns. 2026-02-17 09:20:04 +09:00
cpojer
90ef2d6bdf chore: Update formatting. 2026-02-17 09:18:40 +09:00
cpojer
1e13a3933c chore: Update deps. 2026-02-17 09:14:42 +09:00
Peter Steinberger
5cbdd3a9c1 test(auto-reply): dedupe command spawn test harness 2026-02-17 00:11:02 +00:00
Peter Steinberger
b9aed3a07c refactor(infra): reuse device auth scope normalization 2026-02-17 00:11:02 +00:00
Peter Steinberger
fbd3786e7a refactor(channels): share target parsing helpers 2026-02-17 00:11:02 +00:00
Peter Steinberger
9bfd3ca195 refactor(memory): consolidate embeddings and batch helpers 2026-02-17 00:11:02 +00:00
Peter Steinberger
423b7a0f28 refactor(auto-reply): reuse embedded run context helpers 2026-02-17 00:11:02 +00:00
Peter Steinberger
246bb7f30f refactor(agents): share model auth label resolution 2026-02-17 00:11:02 +00:00
Shadow
ff2e790e03 CI: increase stale operations per run 2026-02-16 18:06:35 -06:00
Peter Steinberger
4088c0b89d refactor(core): dedupe schema and command parsing helpers 2026-02-16 23:48:43 +00:00
Peter Steinberger
c55e017c19 refactor(daemon): dedupe user bin path assembly helpers 2026-02-16 23:48:43 +00:00
Peter Steinberger
3451159174 refactor(channels): share draft stream loop across slack and telegram 2026-02-16 23:48:43 +00:00
Peter Steinberger
f6111622e6 refactor(commands): share system prompt bundle for context and export 2026-02-16 23:48:43 +00:00
Peter Steinberger
32e2c369d7 refactor(agents): extract shared session dir resolver 2026-02-16 23:48:43 +00:00
Peter Steinberger
82c4f8ca22 chore(ci): align lint and format checks for templated exports 2026-02-16 23:48:43 +00:00
Peter Steinberger
170e6f33b9 docs(commands): add export-session aliases to slash command list 2026-02-16 23:48:43 +00:00
Peter Steinberger
b3d0e0cb45 fix(cron): preserve overrides and harden next-run calculation 2026-02-16 23:48:26 +00:00
Peter Steinberger
968bba5c18 refactor(telegram): remove duplicate poll dispatch branch 2026-02-16 23:47:57 +00:00
Peter Steinberger
0a188ee49a test(ci): stabilize update and discord process tests 2026-02-16 23:47:57 +00:00
Peter Steinberger
a186ce2158 fix(ci): preserve whatsapp send API compatibility 2026-02-16 23:47:57 +00:00
Peter Steinberger
94e4631171 refactor(onboarding): simplify zalo allowFrom merge paths 2026-02-16 23:47:57 +00:00
Peter Steinberger
d89d951c3e refactor(onboarding): reuse allowFrom merge helper in matrix 2026-02-16 23:47:57 +00:00
Peter Steinberger
7632e60d70 refactor(onboarding): reuse allowFrom merge helper in extensions 2026-02-16 23:47:57 +00:00
Peter Steinberger
12a947223b fix(ci): restore main checks after bulk merges 2026-02-16 23:47:27 +00:00
Peter Steinberger
8c241449f5 fix(protocol): sync generated gateway swift models 2026-02-16 23:33:05 +00:00
Peter Steinberger
83a8b78a42 fix(ci): guard loop detection integer parsing 2026-02-16 23:27:35 +00:00
Peter Steinberger
eaa2f7a7bf fix(ci): restore main lint/typecheck after direct merges 2026-02-16 23:26:11 +00:00
Peter Steinberger
076df941a3 feat: add configurable tool loop detection 2026-02-17 00:17:01 +01:00
Rain
dacffd7ac8 fix(sandbox): parse Windows bind mounts in fs-path mapping 2026-02-17 00:02:12 +01:00
尹凯
3f617e33b7 style(discord): format provider after proxy fetch changes 2026-02-17 00:02:09 +01:00
尹凯
e997545d4b fix(discord): apply proxy to app-id and allowlist REST lookups 2026-02-17 00:02:09 +01:00
Jonathan Gelin
bc2e02bb34 fix(ui/usage): remove remaining timeSeriesCursor reference in renderContextPanel 2026-02-17 00:02:05 +01:00
Jonathan Gelin
647d69881b fix(ui/usage): align client log limit with server cap (1000) and remove unused param
- Client requested 2000 logs but server caps at 1000
- Remove unused timeSeriesCursor param from renderContextPanel
2026-02-17 00:02:05 +01:00
Jonathan Gelin
0302cf89b0 feat(timeline): dual-handle range selection on Usage Over Time chart
- Dual drag handles on SVG chart for time range selection
- Bars outside range dimmed, stats + conversation filtered to range
- Slot-based bar sizing prevents overflow at any point count
- Handle-only drag zones with col-resize cursor
- Reset button to clear selection
- computeFilteredUsage() helper with 8 unit tests
- Named constants, CSS classes instead of inline styles
2026-02-17 00:02:05 +01:00
gaowanqi08141999
86517b8e30 feat(feishu): add bitable create app and create field tools 2026-02-17 00:02:00 +01:00
ikari
84383b5e0f fix(tts): show all provider errors instead of only the last one
When TTS conversion fails, the error message now includes failures
from every provider in the fallback chain instead of only the last
one tried. Previously, a timeout on the primary provider (e.g.
ElevenLabs) would be masked by the final fallback's error (e.g.
"edge: disabled"), making it impossible to diagnose the real issue.

Before: "TTS conversion failed: edge: disabled"
After:  "TTS conversion failed: elevenlabs: timeout (30004ms); openai: no API key; edge: disabled"
2026-02-17 00:01:56 +01:00
Operative-001
de6cc05e7e fix(cron): prevent spin loop when job completes within firing second (#17821)
When a cron job fires at 13:00:00.014 and completes at 13:00:00.021,
computeNextRunAtMs was flooring nowMs to 13:00:00.000 and asking croner
for the next occurrence from that exact boundary. Croner could return
13:00:00.000 (same second) since it uses >= semantics, causing the job
to be immediately re-triggered hundreds of times.

Fix: Ask croner for the next occurrence starting from the NEXT second
(e.g., 13:00:01.000). This ensures we always skip the current/elapsed
second and correctly return the next day's occurrence.

This also correctly handles the before-match case: if nowMs is
11:59:59.500, we ask from 12:00:00.000, and croner returns today's
12:00:00.000 match.

Added regression tests for the spin loop scenario.
2026-02-17 00:01:53 +01:00
度人自度
531f735c8a Update extensions/openclaw-zh-cn-ui/README.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-17 00:01:46 +01:00
度人自度
aeadeffa15 Update README.md 2026-02-17 00:01:46 +01:00
度人自度
967efc8e1b Update README.md 2026-02-17 00:01:46 +01:00
度人自度
c88a90c7c3 Create README.md 2026-02-17 00:01:46 +01:00
Jonathan Gelin
bdbb872c07 fix(ui/usage): replace undefined --text-muted CSS variable with --muted
The usage tab styles referenced var(--text-muted) which is not defined
anywhere in the CSS. This resolved to transparent/initial, making text
invisible in dark mode. The correct variable is var(--muted) (#71717a),
which is used throughout the rest of the UI (85+ occurrences).

47 occurrences fixed across 3 style files.
2026-02-17 00:01:42 +01:00
Operative-001
16ddbbc628 fix(sessions): skip cache when initializing session state
Fixes #17971

When initSessionState() reads the session store, use skipCache: true
to ensure fresh data from disk. The session store cache is process-local
and uses mtime-based invalidation, which can fail in these scenarios:

1. Multiple gateway processes (each has separate in-memory cache)
2. Windows file system where mtime granularity may miss rapid writes
3. Race conditions between messages 6-8 seconds apart

Symptoms: 134+ orphaned .jsonl transcript files, each with only 1
exchange. Session rotates on every incoming message even when
sessionKey is stable.

Root cause: loadSessionStore() returns stale cache → entry not found
for sessionKey → new sessionId generated → new transcript file.

The fix ensures session identity (sessionId) is always resolved from
the latest on-disk state, not potentially-stale cache.
2026-02-17 00:01:37 +01:00
Elie Habib
5b3873add4 fix(skills): guard against skills prompt bloat 2026-02-17 00:01:34 +01:00
Ibrahim Qureshi
4f5b9da503 Update docs/reference/templates/SOUVENIR.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-17 00:01:30 +01:00
Ibrahim Qureshi
8a3f3a49a5 Update docs/reference/templates/GOALS.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-17 00:01:30 +01:00
Ibrahim Qureshi
bf1b4386df Update docs/reference/templates/GOALS.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-17 00:01:30 +01:00
Ibrahim Qureshi
1a9a2e396f feat: Add GOALS.md and SOUVENIR.md template files
- GOALS.md: Direction & Execution Strategy template
- SOUVENIR.md: Memory & Reflection Layer template
- Both files pass oxfmt formatting check
2026-02-17 00:01:30 +01:00
artale
b4a90bb743 fix(telegram): suppress message_thread_id for private chat sends (#17242)
Private chats (positive numeric chat IDs) never support forum topics.
Sending message_thread_id to a private chat causes Telegram to reject
the request with '400: Bad Request: message thread not found', silently
dropping the message.

Guard all three send functions (sendMessageTelegram, sendStickerTelegram,
sendPollTelegram) to omit thread-related parameters when the target is a
private chat.

Root cause: the auto-reply pipeline can set messageThreadId from a
previous forum-group context, then reuse it when sending a DM.

Tests: add private-chat suppression assertions; update existing thread-
retry tests to use group chat IDs so the retry path is still exercised.
2026-02-17 00:01:26 +01:00
simonemacario
2ed43fd7b4 fix(cron): resolve accountId from agent bindings in isolated sessions
When an isolated cron session has no lastAccountId (e.g. first-run or
fresh session), the message tool receives an undefined accountId which
defaults to "default". In multi-account setups where accounts are named
(e.g. "willy", "betty"), this causes resolveTelegramToken() to fail
because accounts["default"] doesn't exist.

This change adds a fallback in resolveDeliveryTarget(): when the
session-derived accountId is undefined, look up the agent's bound
account from the bindings config using buildChannelAccountBindings().
This mirrors the same binding resolution used for inbound routing,
closing the gap between inbound and outbound account resolution.

Session-derived accountId still takes precedence when present.

Fixes #17889
Related: #12628, #16259

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 00:01:22 +01:00
Operative-001
e9f2e6a829 fix(heartbeat): prune transcript for HEARTBEAT_OK turns
When a heartbeat run results in HEARTBEAT_OK (or empty/duplicate), the user+assistant
turns are now pruned from the session transcript. This prevents context window
pollution from zero-information exchanges.

Implementation:
- captureTranscriptState(): records transcript file path and size before heartbeat
- pruneHeartbeatTranscript(): truncates file back to pre-heartbeat size
- Called in ok-empty, ok-token, and duplicate cases (same places as restoreHeartbeatUpdatedAt)

This extends the existing pattern where delivery is suppressed and updatedAt is restored
for HEARTBEAT_OK responses - now the transcript is also cleaned up.

Fixes #17804
2026-02-17 00:01:15 +01:00
artale
7bb9a7dcfc fix(telegram): wire sendPollTelegram into channel action handler (#16977)
The Telegram channel adapter listed no 'poll' action, so agents could
not create polls via the unified action interface. The underlying
sendPollTelegram function was already implemented but unreachable.

Changes:
- telegram.ts: add 'poll' to listActions (enabled by default via gate),
  add handleAction branch that reads pollQuestion/pollOption params and
  delegates to handleTelegramAction with action 'sendPoll'.
- telegram-actions.ts: add 'sendPoll' handler that validates question,
  options (≥2), and forwards to sendPollTelegram with threading, silent,
  and anonymous options.
- actions.test.ts: add test verifying poll action routes correctly.

Fixes #16977
2026-02-17 00:01:07 +01:00
amabito
068b9c9749 feat: wrap compaction generateSummary in retryAsync
Integrate retry logic with abort-classifier for /compact endpoint:
- Wrap generateSummary calls in retryAsync with exponential backoff
- Auto-skip retry on user cancellation and gateway restart (AbortError)
- Config: 3 attempts, 500ms-5s delay, 20% jitter
- Add comprehensive Vitest tests (5/5 passed)

Related: #16809, #5744, #17143
2026-02-17 00:01:03 +01:00
Ralph
990cf2d226 fix(extensions): address greptile review comments for openai-codex-auth
- Change provider ID from 'openai-codex' to 'openai-codex-import' to avoid
  conflict with core's built-in openai-codex provider
- Update model prefix from 'openai/' to 'openai-codex/' to match core's
  namespace convention and avoid collision with standard OpenAI API provider
- Use correct Codex models (gpt-5.3-codex, gpt-5.2-codex) instead of generic
  OpenAI models (gpt-4.1, o1, o3)
- Respect CODEX_HOME env var when resolving auth file path, matching core
  behavior in src/agents/cli-credentials.ts
- Validate refresh token presence and throw clear error instead of using
  empty string which causes silent failures
2026-02-17 00:01:00 +01:00
Ralph
45b3c883b8 fix: regenerate pnpm lockfile 2026-02-17 00:01:00 +01:00
Ralph
24569d093a style: fix import ordering in openai-codex-auth 2026-02-17 00:01:00 +01:00
Ralph
3ac422fe2e feat(extensions): add OpenAI Codex CLI auth provider
Adds a new authentication provider that reads OAuth tokens from the
OpenAI Codex CLI (~/.codex/auth.json) to authenticate with OpenAI's API.

This allows ChatGPT Plus/Pro subscribers to use OpenAI models in OpenClaw
without needing a separate API key - just authenticate with 'codex login'
first, then enable this plugin.

Features:
- Reads existing Codex CLI credentials from ~/.codex/auth.json
- Supports all Codex-available models (gpt-4.1, gpt-4o, o1, o3, etc.)
- Automatic token expiry detection from JWT
- Clear setup instructions and troubleshooting docs

Usage:
  openclaw plugins enable openai-codex-auth
  openclaw models auth login --provider openai-codex --set-default
2026-02-17 00:01:00 +01:00
boris
f82a3d3e2b fix: use resolveUserPath utility for tilde expansion 2026-02-17 00:00:57 +01:00
boris
4cd75d5d0f fix: remove accidental openclaw link dependency 2026-02-17 00:00:57 +01:00
boris
f70b3a2e68 refactor: bundle export-html templates instead of reading from node_modules
- Copy templates from pi-coding-agent into src/auto-reply/reply/export-html/
- Add build script to copy templates to dist/
- Remove fragile node_modules path traversal
- Templates are now self-contained (~250KB total)
2026-02-17 00:00:57 +01:00
boris
1eb1a33f37 chore: remove --open option (not useful for remote sessions) 2026-02-17 00:00:57 +01:00
boris
ffe700bf94 fix: use proper pi-mono dark theme colors for export HTML 2026-02-17 00:00:57 +01:00
boris
add3afb743 feat: add /export-session command
Export current session to HTML file with full system prompt included.
Uses pi-coding-agent templates for consistent rendering.

Features:
- Exports session entries + full system prompt + tools
- Saves to workspace by default, or custom path
- Optional --open flag to open in browser
- Reuses pi-mono export-html templates

Usage:
  /export-session           # Export to workspace
  /export-session ~/export  # Export to custom path
  /export-session --open    # Export and open in browser
2026-02-17 00:00:57 +01:00
Xinhua Gu
3c3a39d165 fix(test): use path.resolve for cross-platform Windows compatibility 2026-02-17 00:00:54 +01:00
Xinhua Gu
90774c098a fix(sessions): allow cross-agent session file paths in multi-agent setups
When OPENCLAW_STATE_DIR changes between session creation and resolution
(e.g., after reinstall or config change), absolute session file paths
pointing to other agents' sessions directories were rejected even though
they structurally match the valid .../agents/<agentId>/sessions/... pattern.

The existing fallback logic in resolvePathWithinSessionsDir extracts the
agent ID from the path and tries to resolve it via the current env's
state directory. When those directories differ, the containment check
fails. Now, if the path structurally matches the agent sessions pattern
(validated by extractAgentIdFromAbsoluteSessionPath), we accept it
directly as a final fallback.

Fixes #15410, Fixes #15565, Fixes #15468
2026-02-17 00:00:54 +01:00
8BlT
e20b87f1ba fix: handle forum/topics in Telegram DM thread routing (#17980)
resolveTelegramThreadSpec now checks isForum in the non-group path.
DMs with forum/topics enabled return scope 'forum' so each topic
gets its own session, while plain DM threads keep scope 'dm'.
2026-02-17 00:00:51 +01:00
SK Akram
c25c276e00 refactor: remove unnecessary optional chaining from agent meta usage in reply and cron modules 2026-02-17 00:00:47 +01:00
SK Akram
d649069184 fix: add optional chaining to runResult.meta accesses to prevent crashes on aborted runs 2026-02-17 00:00:47 +01:00
Operative-001
690ec492df refactor: remove redundant field assignments in resolveCronSession
Addresses Greptile review comment: when !isNewSession, the spread already
copies all entry fields. The explicit entry?.field assignments were
redundant and could cause confusion. Simplified to only override the
core fields (sessionId, updatedAt, systemSent).
2026-02-17 00:00:40 +01:00
Operative-001
57c8f62396 fix(cron): reuse existing sessionId for webhook/cron sessions
When a webhook or cron job provides a stable sessionKey, the session
should maintain conversation history across invocations. Previously,
resolveCronSession always generated a new sessionId and hardcoded
isNewSession: true, preventing any conversation continuity.

Changes:
- Check if existing entry has a valid sessionId
- Evaluate freshness using configured reset policy
- Reuse sessionId and set isNewSession: false when fresh
- Add forceNew parameter to override reuse behavior
- Spread existing entry to preserve conversation context

This enables persistent, stateful conversations for webhook-driven
agent endpoints when allowRequestSessionKey is configured.

Fixes #18027
2026-02-17 00:00:40 +01:00
Clawdbot
952db1a3e2 fix(discord): route audioAsVoice payloads through voice message API
deliverDiscordReply now checks payload.audioAsVoice and routes through
sendVoiceMessageDiscord instead of sendMessageDiscord when true.

This matches the existing Telegram behavior where audioAsVoice triggers
the voice message path (wantsVoice: true).

Fixes #17990
2026-02-17 00:00:34 +01:00
Peter Steinberger
2fa9ddebdb fix(mattermost): add actions config typing 2026-02-16 23:00:32 +00:00
Peter Steinberger
9f0fc74d10 refactor(model): share normalized provider map lookups 2026-02-16 23:00:32 +00:00
Clawdbot
1fca7c3928 fix(discord): strip user:/discord:/pk: prefixes in command allowFrom
Discord's formatAllowFrom now strips these prefixes before matching,
aligning with normalizeDiscordAllowList behavior used in DM admission.

Before: commands.allowFrom: ["user:123"] → no match (senderCandidates: ["123", "discord:123"])
After: commands.allowFrom: ["user:123"] → "123" → matches sender "123"

Fixes #17937
2026-02-17 00:00:30 +01:00
Operative-001
6931ca7035 fix(subagent): route nested announce to parent even when parent run ended
When a depth-2 subagent (Birdie) completes and its parent (Newton) is a
depth-1 subagent, the announce should go to Newton, not bypass to the
grandparent (Jaris).

Previously, isSubagentSessionRunActive(Newton) returned false because
Newton's agent turn completed after spawning Birdie. This triggered the
fallback to grandparent even though Newton's SESSION was still alive and
waiting for child results.

Now we only fallback to grandparent if the parent SESSION is actually
deleted (no sessionId in session store). If the parent session exists,
we inject into it even if the current run has ended — this starts a new
agent turn to process the child result.

Fixes #18037

Test Plan:
- Added regression test: routes to parent when run ended but session alive
- Added regression test: falls back to grandparent only when session deleted
2026-02-17 00:00:27 +01:00
aether-ai-agent
235794d9f6 fix(security): OC-09 credential theft via environment variable injection
Implement comprehensive environment variable sanitization before Docker
container creation to prevent credential theft via post-exploitation
environment access.

Security Impact:
- Blocks 39+ sensitive credential patterns (API keys, tokens, passwords)
- Prevents exfiltration of ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
- Fail-secure validation with audit logging

Changes:
- Add sanitize-env-vars.ts with blocklist/allowlist validation
- Integrate sanitization into docker.ts (lines 273-294)
- Add validateEnvVars() to security validation
- Comprehensive test suite (62 tests, 100% pass rate)

Test Results: 62/62 passing
Code Review: 9.5/10 approved
Severity: HIGH (CWE-200, CVSS 7.5)

Signed-off-by: Aether AI Agent <github@tryaether.ai>
2026-02-17 00:00:23 +01:00
康熙
65a1787f92 fix: normalize paths to forward slashes for Windows RegExp compatibility
Windows path.relative() produces backslashes (e.g., memory\2026-02-16.md)
which fail to match RegExp patterns using forward slashes.

Normalize relative paths to forward slashes before RegExp matching
using rel.split(path.sep).join('/').

Fixes 4 test failures on Windows CI.
2026-02-17 00:00:20 +01:00
康熙
811c4f5e91 feat: add post-compaction read audit (Layer 3) 2026-02-17 00:00:20 +01:00
康熙
3296a25cc6 fix: format compaction-safeguard.ts with oxfmt 2026-02-17 00:00:20 +01:00
康熙
c4f829411f feat: append workspace critical rules to compaction summary
- Add readWorkspaceContextForSummary() to extract Session Startup + Red Lines from AGENTS.md
- Inject workspace context into compaction summary (limited to 2000 chars)
- Export extractSections() from post-compaction-context.ts for reuse
- Ensures compaction summary includes core rules needed for recovery

Part 1 of post-compaction context injection feature.
2026-02-17 00:00:20 +01:00
康熙
d0b33f23eb fix: improve section extraction robustness (case-insensitive, H3, code blocks) 2026-02-17 00:00:20 +01:00
康熙
90476d465d fix: format post-compaction-context test file 2026-02-17 00:00:20 +01:00
康熙
35a3e1b788 feat: inject post-compaction workspace context as system event (#18023) 2026-02-17 00:00:20 +01:00
artale
b1d5c71609 fix(cli): use standalone script for service restart after update (#17225)
The updater was previously attempting to restart the service using the
installed codebase, which could be in an inconsistent state during the
update process. This caused the service to stall when the updater
deleted its own files before the restart could complete.

Changes:
- restart-helper.ts: new module that writes a platform-specific restart
  script to os.tmpdir() before the update begins (Linux systemd, macOS
  launchctl, Windows schtasks).
- update-command.ts: prepares the restart script before installing, then
  uses it for service restart instead of the standard runDaemonRestart.
- restart-helper.test.ts: 12 tests covering all platforms, custom
  profiles, error cases, and shell injection safety.

Review feedback addressed:
- Use spawn(detached: true) + unref() so restart script survives parent
  process termination (Greptile).
- Shell-escape profile values using single-quote wrapping to prevent
  injection via OPENCLAW_PROFILE (Greptile).
- Reject unsafe batch characters on Windows.
- Self-cleanup: scripts delete themselves after execution (Copilot).
- Add tests for write failures and custom profiles (Copilot).

Fixes #17225
2026-02-17 00:00:16 +01:00
artale
a62ff19a66 fix(agent): isolate last-turn total in token usage reporting (#17016)
recordAssistantUsage accumulated cacheRead across the entire multi-turn
run, and totalTokens was clamped to contextTokens. This caused
session_status to report 100% context usage regardless of actual load.

Changes:
- run.ts: capture lastTurnTotal from the most recent model call and
  inject it into the normalized usage before it reaches agentMeta.
- usage-reporting.test.ts: verify usage.total reflects current turn,
  not accumulated total.

Fixes #17016
2026-02-17 00:00:12 +01:00
OpenClaw Bot
d6acd71576 fix: session-memory hook finds previous session file after /new/reset
When /new or /reset is triggered, the session file gets rotated
before the hook runs. The hook was reading the new (empty) file
instead of the previous session content.

This fix:
1. Checks if the session file looks like a reset file (.reset.)
2. Falls back to finding the most recent non-reset .jsonl file
3. Logs debug info about which file was used

Fixes openclaw/openclaw#18088
2026-02-17 00:00:08 +01:00
OpenClaw Bot
767109e7d5 fix(skills): improve git credential handling for gh-issues sub-agents
- Add explicit GH_TOKEN setup in sub-agent environment
- Disable credential helper before push
- Use GIT_ASKPASS to prevent credential prompts
2026-02-17 00:00:08 +01:00
OpenClaw Bot
068260bbea fix: add api-version query param for Azure verification 2026-02-17 00:00:08 +01:00
OpenClaw Bot
960cc11513 fix: add Azure AI Foundry URL support for custom providers
Detects Azure AI Foundry URLs (services.ai.azure.com and
openai.azure.com) and transforms them to include the proper
deployment path (/openai/deployments/<model-id>) required by
Azure's API. This fixes the 400 error when configuring OpenAI
models from Azure AI Foundry.

Fixes openclaw/openclaw#17992
2026-02-17 00:00:08 +01:00
Rain
4e5a9d83b7 fix(gateway): preserve unbracketed IPv6 host headers 2026-02-17 00:00:03 +01:00
Iron9521
8e55503d77 fix(browser): track original port mapping for EADDRINUSE fallback
Address review feedback: when port fallback occurs, maintain mapping from
original requested port to the relay server for proper cleanup and reuse.

- Add relayByOriginalPort map to track original port -> relay
- Update ensureChromeExtensionRelayServer to check both maps
- Update stopChromeExtensionRelayServer to clean up both mappings
- Stop function now uses the relay's actual bound port for auth cleanup
2026-02-16 23:59:59 +01:00
Iron
0e6daa2e6e fix(browser): handle EADDRINUSE with automatic port fallback
When the Chrome extension relay server fails to bind due to port
conflict (EADDRINUSE), automatically try alternative ports in the
dynamic range (49152-65535) instead of failing immediately.

This resolves issues where stale processes hold onto port 18792
after gateway restarts or crashes.

Fixes potential issues related to #8926, #13867, #17584
2026-02-16 23:59:59 +01:00
artale
a1a1f56841 fix(process): disable detached spawn on Windows to fix empty exec output (#18035)
The supervisor's child adapter always spawned with `detached: true`,
which creates a new process group. On Windows Scheduled Tasks (headless,
no console), this prevents stdout/stderr pipes from properly connecting,
causing all exec tool output to silently disappear.

The old exec path (pre-supervisor refactor) never used `detached: true`.
The regression was introduced in cd44a0d01 (refactor process spawning).

Changes:
- child.ts: set `detached: false` on Windows, keep `detached: true` on
  POSIX (where it's needed to survive parent exit). Skip the no-detach
  fallback on Windows since it's already the default.
- child.test.ts: platform-aware assertions for detached behavior.

Fixes #18035
Fixes #17806
2026-02-16 23:59:53 +01:00
Operative-001
d0a5ee0176 fix: include token drift warning in JSON response
Address review feedback - when --json mode is used, the drift warning
was completely suppressed. Now it's included in the warnings array
of the DaemonActionResponse so programmatic consumers can surface it.
2026-02-16 23:59:50 +01:00
Operative-001
d6e85aa6ba fix(daemon): warn on token drift during restart (#18018)
When the gateway token in config differs from the token embedded in the
service plist/unit file, restart will not apply the new token. This can
cause silent auth failures after OAuth token switches.

Changes:
- Add checkTokenDrift() to service-audit.ts
- Call it in runServiceRestart() before restarting
- Warn user with suggestion to run 'openclaw gateway install --force'

Closes #18018
2026-02-16 23:59:50 +01:00
Marcus Widing
8af4712c40 fix(cron): prevent spin loop when job completes within scheduled second (#17821)
When a cron job fires and completes within the same wall-clock second it
was scheduled for, the next-run computation could return undefined or the
same second, causing the scheduler to re-trigger the job hundreds of
times in a tight loop.

Two-layer fix:

1. computeJobNextRunAtMs: When computeNextRunAtMs returns undefined for a
   cron-kind schedule (edge case where floored nowSecondMs matches the
   schedule), retry with the ceiling (next second) as reference time.
   This ensures we always get the next valid occurrence.

2. applyJobResult: Add MIN_REFIRE_GAP_MS (2s) safety net for cron-kind
   jobs.  After a successful run, nextRunAtMs is guaranteed to be at
   least 2s in the future.  This breaks any remaining spin-loop edge
   cases without affecting normal daily/hourly schedules (where the
   natural next run is hours/days away).

Fixes #17821
2026-02-16 23:59:44 +01:00
popomore
eed806ce58 f 2026-02-16 23:59:41 +01:00
popomore
a42ccb9c1d f 2026-02-16 23:59:41 +01:00
popomore
c315246971 fix(feishu): fix mention detection for post messages with embedded docs
Parse "at" elements from post content when message.mentions is empty to
detect bot mentions in rich text messages containing documents.
2026-02-16 23:59:41 +01:00
Glucksberg
cd4f7524e3 feat(telegram): receive and surface user message reactions (#10075) 2026-02-16 23:59:36 +01:00
Rain
d3698f4eb6 fix(gateway): trim trusted proxy entries before matching 2026-02-16 23:59:32 +01:00
HAL
e24e465c00 fix(webchat): strip reply/audio directive tags before rendering #18079
The webchat UI rendered [[reply_to_current]], [[reply_to:<id>]], and
[[audio_as_voice]] tags as literal text because extractText() passed
assistant content through without stripping inline directives.

Add stripDirectiveTags() to the UI chat layer and apply it to all three
extractText code paths (string content, content array, .text property)
for assistant messages only. Regex mirrors src/utils/directive-tags.ts.

Fixes #18079
2026-02-16 23:59:29 +01:00
AKASH KOBAL
9c3eed5970 Update Akash Kobal's avatar link in README 2026-02-16 23:59:25 +01:00
AKASH KOBAL
18f3bbfe05 Add avatar link for AkashKobal to README 2026-02-16 23:59:25 +01:00
Vishal Doshi
e91a5b0216 fix: release stale session locks and add watchdog for hung API calls (#18060)
When a model API call hangs indefinitely (e.g. Anthropic quota exceeded
mid-call), the gateway acquires a session .jsonl.lock but the promise
never resolves, so the try/finally block never reaches release(). Since
the owning PID is the gateway itself, stale detection cannot help —
isPidAlive() always returns true.

This commit adds four layers of defense:

1. **In-process lock watchdog** (session-write-lock.ts)
   - Track acquiredAt timestamp on each held lock
   - 60-second interval timer checks all held locks
   - Auto-releases any lock held longer than maxHoldMs (default 5 min)
   - Catches the hung-API-call case that try/finally cannot

2. **Gateway startup cleanup** (server-startup.ts)
   - On boot, scan all agent session directories for *.jsonl.lock files
   - Remove locks with dead PIDs or older than staleMs (30 min)
   - Log each cleaned lock for diagnostics

3. **openclaw doctor stale lock detection** (doctor-session-locks.ts)
   - New health check scans for .jsonl.lock files
   - Reports PID status and age of each lock found
   - In --fix mode, removes stale locks automatically

4. **Transcript error entry on API failure** (attempt.ts)
   - When promptError is set, write an error marker to the session
     transcript before releasing the lock
   - Preserves conversation history even on model API failures

Closes #18060
2026-02-16 23:59:22 +01:00
Rodrigo Uroz
7d8d8c338b config: align memory hybrid UI metadata with schema labels/help 2026-02-16 23:59:19 +01:00
Rodrigo Uroz
65ad9a4262 Memory: fix MMR tie-break and temporal timestamp dedupe 2026-02-16 23:59:19 +01:00
Rodrigo Uroz
33cf27a52a fix: MMR default disabled, tie-break null guard, correct docs URL
- DEFAULT_MMR_CONFIG.enabled = false (opt-in, was incorrectly true)
- Tie-break: handle bestItem === null so first candidate always wins
- CHANGELOG URL: docs.clawd.bot → docs.openclaw.ai
- Tests updated to pass enabled: true explicitly where needed
2026-02-16 23:59:19 +01:00
Rodrigo Uroz
6b3e0710f4 feat(memory): Add opt-in temporal decay for hybrid search scoring
Exponential decay (half-life configurable, default 30 days) applied
before MMR re-ranking. Dated daily files (memory/YYYY-MM-DD.md) use
filename date; evergreen files (MEMORY.md, topic files) are not
decayed; other sources fall back to file mtime.

Config: memorySearch.query.hybrid.temporalDecay.{enabled, halfLifeDays}
Default: disabled (backwards compatible, opt-in).
2026-02-16 23:59:19 +01:00
Rodrigo Uroz
fa9420069a feat(memory): Add MMR re-ranking for search result diversity
Adds Maximal Marginal Relevance (MMR) re-ranking to hybrid search results.

- New mmr.ts with tokenization, Jaccard similarity, and MMR algorithm
- Integrated into mergeHybridResults() with optional mmr config
- 40 comprehensive tests covering edge cases and diversity behavior
- Configurable lambda parameter (default 0.7) to balance relevance vs diversity
- Updated CHANGELOG.md and memory docs

This helps avoid redundant results when multiple chunks contain similar content.
2026-02-16 23:59:19 +01:00
Rain
a0ab301dc3 Fix Discord auto-thread attempting to thread in Forum/Media channels\n\nCreating threads on messages within Forum/Media channels is often redundant\nor invalid (as messages are already posts). This prevents API errors and spam.\n\nFix: Check channel type before attempting auto-thread creation. 2026-02-16 23:59:16 +01:00
Rain
b90d7625e5 Fix Discord session routing continuity (enable lastRoute for groups)\n\nPreviously, 'updateLastRoute' was only enabled for Direct Messages.\nThis meant that group/channel sessions did not update their routing\nmetadata (last channel/to/accountId) in 'session-meta.json'.\n\nIf the bot restarted or a proactive cron job tried to send a message\nto a group session using 'sessions_send' without an explicit 'to' field,\nit would fail because 'lastRoute' was missing or stale.\n\nFix: Enable 'updateLastRoute' for all Discord messages (Group + DM),\nensuring the session store always has the latest valid routing target. 2026-02-16 23:59:16 +01:00
Rob Dunn
dbe2ab6f62 cron: keep usage telemetry in run log types + error paths 2026-02-16 23:58:38 +01:00
Rob Dunn
ddea5458d0 cron: log model+token usage per run + add usage report script 2026-02-16 23:58:38 +01:00
tian Xiao
edbc68e9f1 feat: support Z.AI tool_stream for real-time tool call streaming
Add support for Z.AI's native tool_stream parameter to enable real-time
visibility into model reasoning and tool call execution.

- Automatically inject tool_stream=true for zai/z-ai providers
- Allow disabling via params.tool_stream: false in model config
- Follows existing pattern of OpenRouter and OpenAI wrappers

This enables Z.AI API features described in:
https://docs.z.ai/api-reference#streaming

AI-assisted: Claude (OpenClaw agent) helped write this implementation.
Testing: lightly tested (code review + pattern matching existing wrappers)

Closes #18135
2026-02-16 23:58:35 +01:00
ranausmanai
c529e6005a fix(gateway): set explicit chat timeouts for mesh gateway calls 2026-02-16 23:58:23 +01:00
ranausmanai
16e59b26a6 Add mesh auto-planning with chat command UX and hardened auth/session behavior 2026-02-16 23:58:23 +01:00
ranausmanai
83990ed542 Add mesh orchestration gateway methods with DAG execution and retry 2026-02-16 23:58:23 +01:00
Parker Todd Brooks
15fe87e6b7 feat: add before_message_write plugin hook
Synchronous hook that lets plugins inspect and optionally block messages
before they are written to the session JSONL file. Primary use case is
private mode... when enabled, the plugin returns { block: true } and the
message never gets persisted.

The hook runs on the hot path (synchronous, like tool_result_persist).
Handlers execute sequentially in priority order. If any handler returns
{ block: true }, the write is skipped immediately. Handlers can also
return a modified message to write instead of the original.

Changes:
- src/plugins/types.ts: add hook name, event/result types, handler map entry
- src/plugins/hooks.ts: add runBeforeMessageWrite() following tool_result_persist pattern
- src/agents/session-tool-result-guard.ts: invoke hook before every originalAppend() call
- src/agents/session-tool-result-guard-wrapper.ts: wire hook runner to the guard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:58:12 +01:00
Winston
94eecaa446 fix: atomic session store writes to prevent context loss on Windows
On Windows, fs.promises.writeFile truncates the target file to 0 bytes
before writing. Since loadSessionStore reads the file synchronously
without holding the write lock, a concurrent read can observe the empty
file, fail to parse it, and fall through to an empty store — causing the
agent to lose its session context.

Changes:
- saveSessionStoreUnlocked (Windows path): write to a temp file first,
  then rename it onto the target. If rename fails due to file locking,
  retry 3 times with backoff, then fall back to copyFile (which
  overwrites in-place without truncating to 0 bytes).
- loadSessionStore: on Windows, retry up to 3 times with 50ms
  synchronous backoff (via Atomics.wait) when the file is empty or
  unparseable, giving the writer time to finish. SharedArrayBuffer is
  allocated once and reused across retry attempts.
2026-02-16 23:57:21 +01:00
Rain
1bef2fc68b fix(whatsapp): allow per-message link preview override\n\nWhatsApp messages default to enabling link previews for URLs. This adds\nsupport for overriding this behavior per-message via the \nparameter (e.g. from tool options), consistent with Telegram.\n\nFix: Updated internal WhatsApp Web API layers to pass option\ndown to Baileys . 2026-02-16 23:57:09 +01:00
misterdas
312a7f7880 fix: make tool exit code handling less aggressive
Treat normal process exits (even with non-zero codes) as completed tool results.
This prevents standard exit codes (like grep exit 1) from being surfaced
as 'Tool Failure' warnings in the UI. The exit code is still appended
to the tool output for assistant awareness.
2026-02-16 23:56:56 +01:00
Buddy (AI)
91903bac15 fix: include OPENCLAW_SERVICE_VERSION in system presence version detection
The gateway's system-presence.ts was not detecting the version when
OpenClaw is run as a launchd service, because the daemon-runtime.ts
sets OPENCLAW_SERVICE_VERSION but system-presence.ts only checked
OPENCLAW_VERSION and npm_package_version.

This caused 'openclaw status' to show 'unknown' for the version.

Issue: #18456

🤖 AI-assisted (lightly tested)
2026-02-16 23:56:10 +01:00
Rick Qian
5d9a026a9e gateway: hard-cap chat.history oversized payloads 2026-02-16 23:56:05 +01:00
Peter Steinberger
97e0f8d551 fix(onboarding): keep wildcard allowFrom helper string-typed 2026-02-16 22:55:59 +00:00
Peter Steinberger
64f5e4a424 refactor(onboarding): reuse allowlist merge across channels 2026-02-16 22:55:59 +00:00
Peter Steinberger
486b7379d4 refactor(test): dedupe doctor harness mock payload factories 2026-02-16 22:55:59 +00:00
Peter Steinberger
230e1d9962 refactor(auth): share profile id dedupe helper 2026-02-16 22:55:59 +00:00
Peter Steinberger
ff7a735115 refactor(onboarding): share allowlist merge helpers 2026-02-16 22:55:59 +00:00
Echo
1dfacd4dd1 fix(status): avoid bot+app token warning for mattermost 2026-02-16 23:55:56 +01:00
Echo
82861968c2 fix(mattermost): address review feedback on reactions PR 2026-02-16 23:55:40 +01:00
Echo
2a2372cd6c feat(mattermost): add emoji reactions support 2026-02-16 23:55:40 +01:00
Tom Peri
b57d29d833 fix(slack): extract text and media from forwarded message attachments 2026-02-16 23:55:34 +01:00
SK Heavy Industries
4928717b92 fix: handle Qwen 3 reasoning field in Ollama responses
Qwen 3 (and potentially other reasoning-capable models served via Ollama)
returns its final answer in a `reasoning` field with an empty `content`
field. This causes blank/empty responses since OpenClaw only reads `content`.

Changes:
- Add `reasoning?` to OllamaChatResponse message type
- Fall back to `reasoning` when `content` is empty in buildAssistantMessage
- Accumulate `reasoning` chunks during streaming when `content` is empty

This allows Qwen 3 to work correctly both with and without /no_think mode.
2026-02-16 23:55:31 +01:00
Ty Sabs
46bf210e04 fix: always drop orphaned OpenAI reasoning blocks in session history
downgradeOpenAIReasoningBlocks was only called on model change, but
orphaned reasoning items (e.g. from an aborted stream) can exist without
a model switch and cause a 400 from the OpenAI Responses API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:55:28 +01:00
Usama Saqib, Ph.D.
e33017982c Update README.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-16 23:55:02 +01:00
Usama Saqib, Ph.D.
e759b4cd58 Update README.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-16 23:55:02 +01:00
Usama Saqib, Ph.D.
572cfb7a53 Update README.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-16 23:55:02 +01:00
Usama Saqib, Ph.D.
57aef596b4 Revise Android installation link in README
Updated Android installation instructions in README.
2026-02-16 23:55:02 +01:00
Krish
0a02b91638 Handle Telegram poll vote updates for agent context 2026-02-16 23:54:56 +01:00
Krish
5cbfaf5cc7 Add Telegram polls action to config typing 2026-02-16 23:54:56 +01:00
Krish
b2fe44b1ee Fix lint in telegram poll action handler 2026-02-16 23:54:56 +01:00
Krish
c43e95e011 Default Telegram polls to public 2026-02-16 23:54:56 +01:00
Krish
556b531a14 Fix Telegram poll action wiring 2026-02-16 23:54:56 +01:00
Mitsuyuki Osabe
afd354c482 fix: add catalog validation to models set command
`models set` accepts any syntactically valid model ID without checking
the catalog, allowing typos to silently persist in config and fail at
runtime. It also unconditionally adds an empty `{}` entry to
`agents.defaults.models`, bypassing any provider routing constraints.

This commit:
- Validates the model ID against the catalog (skipped when catalog is
  empty during initial setup)
- Warns when a new entry is added with empty config (no provider routing)

Closes openclaw/openclaw#17183

✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)
2026-02-16 23:54:52 +01:00
Rami Abdelrazzaq
0b8b95f2c9 fix(update): prevent gateway crash loop after failed self-update
The gateway unconditionally scheduled a SIGUSR1 restart after every
update.run call, even when the update itself failed (broken deps,
build errors, etc.). This left the process restarting into a broken
state — corrupted node_modules, partial builds — causing a crash loop
that required manual intervention.

Three fixes:

1. Only restart on success: scheduleGatewaySigusr1Restart is now
   gated on result.status === "ok". Failed or skipped updates still
   write the restart sentinel (so the status can be reported back to
   the user) but the running gateway stays alive.

2. Early bail on step failure: deps install, build, and ui:build now
   check exit codes immediately (matching the preflight section) so a
   failed deps install no longer cascades into a broken build and
   ui:build.

3. Auto-repair config during update: the doctor step now runs with
   --fix alongside --non-interactive, so unknown config keys left over
   from schema changes between versions are stripped automatically
   instead of causing a startup validation crash.
2026-02-16 23:54:49 +01:00
wu-tian807
671f913123 feat: support per-model thinkingDefault override in models config
The global `agents.defaults.thinkingDefault` forces a single thinking
level for all models.  Users running multiple models with different
reasoning capabilities (e.g. Claude with extended thinking, GPT-4o
without, Gemini Flash with lightweight reasoning) cannot optimise the
thinking level per model.

Add an optional `thinkingDefault` field to `AgentModelEntryConfig` so
each entry under `agents.defaults.models` can declare its own default.
Resolution priority: per-model → global → catalog auto-detect.

Example config:

    "models": {
      "anthropic/claude-sonnet-4-20250514": { "thinkingDefault": "high" },
      "openai/gpt-4o":                      { "thinkingDefault": "off" }
    }

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 23:54:45 +01:00
Ocean Vael
e368c36503 feat: add llms.txt discovery as default agent behavior
Add automatic llms.txt awareness so agents check for /llms.txt or
/.well-known/llms.txt when exploring new domains.

Changes:
- System prompt: new 'llms.txt Discovery' section (full mode only,
  when web_fetch is available) instructing agents to check for llms.txt
  files when visiting new domains
- web_fetch tool: updated description to mention llms.txt discovery

llms.txt is an emerging standard (like robots.txt for AI) that helps
site owners describe how AI agents should interact with their content.
Making this a default behavior helps the ecosystem adopt agent-native
web experiences.

Ref: https://llmstxt.org
2026-02-16 23:54:40 +01:00
artale
4df970d711 fix: improve error for unconfigured local providers (ollama/vllm) (#17328)
When a user sets `agents.defaults.model.primary: "ollama/gemma3:4b"`
but forgets to set OLLAMA_API_KEY, the error is a confusing
"unknown model: ollama/gemma3:4b". The Ollama provider requires any
dummy API key to register (the local server doesn't actually check it),
but this isn't obvious from the error.

Add `buildUnknownModelError()` that detects known local providers
(ollama, vllm) and appends an actionable hint with the env var name
and a link to the relevant docs page.

Before: Unknown model: ollama/gemma3:4b
After:  Unknown model: ollama/gemma3:4b. Ollama requires authentication
        to be registered as a provider. Set OLLAMA_API_KEY="ollama-local"
        (any value works) or run "openclaw configure".
        See: https://docs.openclaw.ai/providers/ollama

Closes #17328
2026-02-16 23:54:31 +01:00
OpenClaw Bot
6e1edc7d62 fix: correct Sparkle appcast version for 2026.2.15
The sparkle:version was incorrectly set to '11213' instead of '202602150',
causing the macOS app to not detect the 2026.2.15 update. Sparkle compares
versions as strings, so '11213' < '202602140' (2026.2.14's version), preventing
the update from being offered to users.

Fixes openclaw/openclaw#18178
2026-02-16 23:54:23 +01:00
OpenClaw Bot
b2d622cfa3 fix: clear stale device-auth token on token mismatch
When the gateway connection fails due to device token mismatch (e.g., after
re-pairing the device), clear the stored device-auth token so that
subsequent connection attempts can obtain a fresh token.

This fixes the cron tool failing with 'device token mismatch' error
after running 'openclaw configure' to re-pair the device.

Fixes #18175
2026-02-16 23:54:23 +01:00
Mahsum Aktas
0ee3480690 fix(cron): preserve model fallbacks when agent overrides primary
When an agent config specifies `model: { primary: "..." }` without
an explicit `fallbacks` array, the existing code replaced the entire
model object from `agents.defaults`—discarding the default fallbacks.

This caused cron jobs (and agent sessions) to have only one model
candidate (the pinned model) plus the global primary as a final
fallback, skipping all intermediate fallback models.

The fix merges the agent model override into the existing defaults
model object using spread, so that keys like `fallbacks` survive
when the agent only overrides `primary`. Agents can still explicitly
override or clear fallbacks by providing their own `fallbacks` array.

Reproduction scenario:
- `agents.defaults.model = { primary: "codex", fallbacks: ["opus", "flash", "deepseek"] }`
- Agent config: `model: { primary: "codex" }`
- Cron job pins: `model: "flash"`
- Before fix: fallback candidates = [flash, codex] (3 models lost)
- After fix: fallback candidates = [flash, opus, deepseek, ..., codex]
2026-02-16 23:54:17 +01:00
Joshua Mitchell
5a3a448bc4 feat(commands): add /subagents spawn command
Add a `spawn` action to the /subagents command handler that invokes
spawnSubagentDirect() to deterministically launch a named subagent.

Usage: /subagents spawn <agentId> <task> [--model <model>] [--thinking <level>]

Also includes the shared subagent-spawn module extraction (same as the
refactor/extract-shared-subagent-spawn branch) since it hasn't merged yet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:54:14 +01:00
Saurabh.Chopade
bb5ce3b02f CLI: preserve message send components payload 2026-02-16 23:54:08 +01:00
Sriram Naidu Thota
63fb998074 fix: address code review feedback
- Use stricter regex: /^[A-Za-z0-9+/]*={0,2}$/ ensures = only at end
- Normalize URL-safe base64 to standard (- → +, _ → /)
- Added tests for padding in wrong position and URL-safe normalization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 23:53:54 +01:00
Sriram Naidu Thota
38c96bc53e fix: validate base64 image data before API submission
Adds explicit base64 format validation in sanitizeContentBlocksImages()
to prevent invalid image data from being sent to the Anthropic API.

The Problem:
- Node's Buffer.from(str, "base64") silently ignores invalid characters
- Invalid base64 passes local validation but fails at Anthropic's stricter API
- Once corrupted data persists in session history, every API call fails

The Fix:
- Add validateAndNormalizeBase64() function that:
  - Strips data URL prefixes (e.g., "data:image/png;base64,...")
  - Validates base64 character set with regex
  - Checks for valid padding (0-2 '=' chars)
  - Validates length is proper for base64 encoding
- Invalid images are replaced with descriptive text blocks
- Prevents permanent session corruption

Tests:
- Rejects invalid base64 characters
- Strips data URL prefixes correctly
- Rejects invalid padding
- Rejects invalid length
- Handles empty data gracefully

Closes #18212

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 23:53:54 +01:00
yinghaosang
aeec95f870 fix(gateway): include deliveryContext in update.run restart sentinel (#18239) 2026-02-16 23:53:50 +01:00
Ignacio
d43c11c76d test: update tests and comments to reflect new autoSelectFamily default
- Update test expectation: 'defaults to enable on Node 22'
- Update comment in fetch.ts to explain IPv4 fallback rationale
- Addresses greptile review feedback
2026-02-16 23:53:44 +01:00
Ignacio
c762bf71f6 fix(telegram): enable autoSelectFamily by default for Node.js 22+
Fixes issue where Telegram fails to send messages when IPv6 is configured
but not functional on the network.

Problem:
- Many networks (especially in Latin America) have IPv6 configured but
  not properly routed by ISP/router
- Node.js tries IPv6 first, gets 'Network is unreachable' error
- With autoSelectFamily=false, Node doesn't fallback to IPv4
- Result: All Telegram API calls fail

Solution:
- Change default from false to true for Node.js 22+
- This enables automatic IPv4 fallback when IPv6 fails
- Config option channels.telegram.network.autoSelectFamily still available
  for users who need to override

Symptoms fixed:
- Health check: Telegram | WARN | failed (unknown) - fetch failed
- Logs: Network request for 'sendMessage' failed
- Bot receives messages but cannot send replies

Tested on:
- macOS 26.2 (Sequoia)
- Node.js v22.15.0
- OpenClaw 2026.2.12
- Network with IPv6 configured but not routed
2026-02-16 23:53:44 +01:00
Yao
3ec936d1b4 fix(daemon): prefer current node and add macOS version manager paths to service PATH 2026-02-16 23:53:41 +01:00
Yao
1a8548df18 fix(daemon): prefer current node (process.execPath) and add macOS version manager paths to service PATH
On macOS, `openclaw gateway install` hardcodes the system node
(/opt/homebrew/bin/node) in the launchd plist, ignoring the node from
version managers (fnm/nvm/volta). This causes the Gateway to run a
different node version than the user's shell environment.

Two fixes:

1. `resolvePreferredNodePath` now checks `process.execPath` first.
   If the currently running node is a supported version, use it directly.
   This respects the user's active version manager selection.

2. `buildMinimalServicePath` now includes version manager bin directories
   on macOS (fnm, nvm, volta, pnpm, bun), matching the existing Linux
   behavior.

Fixes #18090
Related: #6061, #6064
2026-02-16 23:53:41 +01:00
David Szarzynski
59eac34c2b changelog: add channel health monitor entry 2026-02-16 23:53:35 +01:00
David Szarzynski
30ee12e40a gateway: wire channel health monitor into startup with configurable interval 2026-02-16 23:53:35 +01:00
David Szarzynski
497e2d76ad feat(gateway): add channel health monitor with auto-restart 2026-02-16 23:53:35 +01:00
David Szarzynski
68489a213f gateway: expose isManuallyStopped and resetRestartAttempts on ChannelManager 2026-02-16 23:53:35 +01:00
Xinhua Gu
ae0b110e44 fix(security): set 0o600 on remaining session file write paths
Follow-up to #18066 — three session file write sites were missed:

- auto-reply/reply/session.ts: forked session transcript header
- pi-embedded-runner/session-manager-init.ts: session file reset
- gateway/server-methods/sessions.ts: compacted transcript rewrite

All now use mode 0o600 consistent with transcript.ts and chat.ts.
2026-02-16 23:53:28 +01:00
Artemii
d4c057f8c1 feat(inbound-meta): expose sender_id in trusted system metadata
Add sender_id (ctx.SenderId) to the openclaw.inbound_meta.v1 payload
so agents can reference it for moderation actions (delete, ban, etc.)
without relying on user-controlled text fields.

message_id and chat_id were already present; sender_id was the missing
piece needed for complete group moderation workflows.
2026-02-16 23:53:24 +01:00
康熙
bcab2469de feat: LLM-based query expansion for FTS mode
When searching in FTS-only mode (no embedding provider), extract meaningful
keywords from conversational queries using LLM to improve search results.

Changes:
- New query-expansion module with keyword extraction
- Supports English and Chinese stop word filtering
- Null safety guards for FTS-only mode (provider can be null)
- Lint compliance fixes for string iteration

This helps users find relevant memory entries even with vague queries.
2026-02-16 23:53:21 +01:00
康熙
65aedac20e fix: enable FTS fallback when no embedding provider available (#17725)
When no embedding provider is available (e.g., OAuth mode without API keys),
memory_search now falls back to FTS-only mode instead of returning disabled: true.

Changes:
- embeddings.ts: return null provider with reason instead of throwing
- manager.ts: handle null provider, use FTS-only search mode
- manager-search.ts: allow searching all models when provider is undefined
- memory-tool.ts: expose search mode in results

The search results now include a 'mode' field indicating 'hybrid' or 'fts-only'.
2026-02-16 23:53:21 +01:00
康熙
153794080e fix: support OAuth for Gemini media understanding
Extract parseGeminiAuth() to shared infra module and use it in both
embeddings-gemini.ts and inline-data.ts.

Previously, inline-data.ts directly set x-goog-api-key header without
handling OAuth JSON format. Now it properly supports both traditional
API keys and OAuth tokens.
2026-02-16 23:53:21 +01:00
康熙
3379b9d341 fix: support OAuth for Gemini embedding API
Add parseGeminiAuth() to detect OAuth JSON format ({"token": "...", "projectId": "..."})
and use Bearer token authentication instead of x-goog-api-key header.

This allows OAuth users (using gemini-cli-auth extension) to use memory_search
with Gemini embedding API.
2026-02-16 23:53:21 +01:00
yinghaosang
d24340d75b channels: migrate extension account listing to factory 2026-02-16 23:53:19 +01:00
yinghaosang
59384001ad channels: migrate core channel account listing to factory 2026-02-16 23:53:19 +01:00
yinghaosang
5544ab820c channels: add createAccountListHelpers factory 2026-02-16 23:53:19 +01:00
Knox
9aa8db5c81 fix(doctor,configure): skip gateway auth for loopback-only setups 2026-02-16 23:53:11 +01:00
yinghaosang
6757a9fedc fix(telegram): clean up update offset on channels remove --delete (#18233) 2026-02-16 23:53:06 +01:00
George McCain
b91e43714b feat(linq): add interactive onboarding adapter
Walk users through Linq setup via `openclaw channels add` wizard
instead of requiring manual JSON config editing. Prompts for API
token, phone number, and webhook config with sensible defaults.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:56 +01:00
George McCain
1d81cc4f1f feat(linq): add read receipts, typing indicators, and User-Agent header
Send read receipt and typing indicator immediately on inbound messages
for a more natural iMessage experience. Add User-Agent header to all
Linq API requests. Fix delivery payload to use .text instead of .body.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:56 +01:00
George McCain
60bd154e5a fix: parse webhook URL pathname instead of raw string match
Fixes incorrect path matching that would reject valid webhooks with
querystrings and match unintended prefixes like /linq-webhookX.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:56 +01:00
George McCain
d4a142fd8f feat: add Linq channel — real iMessage via API, no Mac required
Adds a complete Linq iMessage channel adapter that replaces the existing
iMessage channel's Mac Mini + dedicated Apple ID + SSH wrapper + Full Disk
Access setup with a single API key and phone number.

Core implementation (src/linq/):
- types.ts: Linq webhook event and message types
- accounts.ts: Multi-account resolution from config (env/file/inline token)
- send.ts: REST outbound via Linq Blue V3 API (messages, typing, reactions)
- probe.ts: Health check via GET /v3/phonenumbers
- monitor.ts: Webhook HTTP server with HMAC-SHA256 signature verification,
  replay protection, inbound debouncing, and full dispatch pipeline integration

Extension plugin (extensions/linq/):
- ChannelPlugin implementation with config, security, setup, outbound,
  gateway, and status adapters
- Supports direct and group chats, reactions, and media

Wiring:
- Channel registry, dock, config schema, plugin-sdk exports, and plugin
  runtime all updated to include the new linq channel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:56 +01:00
JayMishra-github
95024d1671 fix: log error on auto-end failure instead of swallowing
Address review feedback: log a warning when endCall fails on stream
disconnect instead of silently discarding the error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:51 +01:00
JayMishra-github
4c0a741308 fix: apply oxfmt 0.32.0 formatting (match CI version)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:51 +01:00
JayMishra-github
d56c04a3b5 fix: apply oxfmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:51 +01:00
JayMishra-github
3eec5e54b1 fix(voice-call): auto-end call when media stream disconnects
When a Twilio media stream disconnects (e.g., caller hangs up or
network drops), the call object was left in an active state indefinitely.
This caused "stuck calls" that consumed resources and blocked new calls.

Now calls are automatically ended when their media stream closes,
matching the expected lifecycle behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:51 +01:00
JayMishra-github
a5c94b8e7b fix: log error on reaper endCall failure instead of swallowing
Address review feedback: log a warning when the stale call reaper
fails to end a call instead of silently discarding the error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:48 +01:00
JayMishra-github
390c503b56 feat(voice-call): add configurable stale call reaper
Adds a periodic reaper that automatically ends calls older than a
configurable threshold. This catches calls stuck in unexpected states,
such as notify-mode calls that never receive a terminal webhook from
the provider.

New config option:
  staleCallReaperSeconds: number (default: 0 = disabled)

When enabled, checks every 30 seconds and ends calls exceeding the
max age. Recommended value: 120-300 for production deployments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:48 +01:00
JayMishra-github
47f8c9209f test: add tests for extraArgs filtering logic
Address review feedback: add tests covering empty strings,
non-strings, mixed arrays, and non-array inputs for extraArgs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:42 +01:00
JayMishra-github
cc3c25e413 fix: apply oxfmt 0.32.0 formatting (match CI version)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:42 +01:00
JayMishra-github
2977f7325d fix: add extraArgs to sandbox browser config and apply oxfmt formatting
Add the missing extraArgs property to buildSandboxBrowserResolvedConfig
to satisfy the ResolvedBrowserConfig type, and fix import ordering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:42 +01:00
JayMishra-github
039fc1e04c feat(browser): add extraArgs config for custom Chrome launch arguments
Adds a `browser.extraArgs` config option (string array) that is appended
to Chrome's launch arguments. This enables users to add stealth flags,
window size overrides, custom user-agent strings, or other Chrome flags
without patching the source code.

Example config:
  browser.extraArgs: ["--window-size=1920,1080", "--disable-infobars"]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:42 +01:00
Marcus Widing
de900bace8 fix: reset announceRetryCount in replaceSubagentRunAfterSteer
Address review feedback: the spread operator carries stale retry state
into replacement runs, potentially causing immediate force-expiration
without ever attempting announce delivery.
2026-02-16 23:52:39 +01:00
Marcus Widing
a6c741eb46 fix(announce): break infinite retry loop with max attempts and expiry (#18264)
When runSubagentAnnounceFlow returns false (deferred), finalizeSubagentCleanup
resets cleanupHandled=false and removes from resumedRuns, allowing
retryDeferredCompletedAnnounces to pick it up again. If the underlying
condition persists (stale registry data, transient state), this creates an
infinite loop delivering 100+ announces over hours.

Fix:
- Add announceRetryCount + lastAnnounceRetryAt to SubagentRunRecord
- finalizeSubagentCleanup: after MAX_ANNOUNCE_RETRY_COUNT (3) failed attempts
  or ANNOUNCE_EXPIRY_MS (5 min) since endedAt, mark as completed and stop
- resumeSubagentRun: skip entries that have exhausted retries or expired
- retryDeferredCompletedAnnounces: force-expire stale entries
2026-02-16 23:52:39 +01:00
JayMishra-github
0764999e2c fix: document intentional non-persistence of initialMessage deletion
Address review feedback: the in-memory deletion of initialMessage is
not persisted to disk, which is acceptable because a gateway restart
would also sever the media stream, making replay impossible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:36 +01:00
JayMishra-github
0291ce30a8 fix: apply oxfmt 0.32.0 formatting (match CI version)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:36 +01:00
JayMishra-github
dd319d05d8 fix: apply oxfmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:36 +01:00
JayMishra-github
2c6db57554 feat(voice-call): pre-cache inbound greeting for instant playback
Pre-generates TTS audio for the configured inboundGreeting at startup
and serves it instantly when an inbound call connects, eliminating the
500ms+ TTS synthesis delay on the first ring.

Changes:
- twilio.ts: Add cachedGreetingAudio storage with getter/setter
- runtime.ts: Pre-synthesize greeting TTS after provider initialization
- webhook.ts: Play cached audio directly via media stream on inbound
  connect, falling back to the original TTS path for outbound calls
  or when no cached audio is available

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:36 +01:00
JayMishra-github
27a4868c2d fix: move Chromium install after pnpm install and use playwright-core/cli.js
Address review feedback:
- Move the OPENCLAW_INSTALL_BROWSER block after pnpm install so
  playwright-core is available in node_modules
- Use node /app/node_modules/playwright-core/cli.js instead of
  npx playwright to avoid npm override conflicts in Docker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:30 +01:00
JayMishra-github
d6aa9adec5 feat(docker): add optional Chromium + Xvfb install in Docker image
Adds a build arg OPENCLAW_INSTALL_BROWSER that, when set, pre-installs
Chromium (via Playwright) and Xvfb into the Docker image. This eliminates
the 60-90 second Playwright install that otherwise happens on every
container start when browser features are used.

Usage:
  docker build --build-arg OPENCLAW_INSTALL_BROWSER=1 -t openclaw:browser .

Without the build arg, behavior is unchanged (no Chromium in image).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:30 +01:00
JayMishra-github
8b14052ebe fix: capture init script exit codes instead of swallowing via pipe
Address review feedback: the pipe to sed swallowed the script's exit
code. Now capture output in a variable and check exit status separately
so failures are logged as warnings in the entrypoint output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:26 +01:00
JayMishra-github
53af9f7437 feat(docker): add init script support via /openclaw-init.d/
Adds an ENTRYPOINT script that runs user-provided init scripts from
/openclaw-init.d/ before starting the gateway. This is the standard
Docker pattern (used by nginx, postgres, etc.) for customizing container
startup without overriding the entire entrypoint.

Usage:
  docker run -v ./my-init-scripts:/openclaw-init.d:ro openclaw

Scripts must be executable. Non-executable files are skipped with a
warning. Scripts run in alphabetical order with output prefixed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:26 +01:00
JayMishra-github
717caa97fb fix: remove stderr suppression so install failures are visible in build logs
Address review feedback: remove 2>/dev/null so that if the LanceDB
native binary download fails, the error is visible in Docker build
logs for debugging rather than silently swallowed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:23 +01:00
JayMishra-github
2ab6313d99 fix(docker): ensure memory-lancedb deps installed in Docker image
The memory-lancedb extension declares openai and @lancedb/lancedb as
dependencies, but these may not be available at runtime due to pnpm
hoisting behavior with native bindings. This adds an explicit install
step after the build to ensure the extension's dependencies are present.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:52:23 +01:00
Zaf (via OpenClaw)
34b18ea9db fix: respect OPENCLAW_HOME for isolated gateway instances
When OPENCLAW_HOME is set (indicating an isolated instance), the gateway
port should be read from config rather than inheriting OPENCLAW_GATEWAY_PORT
from a parent process. This fixes running multiple OpenClaw instances
where a child process would incorrectly use the parent's port.

Changes:
- resolveGatewayPort() now prioritizes config.gateway.port when OPENCLAW_HOME is set
- Added getConfigPath() function for runtime-evaluated config path
- Deprecated CONFIG_PATH constant with warning about module-load-time evaluation
- Updated gateway run command to use getConfigPath() instead of CONFIG_PATH

Fixes the issue where spawning a sandbox OpenClaw instance from within
another OpenClaw process would fail because OPENCLAW_GATEWAY_PORT from
the parent (set in server.impl.ts) would override the child's config.
2026-02-16 23:52:16 +01:00
Jadilson Guedes
4641e452dd fix(docs): update English fallback links after file reorganization
After rebasing onto current main, many English docs were reorganized
   into subdirectories. This updates all "Open English doc" fallback links
   in pt-BR and es translations to point to the correct new paths.

   Fixed 30 broken links across 15 pages × 2 languages:
   - /bedrock → /providers/bedrock
   - /broadcast-groups → /channels/broadcast-groups
   - /debugging → /help/debugging
   - /environment → /help/environment
   - /hooks → /cli/hooks
   - /scripts → /help/scripts
   - /multi-agent-sandbox-tools → /tools/multi-agent-sandbox-tools
   - /testing → /help/testing
   - /token-use → /reference/token-use
   - /concepts/channel-routing → /channels/channel-routing
   - /concepts/group-messages → /channels/group-messages
   - /concepts/groups → /channels/groups
   - /start/pairing → /cli/pairing
   - /gateway/security/formal-verification → /security/formal-verification
   - /hooks/soul-evil → /cli/hooks (no English version exists)

   Verified with: node scripts/docs-link-audit.mjs (0 broken links)
2026-02-16 23:52:06 +01:00
Jadilson Guedes
84764eea52 fix(docs): remove dead references to railway, render and northflank
Remove references in the navigation to deployment pages that do not exist:
- railway.md
- render.md
- northflank.md

These pages were listed in docs.json but the files do not exist
in any of the languages (en, es, pt-BR, zh-CN), causing broken links
in the documentation.

Fixes issues identified in the review of PR #14415.
2026-02-16 23:52:06 +01:00
xvlad
97bdfb6aac docs: scaffold full es and pt-BR doc routes with localized placeholders 2026-02-16 23:52:06 +01:00
xvlad
72676d318e docs: replace english locale mirrors with translated landing pages 2026-02-16 23:52:06 +01:00
xvlad
8bccf9e8ed docs: expand es and pt-BR docs trees 2026-02-16 23:52:06 +01:00
Yash
59e0e7e4ff Onboarding: fix webchat URL loopback and canonical session 2026-02-16 23:52:00 +01:00
Yaroslav Boiko
a02bcb3620 fix(test): add missing media dedup state fields to mock contexts
Pre-existing test mocks lacked pendingMessagingMediaUrls and
messagingToolSentMediaUrls fields added by the media dedup feature,
causing runtime errors in handleToolExecutionEnd.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:51:51 +01:00
Yaroslav Boiko
838259331f fix(discord): add media dedup production code for messaging tool pipeline
Wire media URL tracking through the embedded agent pipeline so that
media already sent via messaging tools is not delivered again by the
reply dispatcher.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:51:51 +01:00
Yaroslav Boiko
c7681c3cff test(media-dedup): add missing coverage for Discord media dedup wiring
Cover three integration points where media dedup could silently regress:
- trimMessagingToolSent FIFO cap at 200 entries
- buildReplyPayloads media filter wiring (new test file)
- followup-runner messagingToolSentMediaUrls filtering
2026-02-16 23:51:51 +01:00
El-Fitz
4640999e77 test: add per-account action gating tests for Discord and Telegram handlers 2026-02-16 23:51:47 +01:00
El-Fitz
a03fec2a3f fix: use per-account action config for Discord and Telegram gating
listActions now unions gates across all enabled accounts (matching the
Signal pattern), and handleDiscordAction/handleTelegramAction resolve
through the per-account merged config instead of reading only the
top-level channel actions object.  This lets account-specific
moderation/sticker/presence overrides take effect at both listing and
execution time.
2026-02-16 23:51:47 +01:00
Colin
1faf8e8e9d Slack: add external select flow for large arg menus 2026-02-16 23:51:44 +01:00
Colin
7a4efbb030 Slack: capture workflow button interaction metadata 2026-02-16 23:51:44 +01:00
Colin
bd20c1e24d Slack: include stacked modal lifecycle context 2026-02-16 23:51:44 +01:00
Colin
ce973332f6 Slack: add media block fallback text handling 2026-02-16 23:51:44 +01:00
Colin
7aaf1547df Slack: escape mrkdwn in interaction confirmations 2026-02-16 23:51:44 +01:00
Colin
a7c1b8aea7 Slack: attribute interaction confirmations and structured selects 2026-02-16 23:51:44 +01:00
Colin
9fcb93dd13 Slack: add rich text previews for modal inputs 2026-02-16 23:51:44 +01:00
Colin
05ab147081 Slack: expand advanced modal controls payloads and confirms 2026-02-16 23:51:44 +01:00
Colin
5bbbc3e3e6 Slack: show picker values in interaction confirmations 2026-02-16 23:51:44 +01:00
Colin
5f9a04604e Slack: add header and context blocks to arg menus 2026-02-16 23:51:44 +01:00
Colin
7c5529a153 Slack: enrich modal input payload normalization 2026-02-16 23:51:44 +01:00
Colin
d1aa2323bd Slack: update action rows for select interactions 2026-02-16 23:51:44 +01:00
Colin
1bfdd4e237 Slack: add overflow menus for slash arg choices 2026-02-16 23:51:44 +01:00
Colin
296ba8e934 Slack: enrich block action context payloads 2026-02-16 23:51:44 +01:00
Colin
7e42408ade Slack: dedupe normalized interaction selections 2026-02-16 23:51:44 +01:00
Colin
6e790303df Slack: validate runtime blocks in send and edit paths 2026-02-16 23:51:44 +01:00
Colin
c01c6b7079 Slack: expand interaction payload normalization coverage 2026-02-16 23:51:44 +01:00
Colin
ac969e602c Slack: add modal private metadata utilities 2026-02-16 23:51:44 +01:00
Colin
82d132f1ba Slack: add send blocks behavior tests 2026-02-16 23:51:44 +01:00
Colin
e8a1d4171d Slack: guard select option value length in slash menus 2026-02-16 23:51:44 +01:00
Colin
c943ffab7c Slack: reject blocks plus media in send paths 2026-02-16 23:51:44 +01:00
Colin
10d876e319 Slack: validate blocks input shape centrally 2026-02-16 23:51:44 +01:00
Colin
e023c84d78 Slack: infer interaction channel type from channel ID 2026-02-16 23:51:44 +01:00
Colin
378e18b75b Slack: support blocks in plugin edit action 2026-02-16 23:51:44 +01:00
Colin
3912a2264b Slack: support blocks in plugin send action 2026-02-16 23:51:44 +01:00
Colin
08bc1dce6a Slack: support Block Kit blocks in editMessage 2026-02-16 23:51:44 +01:00
Colin
c9684a2678 Slack: support Block Kit blocks in sendMessage actions 2026-02-16 23:51:44 +01:00
Colin
bd17587b2a Slack: route modal interactions via private metadata 2026-02-16 23:51:44 +01:00
Colin
d57cbcf713 Slack: use static_select for large slash arg menus 2026-02-16 23:51:44 +01:00
Colin
cf0ca47a82 Slack: capture Block Kit view closed events 2026-02-16 23:51:44 +01:00
Colin
e7cded82b2 Slack: capture Block Kit modal submissions 2026-02-16 23:51:44 +01:00
Colin
21ba564fb0 Slack: fix CI typing for interaction handler 2026-02-16 23:51:44 +01:00
Colin
55b70aa8b4 Slack: register interaction event handler 2026-02-16 23:51:44 +01:00
Colin
9419d029c9 Slack: enrich Block Kit interaction events 2026-02-16 23:51:44 +01:00
Sean McLellan
06b961b037 fix: flatten remaining anyOf/oneOf in Gemini schema cleaning
The Cloud Code Assist API rejects anyOf/oneOf in tool schemas, not just
unsupported keywords. The image tool (index 21) had:
  image: { anyOf: [{ type: "string" }, { type: "array" }] }
which caused "JSON schema is invalid" errors when forwarded to Anthropic
via google-antigravity.

simplifyUnionVariants only handles literal unions and single non-null
variants. This adds a fallback in cleanSchemaForGeminiWithDefs that
flattens any remaining anyOf/oneOf to a simple type schema.

Also reverts the previous provider-aware normalizeToolParameters and
sanitizeToolsForGoogle changes, which were incorrect — the cleaning IS
needed for Google's API regardless of which downstream model is used.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:51:41 +01:00
Sean McLellan
1bbf6206d5 fix: exclude google-antigravity from Gemini schema sanitization
google-antigravity serves Anthropic models (e.g. claude-opus-4-6-thinking),
not Gemini. sanitizeToolsForGoogle was stripping JSON Schema keywords
(minimum, maximum, format, etc.) needed for Anthropic's draft 2020-12
compliance, causing "JSON schema is invalid" rejections on tool 21
(web_search).

This was the actual root cause — the earlier normalizeToolParameters
fix was being overridden by this second sanitization pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:51:41 +01:00
Sean McLellan
fe94e83f6b fix: make tool schema normalization provider-aware
The cleanSchemaForGemini function was being applied universally to all
tools for all providers, stripping out valid JSON Schema keywords like
minimum/maximum that are required by Anthropic's draft 2020-12 validation.

This caused the 21st tool (web_search) to fail with google-antigravity
because its count parameter's constraints were being removed.

Changes:
- Modified normalizeToolParameters to accept modelProvider option
- Only apply Gemini-specific cleaning when provider is Gemini/Google
- Skip aggressive cleaning for Anthropic/google-antigravity providers
- Updated call site in createOpenClawCodingTools to pass modelProvider

Fixes schema validation errors for Anthropic models served via google-antigravity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:51:41 +01:00
Santosh
382158fb30 fix(ui): auto-refresh sessions list after deletion
Remove dead loadSessions call from deleteSession controller that was
silently failing due to sessionsLoading guard. The refresh now happens
explicitly in the UI layer after successful deletion.

- src/ui/controllers/sessions.ts: remove internal loadSessions call
- src/ui/app-render.ts: add async onDelete handler with explicit refresh
2026-02-16 23:51:37 +01:00
Jadilson Guedes
66fc12a40c style: apply oxfmt formatting to app-render.ts 2026-02-16 23:51:34 +01:00
Jadilson Guedes
1bb2d65ff3 fix: remove unused imports and simplify boolean comparison 2026-02-16 23:51:34 +01:00
Jadilson Guedes
fe613297a7 fix: add type assertions for unknown value indexing in translate.ts 2026-02-16 23:51:34 +01:00
Jadilson Guedes
d30f5a2438 fix: resolve linting issues (curly braces, unused imports, any types) 2026-02-16 23:51:34 +01:00
Jadilson Guedes
075317ab16 fix: correct function names in overview.ts and add type assertion in translate.ts 2026-02-16 23:51:34 +01:00
Jadilson Guedes
f20bef3d79 fix: add .ts extensions to i18n imports for ESM compatibility 2026-02-16 23:51:34 +01:00
Jadilson Guedes
e0c45eab49 style: apply oxfmt formatting 2026-02-16 23:51:34 +01:00
Jadilson Guedes
98ed2e7130 fix(i18n): add missing agents and usage tabs to zh-TW locale 2026-02-16 23:51:34 +01:00
Jadilson Guedes
cf44a0c4c1 fix(ui): localize language selector and validate stored locale
- Add translation keys for language selector label and language names
   - Update all locale files (en, pt-BR, zh-CN, zh-TW) with:
     - overview.access.language key for selector label
     - languages.* keys for language display names
   - Localize language selector in overview.ts to react to locale changes
   - Add validation for stored locale in app.ts to prevent invalid values
     from causing silent failures in setLocale

   Fixes issues identified in code review:
   - Unlocalized language selector inconsistency
   - Settings locale type drift risk
2026-02-16 23:51:34 +01:00
Manus AI
a9c952b13a fix(i18n): resolve dynamic import warnings and add zh-TW locale 2026-02-16 23:51:34 +01:00
Manus AI
4b17ce7f48 feat(ui): add i18n support with English, Chinese, and Portuguese 2026-02-16 23:51:34 +01:00
Marcus Widing
a03098ca49 docs(cron): add subagent announce retry troubleshooting section 2026-02-16 23:51:29 +01:00
Marcus Widing
348ea6be96 docs: fix missing period in fly.io frontmatter description 2026-02-16 23:51:25 +01:00
saurav470
d2dd282034 docs(exec): document pty for TTY-only CLIs (gog) 2026-02-16 23:51:22 +01:00
yinghaosang
f275611862 fix(sandbox): restore SHA-1 in slugifySessionKey to preserve workspace dirs (#18503) 2026-02-16 23:51:19 +01:00
norunners
d799a3994f fix(doctor): reconcile gateway service token drift after re-pair
`openclaw doctor` audited gateway service runtime/path settings but did not
check whether the daemon's `OPENCLAW_GATEWAY_TOKEN` matched
`gateway.auth.token` in `openclaw.json`.

After re-pairing or token rotation, the config token and service env token can
drift. The daemon may keep running with a stale service token, leading to
unauthorized handshake failures for cron/tool clients.

Add a gateway service audit check for token drift and pass
`cfg.gateway.auth.token` into service audits so doctor treats config as the
source of truth when deciding whether to reinstall the service.

Key design decisions:
- Use `gateway.auth.token` from `openclaw.json` as the authority for service
  token drift detection
- Only flag mismatch when an authoritative config token exists
- Keep fix in existing doctor service-repair flow (no separate migration step)
- Add focused tests for both audit mismatch behavior and doctor wiring

Fixes #18175
2026-02-16 23:51:16 +01:00
j2h4u
5f821ed067 fix(session): prevent stale threadId leaking into non-thread sessions
When a user interacts with the bot inside a DM topic (thread), the
session persists `lastThreadId`. If the user later sends a message
from the main DM (no topic), `ctx.MessageThreadId` is undefined and
the `||` fallback picks up the stale persisted value — causing the
bot to reply into the old topic instead of the main conversation.

Only fall back to `baseEntry.lastThreadId` for thread sessions where
the fallback is meaningful (e.g. consecutive messages in the same
thread). Non-thread sessions now correctly leave threadId unset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:51:12 +01:00
Brandon Wise
01b37f1d32 fix(telegram): handle large file getFile errors gracefully
Catch GrammyError when getFile fails for files >20MB (Telegram Bot API limit).
Log warning, skip attachment, but continue processing message text.

- Add FILE_TOO_BIG_RE regex to detect 'file is too big' errors
- Add isFileTooBigError() and isRetryableGetFileError() helpers
- Skip retrying permanent 400 errors (they'll fail every time)
- Log specific warning for file size limit errors
- Return null so message text is still processed

Fixes #18518
2026-02-16 23:51:09 +01:00
Dinakar Sarbada
1953b938e3 test(heartbeat): update runner tests to match current implementation 2026-02-16 23:51:05 +01:00
Gustavo Madeira Santana
d35172cce5 docs: add changelog entry for Telegram media placeholder fix 2026-02-16 23:50:59 +01:00
yinghaosang
0587e4cc73 fix(agents): restrict MEDIA: token parsing to line start in tool results (#18510) 2026-02-16 23:50:59 +01:00
Hudson
93fbe6482b fix(sessions): archive transcript files when pruning stale entries
pruneStaleEntries() removed entries from sessions.json but left the
corresponding .jsonl transcript files on disk indefinitely.

Added an onPruned callback to collect pruned session IDs, then
archives their transcript files via archiveSessionTranscripts()
after pruning completes. Only runs in enforce mode.
2026-02-16 23:50:56 +01:00
Hudson
441401221d fix(media): clean expired files in subdirectories
cleanOldMedia() only scanned the top-level media directory, but
saveMediaBuffer() writes to subdirs (inbound/, outbound/, browser/).
Files in those subdirs were never cleaned up.

Now recurses one level into subdirectories, deleting expired files
while preserving the subdirectory folders themselves.
2026-02-16 23:50:56 +01:00
gitwithuli
c89eb351ea style: run oxfmt formatting on doctor-config-flow.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:50:53 +01:00
gitwithuli
304bfefaf9 chore: remove unused channelName parameter from ensureWildcard
Addresses review feedback — channelName was declared but only
prefix was used for change messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:50:53 +01:00
gitwithuli
b05273de61 fix: doctor --fix auto-repairs dmPolicy="open" missing allowFrom wildcard
When a channel is configured with dmPolicy="open" but without
allowFrom: ["*"], the gateway rejects the config and exits.
The error message suggests running "openclaw doctor --fix", but
the doctor had no repair logic for this case.

This adds a repair step that automatically adds "*" to allowFrom
(or creates it) when dmPolicy="open" is set without the required
wildcard. Handles both top-level and nested dm.allowFrom, as well
as per-account configs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:50:53 +01:00
zisisp
71dad89193 Revert "skills/video-quote-finder: add markdown PR hygiene checks"
This reverts commit 38c0d42542.
2026-02-16 23:50:47 +01:00
zisisp
d0793cbb9b skills/video-quote-finder: add markdown PR hygiene checks 2026-02-16 23:50:47 +01:00
zisisp
28216956ec docs: use markdown link to satisfy no-bare-urls lint 2026-02-16 23:50:47 +01:00
zisisp
84a37129fd docs: wrap original prompt blockquote for lint compliance 2026-02-16 23:50:47 +01:00
zisisp
e2f28ff4cb skills/video-quote-finder: strip URL fragments before adding timestamp 2026-02-16 23:50:47 +01:00
zisisp
61726a2fbd skills: add video-quote-finder with timestamp links 2026-02-16 23:50:47 +01:00
Colin
89ce1460e1 feat(slack): add configurable stream modes 2026-02-16 23:50:42 +01:00
Colin
087edec93f feat(slack): add draft preview cleanup lifecycle 2026-02-16 23:50:42 +01:00
Colin
dfd5a79631 fix(slack): pass account token for draft final chat.update 2026-02-16 23:50:42 +01:00
Colin
bec974aba9 feat(slack): stream partial replies via draft message updates 2026-02-16 23:50:42 +01:00
gleb
78c34bcf33 Add runtime quiting functionality to doctor.ts 2026-02-16 23:50:37 +01:00
gleb
2540417170 Add to exit process when doctor has finished 2026-02-16 23:50:37 +01:00
Mrseenz
b6d934c2c7 Agents: improve Windows scaffold helpers for venture studio 2026-02-16 23:50:34 +01:00
Winry
c15385fc94 fix(telegram): enable voice-note transcription in DMs and add CLI fallback
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 patch widens the condition to fire whenever there is audio and no
accompanying text, regardless of chat type.

It also adds a fallback path: if the standard media pipeline returns no
transcript (e.g. format mismatch, missing config), OpenClaw now calls
the configured whisper CLI command directly with the audio file, using
the same {{MediaPath}}/{{OutputBase}} template variables from config.

Co-Authored-By: TH <tzhsn.huang@gmail.com>
2026-02-16 23:50:31 +01:00
HAL
e8b03a8622 fix(agents): replace anyOf with string in image tool schema
Anthropic's API rejects `anyOf` in `input_schema`, causing all Claude
requests to fail when the image tool is registered. Replace
`Type.Union([Type.String(), Type.Array(Type.String())])` with
`Type.String()` — the execute handler already normalizes both string
and array inputs, so this is schema-only.

Fixes #18551
2026-02-16 23:50:27 +01:00
Nate Fikru
6d31d1ecc6 fix(plugins): enforce high-priority override precedence
Make before_agent_start override merging preserve the first defined
model/provider override so higher-priority hooks cannot be overwritten by
lower-priority handlers, and align the corresponding test title and
expectation with the intended precedence behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 23:50:24 +01:00
Nate Fikru
2456b17587 test(plugins): add Layer 1+2 tests for model override hook
Layer 1: Hook merger tests verify modelOverride/providerOverride are
correctly propagated through the before_agent_start merger with
priority ordering, backward compatibility, and field isolation.

Layer 2: Pipeline wiring tests verify the earlyHookResult passthrough
contract between run.ts and attempt.ts, graceful error degradation,
and that overrides correctly modify provider/model variables.

19 tests total across 2 test files.
2026-02-16 23:50:24 +01:00
Nate Fikru
b90eb51520 feat(plugins): add modelOverride/providerOverride to before_agent_start hook
Enable plugins to override the model and provider for agent runs by
returning modelOverride/providerOverride from the before_agent_start
hook. The hook is now invoked early in run.ts (before resolveModel)
so overrides take effect. The result is passed to attempt.ts via
earlyHookResult to prevent double-firing.

This enables security-critical use cases like routing PII-containing
prompts to local models instead of cloud providers.
2026-02-16 23:50:24 +01:00
Hubert
15dd2cda20 feat: show transcript file size in session status
Add transcript size monitoring to /status and session_status tool.
Displays file size and message count (e.g. '📄 Transcript: 1.2 MB,
627 messages'). Shows ⚠️ warning when transcript exceeds 1 MB, which
helps catch sessions approaching the compaction death spiral described
in #13624.

- getTranscriptInfo() reads JSONL file stat + line count
- Wired into both /status command and session_status tool
- 8 new tests covering file reading, formatting, and edge cases
2026-02-16 23:50:21 +01:00
smartprogrammer93
fc6d53c895 fix: correct import path in test and restore deleted schema help entries 2026-02-16 23:50:18 +01:00
smartprogrammer93
6d2e3685d6 feat(tools): add URL allowlist for web_search and web_fetch
Add optional urlAllowlist config at tools.web level that restricts which
URLs can be accessed by web tools:

- Config types (types.tools.ts): Add urlAllowlist?: string[] to tools.web
- Zod schema: Add urlAllowlist field to ToolsWebSchema
- Schema help: Add help text for the new config fields
- web_search: Filter Brave search results by allowlist (provider=brave)
- web_fetch: Block URLs not matching allowlist before fetching
- ssrf.ts: Export normalizeHostnameAllowlist and matchesHostnameAllowlist

URL matching supports:
- Exact domain match (example.com)
- Wildcard patterns (*.github.com)

When urlAllowlist is not configured, all URLs are allowed (backwards compatible).

Tests: Add web-tools.url-allowlist.test.ts with 23 tests covering:
- URL allowlist resolution from config
- Wildcard pattern matching
- web_fetch error response format
- Brave search result filtering
2026-02-16 23:50:18 +01:00
Jean Carlos Nunez
e179d453c7 fix: resolve #12770 - update Antigravity default model and trim leading whitespace in BlueBubbles replies 2026-02-16 23:50:14 +01:00
OpenClaw Agent
0af795287a Fix: Doctor refers to deprecated auth command
Replaces deprecated 'openclaw auth add --provider' with
'openclaw configure --section provider' in doctor-memory-search.ts

Closes #18535
2026-02-16 23:50:11 +01:00
Aditya Singh
facfa410a7 fix(tool-display): satisfy format/lint and address review feedback
- extract web_search/web_fetch detail resolvers into common module\n- fix node -c classification so file path remains positional\n- remove dead git subcommands set\n- keep exec summary refinements (heredoc/node check/git -C/preamble strip)\n- make tests cover node -c syntax-check path\n- run format:check, tsgo, lint, and focused e2e tests
2026-02-16 23:50:08 +01:00
Aditya Singh
24f213e7ed feat(tool-display): add intent-first details and exec summaries
- add human-readable read/write/edit/attach details with path alias support\n- add explicit web_search/web_fetch phrasing (quoted query, mode/limit)\n- make detail text title-first by returning detail-only in formatters\n- add deterministic exec summarizer (wrappers, pipelines, heredoc, git/node/python heuristics, preamble stripping)\n- extend e2e coverage for file/web/exec cases
2026-02-16 23:50:08 +01:00
OscarMinjarez
b9c45d003d chore: format scripts/ui.js with oxfmt 2026-02-16 23:50:05 +01:00
OscarMinjarez
b60b44b42e fix(scripts): fix spawn EINVAL error on Windows in ui.js 2026-02-16 23:50:05 +01:00
Daniel Wondyifraw
290f337594 fix: remove references to non-existent test file 2026-02-16 23:50:01 +01:00
Daniel Wondyifraw
eec1f3e9db fix: address code review feedback - move test data, fix patterns, rewrite docs as RFC 2026-02-16 23:50:01 +01:00
Daniel Wondyifraw
5801c4f983 feat(telegram): add outbound sanitizer leak corpus and docs
- Add leak corpus test cases (tests/data/telegram_leak_cases.json)
- Add sanitizer documentation (docs/telegram-sanitizer.md)
- Block internal diagnostics from reaching users
- Strip wrapper artifacts from LLM output
- Static response for unknown slash commands
2026-02-16 23:50:01 +01:00
Jean Carlos Nunez
c08e8c0359 correct format 2026-02-16 23:49:58 +01:00
Jean Carlos Nunez
a0191426dc clean code - delete message 2026-02-16 23:49:58 +01:00
Jean Carlos Nunez
f476c8b48b Fix #12767: Heartbeat strip responsePrefix before HEARTBEAT_OK suppression 2026-02-16 23:49:58 +01:00
Shaun Mason
feed570984 fix: syncs all credential types to agent auth.json
Previously, the synchronization of credentials to the agent's  file was limited to  OAuth profiles. This prevented other providers and credential types from being correctly registered for agent use.

This update expands the synchronization to include ,  (mappedto ), and  credentials for all configured providers.

It ensures the agent's  accurately reflects available credentials, enabling proper authentication and model discovery.

The synchronization now:
- Converts all supported credential types.
- Skips profiles with empty keys.
- Preserves unrelated entries in the target .
- Only writes to disk when actual changes are detected.
2026-02-16 23:49:54 +01:00
Daniel Sauer
12ce358da5 fix(failover): recognize 'abort' stop reason as timeout for model fallback
When streaming providers (GLM, OpenRouter, etc.) return 'stop reason: abort'
due to stream interruption, OpenClaw's failover mechanism did not recognize
this as a timeout condition. This prevented fallback models from being
triggered, leaving users with failed requests instead of graceful failover.

Changes:
- Add abort patterns to ERROR_PATTERNS.timeout in pi-embedded-helpers/errors.ts
- Extend TIMEOUT_HINT_RE regex to include abort patterns in failover-error.ts

Fixes #18453

Co-authored-by: James <james@openclaw.ai>
2026-02-16 23:49:51 +01:00
Guy
32c66aff49 fix: add windowsHide: true to spawn in runCommandWithTimeout
Fixes flashing conhost.exe windows on Windows when exec module spawns
child processes. The windowsHide: true option prevents orphaned conhost.exe
processes and eliminates disruptive terminal window flashing.

Closes #18613
2026-02-16 23:49:47 +01:00
Daniel Sauer
20957efa46 fix(process): graceful process tree termination with SIGTERM before SIGKILL
Process trees (pty sessions, tool exec) were being SIGKILL'd immediately
without any grace period for cleanup. This prevented child processes from:
- Flushing buffers and closing files cleanly
- Closing network connections
- Terminating their own child processes
- Removing temporary files

Changes:
- Send SIGTERM to process group first (Unix)
- Wait configurable grace period (default 3s)
- Then SIGKILL if process still alive
- Windows: taskkill without /F first, then with /F after grace period
- Use unref() on timeout to not block event loop exit

Fixes #18619

Co-authored-by: James <james@openclaw.ai>
2026-02-16 23:49:44 +01:00
Tomas Hajek
19ae7a4e17 fix(session-memory): fallback to rotated transcript after /new
When /new rotates <session>.jsonl to <session>.jsonl.reset.*, the session-memory hook may read an empty active transcript and write header-only memory entries.

Add fallback logic to read the latest .jsonl.reset.* sibling when the primary file has no usable content.

Also add a unit test covering the rotated transcript path.

Fixes #18088
Refs #17563
2026-02-16 23:49:41 +01:00
Peter Steinberger
769f7631d5 refactor(test): dedupe duplicate dispatch test flow 2026-02-16 22:47:34 +00:00
Peter Steinberger
af5d4ac7d3 refactor(test): dedupe doctor legacy migration fixtures 2026-02-16 22:47:26 +00:00
Peter Steinberger
389eb8ba10 refactor(test): dedupe discord component registry fixtures 2026-02-16 22:43:37 +00:00
Peter Steinberger
abbe04b184 refactor(discord): share attachment media resolution loop 2026-02-16 22:43:30 +00:00
Peter Steinberger
1aabe9712a refactor(discord): dedupe reaction notification flow 2026-02-16 22:39:42 +00:00
Peter Steinberger
61859377a5 refactor(test): dedupe pi-tools loop detection test setup 2026-02-16 22:39:42 +00:00
Peter Steinberger
05bfb7f9f9 refactor(test): reuse discord message handler base context harness 2026-02-16 22:39:42 +00:00
Dakshay Mehta
8947d2dea5 Agents: format process poll backoff files 2026-02-16 23:32:12 +01:00
Dakshay Mehta
23f5cc80a4 Agents: wire command poll backoff into process poll 2026-02-16 23:32:12 +01:00
Peter Steinberger
054745a7e0 refactor(test): dedupe slack monitor event fixtures 2026-02-16 22:30:39 +00:00
Peter Steinberger
11f3da7669 refactor(test): dedupe cron service test harness setup 2026-02-16 22:30:39 +00:00
Peter Steinberger
21e5c0ce57 chore: reorder latest changelog bullets by user impact 2026-02-16 23:27:23 +01:00
Vignesh Natarajan
4e930db432 fix: guard reminder note (#18588) (thanks @vignesh07) 2026-02-16 14:13:17 -08:00
Seb Slight
0f6b39ea57 Docs/Changelog: add missing entry for #18586 (#18604)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 5134983645
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 17:10:14 -05:00
Vignesh Natarajan
5a26d1c622 Agent: guard reminder promises behind cron scheduling 2026-02-16 14:07:16 -08:00
Seb Slight
0cff8bc4e6 fix(telegram): include DM topic thread id in replies (#18586) 2026-02-16 17:02:59 -05:00
pip-nomel
1567d6cbb4 feat(discord): download attachments from forwarded messages (#17049)
Co-authored-by: Shadow <shadow@openclaw.ai>
2026-02-16 15:23:40 -06:00
Shadow
c593709d25 Discord: add per-button component allowlist 2026-02-16 15:15:00 -06:00
Benjamin Jesuiter
fc8290af42 CLI: normalize help command description casing (#18569) 2026-02-16 22:10:21 +01:00
Benjamin Jesuiter
b25f334fa2 CLI: improve command descriptions in help output (#18486)
* CLI: clarify config vs configure descriptions

* CLI: improve top-level command descriptions

* CLI: make direct command help more descriptive

* CLI: add commands hint to root help

* CLI: show root help hint in implicit help output

* CLI: add help example for command-specific help

* CLI: tweak root subcommand marker spacing

* CLI: mark clawbot as subcommand root in help

* CLI: derive subcommand markers from registry metadata

* CLI: escape help regex CLI name
2026-02-16 22:06:25 +01:00
Shadow
05a83b9e97 Discord: add reusable component option 2026-02-16 14:22:49 -06:00
Shadow
fc60336c18 Discord: add native exec options 2026-02-16 14:18:17 -06:00
Sk Akram
e5eb5b3e43 feat: add stuck loop detection and exponential backoff infrastructure for agent polling (#17118)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: eebabf679b
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-16 15:16:35 -05:00
Vignesh Natarajan
1f99d82712 test (heartbeat): relax brittle reply option assertions 2026-02-16 11:57:32 -08:00
Shadow
3646625dc1 Infra: skip Discord text exec approvals 2026-02-16 13:53:12 -06:00
zerone0x
81d2a91a90 fix(discord): send initial message for non-forum thread creation (#18117)
Co-authored-by: Shadow <shadow@openclaw.ai>
2026-02-16 13:48:46 -06:00
victor-wu.eth
7c240a2b58 feat(discord): faster reaction status state machine (watchdog + debounce) (#18248)
* fix(discord): avoid unnecessary message fetches in reaction notifications

* style(discord): format reaction listener for CI

* feat(discord): add reaction status machine and fix tool/final wiring

* fix(discord): harden reaction status transitions and cleanup

* revert(discord): restore status-machine flow from 0a5a72204

* fix(auto-reply): restore lifecycle callback forwarding for channels

* chore(ci): add daily upstream sync workflow for custom branch

* fix(discord): non-blocking reactions and robust cleanup

* chore: remove unrelated workflow from Discord-only PR

* Discord: streamline reaction handling

* Docs: add Discord reaction changelog

---------

Co-authored-by: Shadow <hi@shadowing.dev>
2026-02-16 13:38:39 -06:00
Vignesh Natarajan
c953cfdee7 chore (changelog): note webchat command auth fix 2026-02-16 11:30:49 -08:00
Vignesh Natarajan
e95134ba3f fix (commands): keep webchat auth on internal provider 2026-02-16 11:30:49 -08:00
Shadow
72e228e14b Heartbeat: allow suppressing tool warnings (#18497)
* Heartbeat: allow suppressing tool warnings

* Changelog: note heartbeat tool-warning suppression
2026-02-16 13:29:24 -06:00
Latitude Bot
3238bd78d9 fix(discord): normalize bare numeric IDs in outbound target resolution
Bare numeric Discord IDs (e.g. '1470130713209602050') in cron
delivery.to caused 'Ambiguous Discord recipient' errors and silent
delivery failures.

Adds normalizeDiscordOutboundTarget() to the existing Discord
normalize module (channels/plugins/normalize/discord.ts) alongside
normalizeDiscordMessagingTarget. Defaults bare numeric IDs to
'channel:<id>', matching existing behavior.

Both the Discord extension plugin and standalone outbound adapter
use the shared helper via a one-liner resolveTarget.

Fixes #14753. Related: #13927
2026-02-16 13:25:58 -06:00
nabbilkhan
250896cf6e fix: correct contradictory test name (Greptile review)
The test verifies that cooldownUntil IS cleared when it equals exactly
`now` (>= comparison), but the test name said "does not clear". Fixed
the name to match the actual assertion behavior.
2026-02-16 12:53:45 -06:00
nabbilkhan
03cadc4b7a fix(auth): auto-expire stale auth profile cooldowns and reset error count
When an auth profile hits a rate limit, `errorCount` is incremented and
`cooldownUntil` is set with exponential backoff. After the cooldown
expires, the time-based check correctly returns false — but `errorCount`
persists. The next transient failure immediately escalates to a much
longer cooldown because the backoff formula uses the stale count:

  60s × 5^(errorCount-1), max 1h

This creates a positive feedback loop where profiles appear permanently
stuck after rate limits, requiring manual JSON editing to recover.

Add `clearExpiredCooldowns()` which sweeps all profiles on every call to
`resolveAuthProfileOrder()` and clears expired `cooldownUntil` /
`disabledUntil` values along with resetting `errorCount` and
`failureCounts` — giving the profile a fair retry window (circuit-breaker
half-open → closed transition).

Key design decisions:
- `cooldownUntil` and `disabledUntil` handled independently (a profile
  can have both; only the expired one is cleared)
- `errorCount` reset only when ALL unusable windows have expired
- `lastFailureAt` preserved for the existing failureWindowMs decay logic
- In-memory mutation; disk persistence happens lazily on the next store
  write, matching the existing save pattern

Fixes #3604
Related: #13623, #15851, #11972, #8434
2026-02-16 12:53:45 -06:00
Shadow
d3707147c0 chore: update carbon 2026-02-16 12:45:08 -06:00
Vignesh Natarajan
1cf3aba3f6 chore (changelog): note qmd multi-agent startup fix 2026-02-16 10:35:48 -08:00
Vignesh Natarajan
02c268eec1 fix (gateway/memory): start qmd onBoot for all agents 2026-02-16 10:35:26 -08:00
Vignesh
b0a01fe482 Agents/Tools: preflight exec script files for shell var injection (#18457)
* fix(agents): don't force store=true for codex responses

* test: stabilize respawn + subagent usage assertions

* Agents/Tools: preflight exec to detect shell variable injection in scripts

* Changelog: fix merge marker formatting
2026-02-16 10:34:29 -08:00
Peter Steinberger
9b70849567 refactor(test): dedupe trusted-proxy auth test setup 2026-02-16 18:31:37 +00:00
Peter Steinberger
96eabcbe89 refactor(test): share antigravity usage endpoint fixtures 2026-02-16 18:31:31 +00:00
Peter Steinberger
b0035a1e49 refactor(test): table-drive web tool defaults checks 2026-02-16 18:31:27 +00:00
Peter Steinberger
8a1893a215 refactor(test): table-drive legacy config policy assertions 2026-02-16 18:25:04 +00:00
Peter Steinberger
9372df45f2 refactor(test): table-drive auth choice option checks 2026-02-16 18:25:04 +00:00
Peter Steinberger
23480bb4e3 refactor(test): dedupe trigger model command fixtures 2026-02-16 18:25:04 +00:00
Peter Steinberger
9ff473fa05 refactor(test): share sandbox config test helpers 2026-02-16 18:25:04 +00:00
Peter Steinberger
30c8361d0a refactor(test): dedupe isolated cron turn setup 2026-02-16 18:25:04 +00:00
Shadow
1b7301051b Config: require Discord ID strings (#18220) 2026-02-16 12:22:58 -06:00
Peter Steinberger
5d40d47501 refactor(test): reduce dispatch-from-config setup duplication 2026-02-16 18:09:49 +00:00
Peter Steinberger
74c49c943d refactor(test): share web fetch e2e setup helpers 2026-02-16 18:09:45 +00:00
Peter Steinberger
9c6e879a06 refactor(test): dedupe heartbeat runner e2e scaffolding 2026-02-16 18:09:38 +00:00
Peter Steinberger
c7e386982f refactor(test): dedupe agent and memory cli test setup 2026-02-16 17:57:45 +00:00
Peter Steinberger
616d4692a9 refactor(hooks): share install temp-dir and archive fixtures 2026-02-16 17:57:45 +00:00
Peter Steinberger
9a29d7833b refactor(cli): dedupe browser and hooks command handlers 2026-02-16 17:57:45 +00:00
Nimrod Gutman
5a39e13c92 fix(ios): restore missing location monitor merge files (#18260)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: f60cd10f6d
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-02-17 01:41:53 +08:00
Mariano
f2e12646b4 docs(changelog): credit @Marvae for iOS onboarding QR (#18325)
Co-authored-by: Mariano Belinky <mariano@mb-server-643.local>
2026-02-16 17:39:53 +00:00
Mariano
9e26fe4459 fix(ios): gate talk barge-in on isolated audio routes (#18265)
Co-authored-by: Mariano Belinky <mariano@mb-server-643.local>
2026-02-16 17:37:10 +00:00
Mariano
b3859b488c feat(ios): add background listening core toggle (#18261)
Co-authored-by: Mariano Belinky <mariano@mb-server-643.local>
2026-02-16 17:36:17 +00:00
Mariano
ad27716d3f feat(ios): add Talk voice directive hint toggle (#18250)
* feat(ios): add Talk voice directive hint toggle

* docs(changelog): credit voice directive hint slice

---------

Co-authored-by: Mariano Belinky <mariano@mb-server-643.local>
2026-02-16 17:33:42 +00:00
Peter Steinberger
d688188864 refactor(tests): share outbound runner and delivery helpers 2026-02-16 17:22:26 +00:00
Peter Steinberger
71111c9978 refactor(tests): dedupe gateway send and threading fixtures 2026-02-16 17:22:26 +00:00
Peter Steinberger
291275982c refactor(web): reuse send api + access-control test helpers 2026-02-16 17:22:26 +00:00
Peter Steinberger
94a4dd0189 refactor(gateway): dedupe wizard and exec approval handler paths 2026-02-16 17:22:26 +00:00
Ayaan Zaidi
16327f21da feat(telegram): support inline button styles (#18241)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 239cb3552e
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-16 22:48:47 +05:30
Peter Steinberger
a177f7b9fe refactor(tests): dedupe slack telegram and web monitor setup 2026-02-16 17:06:40 +00:00
Peter Steinberger
8df83d1835 refactor(core): extract shared runtime and wizard schemas 2026-02-16 17:06:40 +00:00
Peter Steinberger
c37f65a449 refactor(tests): share harnesses for cli and monitor fixtures 2026-02-16 17:06:40 +00:00
Peter Steinberger
b991919755 refactor(cron): dedupe next-run recompute paths 2026-02-16 17:06:40 +00:00
Gustavo Madeira Santana
8a67016646 Agents: raise bootstrap total cap and warn on /context truncation (#18229)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: f6620526df
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-16 12:04:53 -05:00
Peter Steinberger
5b185da366 refactor(test): remove remaining command test duplication 2026-02-16 16:52:53 +00:00
Peter Steinberger
0d51869c3c refactor(test): consolidate doctor health and sandbox fixtures 2026-02-16 16:48:55 +00:00
Peter Steinberger
2d8edf85ad refactor(test): share onboarding and model auth test helpers 2026-02-16 16:48:55 +00:00
Peter Steinberger
ac5f6e7c9d refactor(test): dedupe agent and status command fixtures 2026-02-16 16:48:55 +00:00
Mariano
44ef045614 fix(canvas): port remaining iOS branch stability fixes (#18228)
* fix(canvas): prevent snapshot disconnects on proxied gateways

(cherry picked from commit 2a3c9f746a65f3301c0cfe58ebe6596fed06230f)

* fix(canvas): accept url alias for present and navigate

(cherry picked from commit 674ee86a0b776cbb738add1920a4031246125312)

---------

Co-authored-by: Nimrod Gutman <nimrod.g@singular.net>
2026-02-16 16:42:28 +00:00
Ayaan Zaidi
c8a536e30a fix(agents): scope message tool schema by channel (#18215)
Co-authored-by: Shadow <shadow@openclaw.ai>
2026-02-16 10:34:18 -06:00
Peter Steinberger
3a2fffefdb refactor(test): centralize doctor e2e runtime and snapshot scaffolding 2026-02-16 16:32:37 +00:00
Peter Steinberger
ffeeb835aa refactor(test): extract shared doctor migration test setup 2026-02-16 16:32:37 +00:00
Peter Steinberger
261f5ee492 refactor(test): dedupe command config and model test fixtures 2026-02-16 16:32:37 +00:00
Mariano
130e59a9c0 iOS: port onboarding + QR pairing flow stability (#18162)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: a87eadea19
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-16 16:22:51 +00:00
Peter Steinberger
df6d0ee92b refactor(core): dedupe tool policy and IPv4 matcher logic 2026-02-16 16:14:54 +00:00
Peter Steinberger
110b1cf46f refactor(test): centralize auth test env lifecycle cleanup 2026-02-16 16:10:18 +00:00
Mariano
9a1e168685 iOS: port gateway connect/discovery stability + onboarding reset (#18164)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 8165ec5bae
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-16 16:07:22 +00:00
Peter Steinberger
def3a3ced1 refactor(test): reduce auth and channel setup duplication 2026-02-16 16:03:22 +00:00
Peter Steinberger
9adcaccd0b refactor(test): share non-interactive onboarding test helpers 2026-02-16 16:03:22 +00:00
Mariano
2e7fac2231 iOS: port talk redaction, accessibility, and ATS hardening (#18163)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 8a9a05f04e
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-16 16:00:08 +00:00
Peter Steinberger
db3480f9b5 refactor(test): reuse provider-auth onboarding config helper 2026-02-16 15:53:13 +00:00
Mariano
6effcdb551 OpenClawKit: stabilize iOS ChatUI updates after gateway replies (#18165)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 9b6e38d5be
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-16 15:51:11 +00:00
Peter Steinberger
f1351fc545 refactor(test): centralize auth test agent-dir helpers 2026-02-16 15:44:33 +00:00
Peter Steinberger
36a5ff8135 refactor(test): consolidate provider-auth config snapshot typing 2026-02-16 15:42:50 +00:00
Peter Steinberger
a948a3bd00 refactor(test): share gateway onboarding state-dir lifecycle 2026-02-16 15:40:48 +00:00
Peter Steinberger
a0e8f00b20 refactor(test): simplify auth-choice profile assertions 2026-02-16 15:38:37 +00:00
Peter Steinberger
716872c174 refactor(test): dedupe agents identity test setup 2026-02-16 15:38:37 +00:00
Mariano
68e39cf2c3 CLI: restore and harden qr --remote pairing behavior (#18166)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: a79fc2a3c6
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-16 15:38:07 +00:00
Peter Steinberger
1633c6fe98 refactor(test): dedupe auth-choice e2e setup plumbing 2026-02-16 15:25:45 +00:00
Peter Steinberger
94f455c693 refactor(test): share auth test env/profile helpers 2026-02-16 15:25:45 +00:00
Peter Steinberger
1d37389490 test: annotate harness mocks to avoid TS2742 in CI 2026-02-16 15:19:11 +00:00
Peter Steinberger
a1ca9291f3 test(agents): fix reasoning replay input assertion helper 2026-02-16 14:59:31 +00:00
Peter Steinberger
93ca0ed54f refactor(channels): dedupe transport and gateway test scaffolds 2026-02-16 14:59:31 +00:00
Peter Steinberger
f717a13039 refactor(agent): dedupe harness and command workflows 2026-02-16 14:59:30 +00:00
Peter Steinberger
04892ee230 refactor(core): dedupe shared config and runtime helpers 2026-02-16 14:59:30 +00:00
Peter Steinberger
544ffbcf7b refactor(extensions): dedupe connector helper usage 2026-02-16 14:59:30 +00:00
Peter Steinberger
bc55ffb160 test: isolate qr/setup-code token env in unit tests 2026-02-16 14:58:38 +00:00
Peter Steinberger
c9f2c3aef9 test: trim redundant non-stop abort assertion 2026-02-16 14:58:38 +00:00
Peter Steinberger
fc9fae2c29 chore(changelog): restore 2026.2.15 and move entries to 2026.2.16 2026-02-16 15:53:00 +01:00
Mariano
599c890221 CLI/Gateway: restore qr flow with --remote support (clean) (#18091)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 4bee77ce06
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
2026-02-16 14:48:14 +00:00
pierreeurope
fec4be8dec fix(cron): prevent daily jobs from skipping days (48h jump) #17852 (#17903)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 1ffe6a45af
Co-authored-by: pierreeurope <248892285+pierreeurope@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:35:49 -05:00
brandonwise
095d522099 fix(security): create session transcript files with 0o600 permissions (#18066)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 962f497d24
Co-authored-by: brandonwise <21148772+brandonwise@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:33:40 -05:00
sebslight
6931f0fb50 refactor(telegram): avoid double-wrapping proxy fetch 2026-02-16 08:24:55 -05:00
sebslight
b4fa10ae67 refactor(infra): make fetch wrapping idempotent 2026-02-16 08:24:55 -05:00
sebslight
7b8cce0910 test(config): normalize merge-patch regression fixture formatting 2026-02-16 08:24:55 -05:00
sebslight
5b8bfd261b test(gateway): cover mixed-id config.patch rollback 2026-02-16 08:24:55 -05:00
sebslight
f4b2fd00bc fix(config): harden object-array merge-by-id fallback 2026-02-16 08:24:55 -05:00
Hongwei Ma
dddb1bc942 fix(telegram): fix streaming with extended thinking models overwriting previous messages/ also happens to Execution error (#17973)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 34b52eead8
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-16 18:54:34 +05:30
sebslight
553d17f8af refactor(agents): use silent token constant in prompts 2026-02-16 08:20:24 -05:00
Jackten
e3e8046a93 fix(infra): avoid detached finally unhandled rejection in fetch wrapper (#18014)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 4ec21c89cb
Co-authored-by: Jackten <2895479+Jackten@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:17:23 -05:00
不做了睡大觉
cb391f4bdc fix(config): prevent config.patch from destroying arrays when patch entries lack id (#18030)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: a857df9e32
Co-authored-by: stakeswky <64798754+stakeswky@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:13:51 -05:00
sebslight
3a277e394e test(agents): add cooldown expiry helper regressions 2026-02-16 08:10:52 -05:00
sebslight
d224776ffb refactor(agents): extract cooldown probe decision helper 2026-02-16 08:10:52 -05:00
zerone0x
c2a0cf0c28 fix(tts): update tool description to prevent duplicate audio delivery (#18046)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 70c096abaa
Co-authored-by: zerone0x <39543393+zerone0x@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:09:02 -05:00
Ítalo Souza
39bb1b3322 fix: auto-recover primary model after rate-limit cooldown expires (#17478) (#18045)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: f7a7865727
Co-authored-by: PlayerGhost <28265945+PlayerGhost@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:03:35 -05:00
yinghaosang
244ed9db39 fix(telegram): draft stream preview not threaded when replyToMode is on (#17880) (#17928)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: cfd4181a23
Co-authored-by: yinghaosang <261132136+yinghaosang@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-16 18:10:24 +05:30
Ayaan Zaidi
b2aa6e094d fix(telegram): prevent non-abort slash commands from racing chat replies (#17899)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 5c2f6f2c96
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-16 16:21:10 +05:30
Advait Paliwal
bc67af6ad8 cron: separate webhook POST delivery from announce (#17901)
* cron: split webhook delivery from announce mode

* cron: validate webhook delivery target

* cron: remove legacy webhook fallback config

* fix: finalize cron webhook delivery prep (#17901) (thanks @advaitpaliwal)

---------

Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
2026-02-16 02:36:00 -08:00
Peter Steinberger
d841c9b26b test: remove duplicate replyToTag assertion in split-tag case 2026-02-16 10:02:59 +00:00
Peter Steinberger
597f956a4f test: remove duplicate existing-id all-mode planner case 2026-02-16 10:01:58 +00:00
Peter Steinberger
f043f2d8c9 test: trim duplicate first-mode hasReplied assertion variant 2026-02-16 10:00:57 +00:00
Peter Steinberger
a4e7f256db test: drop redundant off-mode hasReplied assertion 2026-02-16 09:59:59 +00:00
Peter Steinberger
893f56b87d test: remove redundant multi-variable template resolution case 2026-02-16 09:59:09 +00:00
Peter Steinberger
4da68afc73 test: remove duplicate off-mode existing-id planner case 2026-02-16 09:58:05 +00:00
Peter Steinberger
7cfd0aed5f test: remove duplicate non-date negative-case assertion 2026-02-16 09:56:46 +00:00
Peter Steinberger
d611db8049 test: remove duplicate provider-prefix assertion variant 2026-02-16 09:55:44 +00:00
Peter Steinberger
3eb9c2105c test: remove duplicate date-suffix assertion variant 2026-02-16 09:54:56 +00:00
Peter Steinberger
9f6462bd56 test: trim duplicate latest-suffix assertion variant 2026-02-16 09:54:05 +00:00
Peter Steinberger
2d03473072 test: trim duplicate provider-prefix assertion in short-model tests 2026-02-16 09:52:16 +00:00
Peter Steinberger
dbcdcc5d19 test: remove duplicate positive template-variable detection case 2026-02-16 09:51:09 +00:00
Peter Steinberger
c4297a8d60 test: remove redundant no-provider short-model case 2026-02-16 09:49:58 +00:00
Peter Steinberger
deef9f91bf test: remove duplicate multi-variable template check case 2026-02-16 09:48:51 +00:00
Peter Steinberger
523193a91f test: remove duplicate static template-variable false case 2026-02-16 09:47:45 +00:00
Peter Steinberger
cd04385f9f test: remove redundant provider-plus-date model-name case 2026-02-16 09:46:44 +00:00
Peter Steinberger
82fa526bb0 test: remove duplicate undefined template-variable guard case 2026-02-16 09:45:51 +00:00
Peter Steinberger
3fb4a7eb53 test: remove duplicate hook-wake heartbeat empty-file case 2026-02-16 09:44:16 +00:00
Peter Steinberger
7a6928712b test: remove redundant explicit telegram heartbeat target case 2026-02-16 09:43:01 +00:00
Peter Steinberger
9b351fcbd8 test: remove duplicate whatsapp group heartbeat target case 2026-02-16 09:41:50 +00:00
Peter Steinberger
d3ddf893c2 test: remove redundant store-rotation integration prune case 2026-02-16 09:39:48 +00:00
Peter Steinberger
a597bd26d4 test: remove duplicate direct-enabled whatsapp ack variant 2026-02-16 09:37:42 +00:00
Peter Steinberger
6fa150a890 test: trim redundant whatsapp mention-true ack reaction case 2026-02-16 09:36:02 +00:00
Peter Steinberger
93ad783c1b test: remove redundant slash channel-policy integration case 2026-02-16 09:34:35 +00:00
Peter Steinberger
acc6b62289 test: remove low-value private-channel lookup slash edge case 2026-02-16 09:32:38 +00:00
Peter Steinberger
fec1566f04 test: remove duplicate ack-reaction none-scope branch case 2026-02-16 09:30:33 +00:00
Peter Steinberger
ced5148afd test: remove redundant identity emoji response-prefix case 2026-02-16 09:29:41 +00:00
Peter Steinberger
c0973f24c6 test: remove low-value concurrency passthrough unit case 2026-02-16 09:28:20 +00:00
Peter Steinberger
6a392b8493 test: trim redundant ack-reaction removeAfterReply guard case 2026-02-16 09:27:12 +00:00
Peter Steinberger
f8ae538985 test: remove low-value slash arg-menu payload-shape case 2026-02-16 09:25:34 +00:00
Peter Steinberger
b63f9b7066 test: remove redundant configured memory-flush prompt case 2026-02-16 09:23:03 +00:00
Peter Steinberger
4e3429ae6e test: remove low-value typing-without-consumer variant 2026-02-16 09:22:17 +00:00
Peter Steinberger
78976d3f6f test: remove low-value dm-fallback slash access edge case 2026-02-16 09:21:39 +00:00
Peter Steinberger
e30900f93e test: remove low-value deprecated pruneDays e2e mapping case 2026-02-16 09:20:40 +00:00
Peter Steinberger
cef02df9d5 test: remove redundant explicit-deny slash policy case 2026-02-16 09:19:55 +00:00
Peter Steinberger
0f7ad51020 test: remove low-signal malformed slash button edge case 2026-02-16 09:18:36 +00:00
Peter Steinberger
4e16893c61 test: remove low-value memory-flush ro workspace case 2026-02-16 09:17:47 +00:00
Peter Steinberger
192dbc3ba9 test: drop duplicate role-ordering exception rewrite case 2026-02-16 09:16:11 +00:00
Peter Steinberger
d0b0ca9fcf test: remove low-value open-policy slash channel case 2026-02-16 09:15:18 +00:00
Peter Steinberger
22c53af604 test: remove redundant saveSessionStore cap e2e case 2026-02-16 09:13:56 +00:00
Peter Steinberger
54948a1d44 test: remove redundant maintenance config mapping e2e case 2026-02-16 09:13:05 +00:00
Peter Steinberger
22a1a56e7e test: remove low-value maintenance defaults e2e assertion 2026-02-16 09:11:17 +00:00
Peter Steinberger
15f8c57797 test: speed up subagent announce e2e and drop duplicate defer case 2026-02-16 09:10:11 +00:00
Peter Steinberger
404a8bc35f test: remove redundant pruning-plus-capping e2e case 2026-02-16 09:07:24 +00:00
Peter Steinberger
7a4c131d6b test: remove low-value mirrored-text media-filename unit case 2026-02-16 09:05:38 +00:00
Peter Steinberger
b156aafab9 test: remove low-value direct metadata-mapping unit case 2026-02-16 09:04:20 +00:00
Peter Steinberger
838d875fcb test: remove low-value custom-root agent-extraction path case 2026-02-16 09:03:07 +00:00
Peter Steinberger
7932387df2 test: remove low-value stale-prune no-updatedAt edge case 2026-02-16 09:02:08 +00:00
Peter Steinberger
4d2ba58da5 test: remove low-value legacy dm-direct fallback permutation 2026-02-16 09:00:54 +00:00
Peter Steinberger
7d26eae3ee test: remove low-value no-updatedAt cap-priority edge case 2026-02-16 09:00:02 +00:00
Peter Steinberger
5dc02aa55e test: remove low-value concurrent store-entry merge permutation 2026-02-16 08:58:43 +00:00
Peter Steinberger
c8704297b2 test: remove low-value relative traversal session-file guard case 2026-02-16 08:57:45 +00:00
Peter Steinberger
eb7b5c02c3 test: remove low-value cross-storepath lock parallelism case 2026-02-16 08:56:28 +00:00
Peter Steinberger
314f193030 fix(ci): run scope detection on blacksmith runners 2026-02-16 09:56:11 +01:00
Peter Steinberger
d5bc5ab7ba test: remove low-value resolveStorePath tilde-expansion unit case 2026-02-16 08:54:55 +00:00
Peter Steinberger
fecd623431 test: remove duplicate reset precedence permutation case 2026-02-16 08:53:51 +00:00
Peter Steinberger
1e4cf489e0 fix(ci): keep main runs alive while coalescing newer pushes 2026-02-16 09:53:36 +01:00
Peter Steinberger
5d8f43ae8e test: remove duplicate explicit-agent fallback path case 2026-02-16 08:52:55 +00:00
Peter Steinberger
896f9efcb7 test: remove low-value absolute-in-dir session-file happy path 2026-02-16 08:51:41 +00:00
Peter Steinberger
f448e4bf77 test: remove low-value lock queue cleanup bookkeeping case 2026-02-16 08:50:59 +00:00
Peter Steinberger
ada7a6289f fix(ci): dedupe docker release runs by ref 2026-02-16 09:50:37 +01:00
Peter Steinberger
731d72e119 test: remove redundant in-dir relative session-file acceptance case 2026-02-16 08:49:41 +00:00
Peter Steinberger
bf801f5159 test: remove low-value unknown-session mirror guard case 2026-02-16 08:48:23 +00:00
Peter Steinberger
929a96c2f8 test: remove low-signal mirrored-text trim unit case 2026-02-16 08:47:45 +00:00
Peter Steinberger
2983ef0243 fix(ci): use ref-based concurrency across workflows 2026-02-16 09:47:07 +01:00
Peter Steinberger
b5183c93d6 test: remove low-value lock-storePath guard wrapper test 2026-02-16 08:46:49 +00:00
Peter Steinberger
bd0e7d3d22 test: remove low-value positive session-id validation case 2026-02-16 08:45:30 +00:00
Peter Steinberger
19dfdfe5a8 test: remove low-value missing-session-key mirror guard case 2026-02-16 08:44:46 +00:00
Peter Steinberger
2d6b605cc3 test: remove low-value session-file options wrapper assertion 2026-02-16 08:44:01 +00:00
Peter Steinberger
025d4152d1 fix(ci): key concurrency by ref instead of sha 2026-02-16 09:42:58 +01:00
Peter Steinberger
f9419e26bb test: remove duplicate empty-text mirror integration case 2026-02-16 08:42:38 +00:00
Peter Steinberger
a4f86dc433 test: remove low-value session-file options agent-only case 2026-02-16 08:41:46 +00:00
Peter Steinberger
0c035c85ab test: remove redundant single-error lock queue recovery case 2026-02-16 08:40:34 +00:00
Peter Steinberger
aabc09bb9b test: remove duplicate lock-queue cleanup success case 2026-02-16 08:39:43 +00:00
Peter Steinberger
0d2e13fb73 test: remove redundant transcript-path wrapper case 2026-02-16 08:38:18 +00:00
Peter Steinberger
4f05d045b9 test: remove duplicate absolute outside-session-path guard case 2026-02-16 08:37:19 +00:00
Peter Steinberger
3daaa19426 fix(ci): use JDK 17 for Android SDK setup 2026-02-16 09:36:54 +01:00
Peter Steinberger
ec00efb38d test: remove duplicate reset-by-type direct selection case 2026-02-16 08:36:30 +00:00
Peter Steinberger
83a5f7ba8c test: remove duplicate passthrough storePath guard case 2026-02-16 08:35:14 +00:00
Peter Steinberger
6a759c9191 test: remove duplicate empty-storePath guard case 2026-02-16 08:34:22 +00:00
Peter Steinberger
f6b7736744 test: remove redundant absolute topic-suffix session-file case 2026-02-16 08:33:33 +00:00
Ayaan Zaidi
b6a9741ba4 refactor(telegram): simplify send/dispatch/target handling (#17819)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: fcb7aeeca3
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-16 14:00:34 +05:30
Peter Steinberger
1f607bec49 test: remove low-value no-rotation file-size case 2026-02-16 08:24:46 +00:00
Peter Steinberger
3dbb69da05 test: remove duplicate session file options fallback case 2026-02-16 08:23:52 +00:00
Peter Steinberger
49d383ba7c test: remove redundant default-root explicit fallback case 2026-02-16 08:23:09 +00:00
Peter Steinberger
e72e8ebe62 test: remove redundant default-root extracted fallback case 2026-02-16 08:22:23 +00:00
Peter Steinberger
374ad8c813 test: remove redundant all-stale pruning case 2026-02-16 08:20:51 +00:00
Peter Steinberger
6f4da72cb5 test: remove redundant cap-entry default-fallback case 2026-02-16 08:19:06 +00:00
Peter Steinberger
facf53cc3f test: remove redundant stale-prune default-fallback case 2026-02-16 08:17:51 +00:00
Peter Steinberger
eaec65656f test: remove redundant cap-entry under-limit case 2026-02-16 08:16:34 +00:00
Peter Steinberger
dfaca933c6 test: remove redundant rotate timestamp case 2026-02-16 08:15:20 +00:00
Peter Steinberger
aaf308d7ec test: remove redundant stale-prune under-limit case 2026-02-16 08:13:57 +00:00
Peter Steinberger
d115d48a72 test: remove redundant rotate missing-file no-op case 2026-02-16 08:12:38 +00:00
Peter Steinberger
d174c38737 test: remove redundant stale-prune boundary case 2026-02-16 08:11:40 +00:00
Peter Steinberger
005dbdd13e test: remove redundant stale-prune empty-store case 2026-02-16 08:09:56 +00:00
Peter Steinberger
c31e33cd18 test: remove redundant stale-prune return-count case 2026-02-16 08:08:59 +00:00
Peter Steinberger
f4fbfae97e test: remove redundant cap-entry empty-store case 2026-02-16 08:07:35 +00:00
Peter Steinberger
f349d40e62 test: remove redundant cap-entry return-count case 2026-02-16 08:06:41 +00:00
Peter Steinberger
d71779b46f test: remove redundant session-rotation exact-limit case 2026-02-16 08:05:26 +00:00
Peter Steinberger
df062fdb63 test: remove redundant cap-entry exact-limit case 2026-02-16 08:04:24 +00:00
Peter Steinberger
52ddaed795 test: remove redundant elide exact-limit case 2026-02-16 08:03:02 +00:00
Peter Steinberger
b7a20d8e8d test: remove redundant depth-1 subagent session case 2026-02-16 08:02:10 +00:00
Peter Steinberger
5cb228fdd0 test: remove redundant quick-reply truncation case 2026-02-16 08:00:47 +00:00
Peter Steinberger
8fd6d4d6dd test: remove redundant messageAction default-text case 2026-02-16 07:59:19 +00:00
Peter Steinberger
242e8f5c43 test: remove low-signal line account listing coverage 2026-02-16 07:58:00 +00:00
Peter Steinberger
4aab640fd1 test: remove redundant default-account normalization case 2026-02-16 07:56:41 +00:00
Peter Steinberger
35b6ccd62c test: remove redundant rich-menu action passthrough case 2026-02-16 07:55:39 +00:00
Peter Steinberger
7e1f542233 test: remove redundant uriAction passthrough case 2026-02-16 07:54:37 +00:00
Peter Steinberger
5927c53630 test: remove redundant postback displayText passthrough case 2026-02-16 07:53:10 +00:00
Peter Steinberger
8505577218 test: remove redundant line account-id env listing case 2026-02-16 07:52:08 +00:00
Peter Steinberger
b18b85dc77 test: remove redundant default rich-menu command smoke case 2026-02-16 07:51:04 +00:00
Peter Steinberger
f3eb003db9 test: remove redundant quick-reply creation smoke case 2026-02-16 07:50:02 +00:00
Peter Steinberger
0448693f8f test: remove redundant messageAction passthrough case 2026-02-16 07:49:15 +00:00
Peter Steinberger
e86647889c test: remove redundant datetimepicker passthrough case 2026-02-16 07:47:36 +00:00
Peter Steinberger
993a5e63a1 test: remove redundant yes-no label passthrough case 2026-02-16 07:46:48 +00:00
Peter Steinberger
c01e97f124 test: remove redundant list-card action passthrough case 2026-02-16 07:45:09 +00:00
Peter Steinberger
bf2d78505e test: remove redundant notification title passthrough case 2026-02-16 07:43:03 +00:00
Peter Steinberger
91337b4b6f test: remove redundant confirm alt-text passthrough case 2026-02-16 07:42:09 +00:00
Peter Steinberger
f74d56bd3b test: remove redundant image-card aspect ratio passthrough case 2026-02-16 07:40:45 +00:00
Peter Steinberger
56d0ad6942 test: remove redundant action-card hero passthrough case 2026-02-16 07:39:50 +00:00
Peter Steinberger
5997a4b0ef test: remove redundant media-player image passthrough case 2026-02-16 07:39:01 +00:00
Peter Steinberger
720aa3c1e6 test: drop redundant line info-card default footer case 2026-02-16 07:37:41 +00:00
Peter Steinberger
223e2a7127 test: remove redundant button thumbnail passthrough case 2026-02-16 07:36:45 +00:00
Peter Steinberger
31ab8ad46d test: remove overlapping short line grid layout case 2026-02-16 07:35:16 +00:00
Peter Steinberger
82a8fc0bc7 test: remove redundant yes-no default template case 2026-02-16 07:34:18 +00:00
Peter Steinberger
227e31d791 test: remove redundant line default menu config case 2026-02-16 07:32:30 +00:00
Peter Steinberger
357b1e8fee test: remove duplicate line account listing case 2026-02-16 07:30:37 +00:00
Peter Steinberger
4c46c23ca8 test: remove redundant default line account id case 2026-02-16 07:29:10 +00:00
Peter Steinberger
189b2e0588 test: remove redundant line default-menu bounds case 2026-02-16 07:28:02 +00:00
Peter Steinberger
a39c2263e5 test: prune overlapping line markdown conversion cases 2026-02-16 07:26:43 +00:00
Peter Steinberger
0490d0e173 test: drop redundant product carousel limit case 2026-02-16 07:25:16 +00:00
Peter Steinberger
64a0339d58 test: trim redundant line quick-reply account checks 2026-02-16 07:23:40 +00:00
Peter Steinberger
077130bdb8 test: remove overlapping line webhook/account cases 2026-02-16 07:22:30 +00:00
Peter Steinberger
12d6b3b0c9 test: prune redundant line action-type checks 2026-02-16 07:20:57 +00:00
Peter Steinberger
3028a1bd3e test: remove redundant line template type assertions 2026-02-16 07:19:41 +00:00
Peter Steinberger
57e055ddb5 test: remove line text quick-reply passthrough tests 2026-02-16 07:17:39 +00:00
Peter Steinberger
4fd008e918 test: remove redundant flex message wrapper test 2026-02-16 07:16:46 +00:00
Peter Steinberger
d39b8541f8 test: prune redundant markdown extractor plain-text negatives 2026-02-16 07:15:47 +00:00
Peter Steinberger
ac4183edd7 test: remove redundant line existence assertions 2026-02-16 07:14:54 +00:00
Peter Steinberger
838963d66c test: drop low-signal line media player footer assertion 2026-02-16 07:13:47 +00:00
Peter Steinberger
4852dd4503 test: remove duplicate line flex wrapper coverage 2026-02-16 07:12:52 +00:00
Peter Steinberger
4d1cb661fc test: remove redundant line link menu wrapper test 2026-02-16 07:11:16 +00:00
Peter Steinberger
3bd961f00a test: drop duplicate line quick-reply wrapper assertion 2026-02-16 07:10:19 +00:00
Peter Steinberger
583345fdfe test: collapse redundant markdown conversion micro-tests 2026-02-16 07:09:31 +00:00
Peter Steinberger
3d550ed4c3 test: remove low-signal line card existence tests 2026-02-16 07:08:32 +00:00
Peter Steinberger
c37cc5ffad test: trim redundant markdown strip and table layout checks 2026-02-16 07:07:07 +00:00
Peter Steinberger
b83ccfba13 test: remove redundant line flex baseline checks 2026-02-16 07:04:56 +00:00
Peter Steinberger
8ea890e8fb test: remove duplicate line quick-reply assertions 2026-02-16 07:03:51 +00:00
Peter Steinberger
ae6060d777 test: remove redundant line markdown conversion smoke checks 2026-02-16 07:02:37 +00:00
Peter Steinberger
ec708b6ab5 test: trim redundant line action helper smoke checks 2026-02-16 07:01:43 +00:00
Peter Steinberger
944a32cf02 test: remove redundant line flex smoke checks 2026-02-16 06:59:46 +00:00
Peter Steinberger
c4880675e1 test: prune redundant line template constructor checks 2026-02-16 06:58:33 +00:00
Peter Steinberger
8b6537d857 test: trim redundant line template shape checks 2026-02-16 06:57:15 +00:00
Peter Steinberger
12c3821acb test: prune low-signal line flex template checks 2026-02-16 06:55:49 +00:00
Peter Steinberger
a69c06e3cc test: remove duplicate daemon profile trim wrappers 2026-02-16 06:53:13 +00:00
Peter Steinberger
67aa7eefe5 test: remove redundant sticker thread id assertion 2026-02-16 06:51:50 +00:00
Peter Steinberger
425c715a05 test: remove duplicate sticker recipient normalization checks 2026-02-16 06:50:44 +00:00
Peter Steinberger
dcba3e5699 test: trim redundant telegram thread+reply combination checks 2026-02-16 06:49:17 +00:00
Peter Steinberger
27083e6f1a test: remove redundant telegram requireMention negative case 2026-02-16 06:47:45 +00:00
Peter Steinberger
18bb242316 test: remove duplicate line action creator coverage 2026-02-16 06:46:21 +00:00
the sun gif man
68ea063958 🤖 fix: preserve openai reasoning replay ids (#17792)
What:
- disable tool-call id sanitization for OpenAI/OpenAI Codex transcript policy
- gate id sanitization in image sanitizer to full mode only
- keep orphan reasoning downgrade scoped to OpenAI model-switch replay path
- update transcript policy, session-history, sanitizer, and reasoning replay tests
- document OpenAI model-switch orphan-reasoning cleanup behavior in transcript hygiene reference

Why:
- OpenAI Responses replay depends on canonical call_id|fc_id pairings for reasoning followers
- strict id rewriting in OpenAI path breaks follower matching and triggers rs_* orphan 400s
- limiting scope avoids behavior expansion while fixing the identified regression

Tests:
- pnpm vitest run src/agents/transcript-policy.test.ts src/agents/pi-embedded-runner.sanitize-session-history.test.ts src/agents/openai-responses.reasoning-replay.test.ts
- pnpm vitest run --config vitest.e2e.config.ts src/agents/transcript-policy.e2e.test.ts src/agents/pi-embedded-runner.sanitize-session-history.e2e.test.ts src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.e2e.test.ts src/agents/pi-embedded-helpers.sanitizeuserfacingtext.e2e.test.ts
- pnpm lint
- pnpm format:check
- pnpm check:docs
- pnpm test (fails in current macOS bash 3.2 env at test/git-hooks-pre-commit.integration.test.ts: mapfile not found)
2026-02-15 22:45:01 -08:00
Peter Steinberger
eefda1314f test: drop duplicate telegram username allowFrom check 2026-02-16 06:44:38 +00:00
Peter Steinberger
a8a22920f1 test: remove duplicate telegram allowFrom cases 2026-02-16 06:43:24 +00:00
Peter Steinberger
a8084b24d6 test: trim additional low-signal flex template checks 2026-02-16 06:40:26 +00:00
Peter Steinberger
97d5ff3500 test: remove low-signal flex template option-only assertions 2026-02-16 06:38:41 +00:00
Peter Steinberger
abb7618b0f test: remove pass-through rich menu action mode checks 2026-02-16 06:37:38 +00:00
Peter Steinberger
1ec0f3b81d test: drop redundant daemon profile normalization wrappers 2026-02-16 06:36:15 +00:00
Peter Steinberger
6c3e7896c5 test: remove duplicate lowercase default profile daemon path cases 2026-02-16 06:34:05 +00:00
Peter Steinberger
2a5fa426f2 test: remove redundant schtasks command parsing cases 2026-02-16 06:32:59 +00:00
Peter Steinberger
29203884c2 test: consolidate gateway profile normalization coverage 2026-02-16 06:31:36 +00:00
Peter Steinberger
91e120870f test: remove duplicate uppercase default profile daemon cases 2026-02-16 06:29:20 +00:00
Peter Steinberger
6a9ead3813 test: remove duplicate profile-specific daemon constant cases 2026-02-16 06:28:15 +00:00
Peter Steinberger
cb998aa7f9 test: remove duplicate systemd exec-start split assertion 2026-02-16 06:27:19 +00:00
Peter Steinberger
ac02e45a88 test: drop redundant empty-profile extraction cases 2026-02-16 06:25:18 +00:00
Peter Steinberger
8f603ec03d test: remove duplicate default-profile casing checks 2026-02-16 06:23:34 +00:00
Peter Steinberger
84e0ee3c31 test: remove duplicate uppercase default profile case 2026-02-16 06:22:04 +00:00
Peter Steinberger
da2bdbef7e test: remove duplicate systemd exec-start split case 2026-02-16 06:21:13 +00:00
Peter Steinberger
1be7c4ba8e test: move session store pruning integration suite to e2e lane 2026-02-16 06:19:49 +00:00
Peter Steinberger
a82df1015b test: remove duplicate hr spacing assertion 2026-02-16 06:16:33 +00:00
Peter Steinberger
a0b459b8f9 test: remove duplicate undefined-profile default cases 2026-02-16 06:15:26 +00:00
Peter Steinberger
28118ca051 test: drop duplicate internal hook lifecycle case 2026-02-16 06:14:23 +00:00
Peter Steinberger
d374a64658 test: move skills-cli integration coverage to e2e lane 2026-02-16 06:13:46 +00:00
Peter Steinberger
0895bb6de6 test: move skills-install fallback suite to e2e lane 2026-02-16 06:11:01 +00:00
Peter Steinberger
189cba0100 test: remove duplicate sandbox access memory-flush case 2026-02-16 06:08:44 +00:00
Peter Steinberger
108ebc380f test: remove duplicate registry throw assertion 2026-02-16 06:06:24 +00:00
Peter Steinberger
93e62d8e3e test: remove duplicate slack dm authorization case 2026-02-16 06:04:58 +00:00
Peter Steinberger
ed28ad2822 test: remove duplicate configured canonical key case 2026-02-16 06:03:09 +00:00
Peter Steinberger
00bbddeef5 test: move git hook regression to e2e lane 2026-02-16 06:01:07 +00:00
Peter Steinberger
5ac59e6e02 test: remove duplicate availability-unavailable fallback case 2026-02-16 05:58:49 +00:00
Peter Steinberger
bfb5a44089 test: speed up plugin optional tools suite 2026-02-16 05:56:26 +00:00
Peter Steinberger
599195fb31 test: trim duplicate antigravity availability case 2026-02-16 05:53:54 +00:00
Peter Steinberger
705d83aec7 test: drop duplicate z-ai alias filter case 2026-02-16 05:41:58 +00:00
Peter Steinberger
c80017e704 test: trim duplicate z.ai provider alias case 2026-02-16 05:39:42 +00:00
Peter Steinberger
9e67f9d889 test: remove duplicate invalid slash-button case 2026-02-16 05:36:55 +00:00
Peter Steinberger
9383f85046 test: trim redundant web media prefix coverage 2026-02-16 05:34:08 +00:00
Peter Steinberger
5212d1c79e test: make sandbox symlink-escape assertion platform-aware 2026-02-16 06:26:08 +01:00
Peter Steinberger
7aa7b04fb0 test: rebalance isolated unit test lane 2026-02-16 05:22:00 +00:00
Peter Steinberger
b3d3f36360 test: speed up slack slash monitor tests 2026-02-16 05:20:22 +00:00
Peter Steinberger
2b6f8548c9 test: trim pre-commit hook integration setup 2026-02-16 05:15:55 +00:00
Peter Steinberger
9684ae4c6d test: tighten process timeout thresholds with stabilized emit guard 2026-02-16 05:09:47 +00:00
Peter Steinberger
39fa81dc96 chore: bump version to 2026.2.16 2026-02-16 06:08:47 +01:00
Peter Steinberger
f1654b4ba2 test: isolate telegram bot behavior suite from unit-fast lane 2026-02-16 04:50:19 +00:00
Peter Steinberger
0b780789bc test: further reduce process timeout waits in fast suites 2026-02-16 04:48:55 +00:00
Peter Steinberger
795874711b test: shorten process timeout waits in exec and supervisor suites 2026-02-16 04:45:44 +00:00
Peter Steinberger
17d8e2a1c8 test: reduce supervisor no-output wait threshold 2026-02-16 04:43:33 +00:00
Peter Steinberger
c53e4e6c8f test: trim exec timeout waits for faster suite runtime 2026-02-16 04:41:45 +00:00
Peter Steinberger
4f5bc0a493 chore(release): align 2026.2.15 metadata 2026-02-16 05:38:58 +01:00
Peter Steinberger
92ec3ddc14 test: drop brittle pre-commit script structure test 2026-02-16 04:35:54 +00:00
Peter Steinberger
510889d439 test: isolate slack slash and telegram bootstrap suites 2026-02-16 04:34:51 +00:00
Peter Steinberger
ceddb4a593 style(memory): format flaky ci test files 2026-02-16 05:32:42 +01:00
Peter Steinberger
794808b169 test: isolate hook installer suite from unit-fast lane 2026-02-16 04:31:30 +00:00
Peter Steinberger
fb6dba2058 fix(ci): align tar override and lockfile to 7.5.9 2026-02-16 05:30:02 +01:00
Varun Kruthiventi
c62b90a2b7 fix(telegram): stop block streaming from splitting messages when streamMode is off (#17704)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 847162caad
Co-authored-by: saivarunk <2976867+saivarunk@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-16 09:57:29 +05:30
Peter Steinberger
1b223dbdd8 test: isolate git-hooks integration and stabilize exec timeout 2026-02-16 04:24:00 +00:00
Peter Steinberger
e7ccbd1445 test: isolate block-streaming suite from unit-fast lane 2026-02-16 04:20:21 +00:00
Peter Steinberger
bc65e787c8 test: trim process no-output timeout waits 2026-02-16 04:17:38 +00:00
Vignesh Natarajan
3e1986f119 chore (changelog): note qmd collection isolation fix 2026-02-15 20:15:12 -08:00
Vignesh Natarajan
b32ae6fa0c fix (memory/qmd): isolate managed collections per agent 2026-02-15 20:14:45 -08:00
Peter Steinberger
5d436f48b2 docs: add mac beta release runbook note 2026-02-16 05:13:49 +01:00
Peter Steinberger
90800cd23e chore(release): update appcast for 2026.2.15 2026-02-16 05:10:59 +01:00
Peter Steinberger
f52805a783 test: reuse heartbeat suite fixtures across cases 2026-02-16 04:10:51 +00:00
Peter Steinberger
a7385aa8ac test: reduce process timeout test latency 2026-02-16 04:08:50 +00:00
Peter Steinberger
e8a50e41a5 test: reuse fixtures in skills install fallback suite 2026-02-16 04:03:24 +00:00
Peter Steinberger
83ce48302f test: trim timeout-heavy exec and telegram cases 2026-02-16 04:00:53 +00:00
Peter Steinberger
25dc4293bf test: speed up isolated-agent and pty test suites 2026-02-16 03:58:43 +00:00
Peter Steinberger
3fe22ea2fd chore(release): align .15 changelog ordering and release notes 2026-02-16 04:50:24 +01:00
Peter Steinberger
31939397a9 test: optimize hot-path test runtime 2026-02-16 03:49:05 +00:00
Ayaan Zaidi
9b2e1769c5 docs(contributing): update maintainers list (#17719)
* docs(contributing): refresh maintainer list

* docs(contributing): fix tyler x handle

* docs(contributing): add discord admin scope for shadow

* docs(contributing): add irc scope for vignesh
2026-02-16 09:18:35 +05:30
Peter Steinberger
61a865031f fix: sync pnpm lockfile for docker onboard 2026-02-16 04:45:42 +01:00
Peter Steinberger
ae6fe67550 test: align e2e coverage with supervisor session flow 2026-02-16 03:41:58 +00:00
Peter Steinberger
702b94fe8f style(line): format files to unblock ci check 2026-02-16 03:39:41 +00:00
Peter Steinberger
b5a63e18f9 test(sandbox): add array-order hash and recreate regression tests 2026-02-16 04:36:24 +01:00
Vignesh Natarajan
78277152ca test(heartbeat): cover telegram showOk suppression 2026-02-15 19:35:25 -08:00
Peter Steinberger
d1fca442b4 refactor(sandbox): centralize sha256 helpers 2026-02-16 04:33:47 +01:00
Sebastian
3c467baa2d test(skills): add status-to-install apt fallback coverage 2026-02-15 22:32:51 -05:00
Sebastian
c8e110e2e3 refactor(skills): extract installer strategy helpers 2026-02-15 22:32:51 -05:00
Peter Steinberger
41ded303b4 fix(sandbox): preserve array order in config hashing 2026-02-16 04:32:03 +01:00
Vignesh Natarajan
cbf58d2e1c fix(memory): harden context window cache collisions 2026-02-15 19:31:52 -08:00
Peter Steinberger
559c8d9930 fix: replace deprecated SHA-1 in sandbox config hash 2026-02-16 04:30:59 +01:00
Peter Steinberger
aef1d55300 fix(cron): normalize skill-filter snapshots and split isolated run helpers 2026-02-16 04:27:12 +01:00
Peter Steinberger
6754a926ee fix(pairing): support legacy telegram allowFrom migration 2026-02-16 03:26:07 +00:00
Vignesh Natarajan
18c6f40d32 chore (changelog): credit LINE webhook fail-closed hardening 2026-02-15 19:25:33 -08:00
Vignesh Natarajan
c7bc7249c3 test (security/line): cover missing webhook auth startup paths 2026-02-15 19:25:33 -08:00
Vignesh Natarajan
beb77229c0 fix (security/line): fail closed when webhook auth is missing 2026-02-15 19:25:33 -08:00
McRolly NWANGWU
d19b746928 feat(skills): add cross-platform install fallback for non-brew environments (#17687)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 3ed4850838
Co-authored-by: mcrolly <60803337+mcrolly@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-15 22:25:26 -05:00
Vignesh Natarajan
9df21da129 chore (changelog): credit memory flush runtime date fix 2026-02-15 19:20:38 -08:00
Vignesh Natarajan
3087657679 test (memory/compaction): cover resolved memory flush prompt semantics 2026-02-15 19:20:38 -08:00
Vignesh Natarajan
ffbcb37342 fix (memory/compaction): inject runtime date-time into memory flush prompt 2026-02-15 19:20:38 -08:00
Shadow
a61c2dc4bd Discord: add component v2 UI tool support (#17419) 2026-02-15 21:19:25 -06:00
Peter Steinberger
b4a9eacd76 chore: format qmd-manager test 2026-02-16 04:18:42 +01:00
Peter Steinberger
ac2ede5bb1 fix(telegram): treat no-op editMessage as success 2026-02-16 04:18:24 +01:00
Vignesh Natarajan
7089885ac4 chore (changelog): credit unicode FTS tokenization fix 2026-02-15 19:17:06 -08:00
Vignesh Natarajan
501e893676 fix (memory/search): support unicode tokens in FTS query builder 2026-02-15 19:17:03 -08:00
Vignesh Natarajan
82631d225c chore (changelog): credit sandbox prompt path guidance fix 2026-02-15 19:16:02 -08:00
Vignesh Natarajan
799049f586 fix (agents/sandbox): clarify container-vs-host workspace paths in prompt 2026-02-15 19:16:02 -08:00
Peter Steinberger
ab1dc89a2d chore(deps): update dependencies 2026-02-16 04:15:03 +01:00
Vignesh Natarajan
0b3d4b8e57 chore (changelog): credit control-ui scope bypass fix 2026-02-15 19:12:10 -08:00
Vignesh Natarajan
eed02a2b57 fix (security/gateway): preserve control-ui scopes in bypass mode 2026-02-15 19:12:06 -08:00
Vignesh Natarajan
a203430aa3 chore (changelog): credit pairing account isolation fix 2026-02-15 19:10:06 -08:00
Vignesh Natarajan
6cf7c02d4a feat (cli): add account selector for pairing commands 2026-02-15 19:10:06 -08:00
Vignesh Natarajan
6957354d48 fix (telegram/whatsapp): use account-scoped pairing allowlists 2026-02-15 19:10:06 -08:00
Vignesh Natarajan
ee10feb80e fix (security/pairing): scope pairing stores by account 2026-02-15 19:10:06 -08:00
Marcus Castro
61c9935264 fix: correct indentation in cron isolated-agent run.ts 2026-02-16 04:09:39 +01:00
Marcus Castro
e5dbfde7e1 test(cron): add empty-skills edge case for skill filter coverage
Addresses Greptile review feedback: locks in behavior when an agent
has skills: [] (explicit empty list), ensuring skillFilter: [] is
forwarded to buildWorkspaceSkillSnapshot to filter out all skills.
2026-02-16 04:09:39 +01:00
Marcus Castro
053affffec fix(cron): pass agent-level skill filter to isolated cron sessions
Isolated cron sessions called buildWorkspaceSkillSnapshot without
the skillFilter parameter, causing all skills to be included even
when an agent had a restricted skills list via agents.list[].skills.

Resolves the filter using resolveAgentSkillsFilter and passes it
through, aligning isolated cron with main session behavior.

Fixes #10804
2026-02-16 04:09:39 +01:00
Peter Steinberger
e1e46dc11b docs: reorder 2026.2.15 changelog entries by impact 2026-02-16 04:06:46 +01:00
Peter Steinberger
7cd288a8a0 docs: add plugin release fast path notes 2026-02-16 04:06:09 +01:00
Peter Steinberger
38ac4b8083 test(pty): stabilize non-windows signal assertion 2026-02-16 03:06:03 +00:00
Vignesh Natarajan
e7a053b4dd chore (changelog): credit qmd session collection rebind fix 2026-02-15 19:03:59 -08:00
Vignesh Natarajan
85430c8495 fix (memory/qmd): rebind drifted managed collection paths 2026-02-15 19:03:55 -08:00
Vignesh Natarajan
8e162d9319 chore (changelog): credit inbound metadata id fix 2026-02-15 19:01:08 -08:00
Vignesh Natarajan
bed8e7abe6 fix (auto-reply): expose inbound message identifiers in trusted metadata 2026-02-15 19:01:08 -08:00
Peter Steinberger
82333add95 test(sessions): cover sandbox session-tools context 2026-02-16 03:00:25 +00:00
Peter Steinberger
7a4a068124 test(sessions): add access and resolution helper coverage 2026-02-16 02:59:30 +00:00
Peter Steinberger
1a03aad246 refactor(sessions): split access and resolution helpers 2026-02-16 03:56:49 +01:00
Peter Steinberger
2f621876f1 test(gateway): cover basePath bootstrap config endpoint 2026-02-16 02:56:23 +00:00
Peter Steinberger
6dfefa1be1 test(ui): cover trailing-slash bootstrap basePath 2026-02-16 02:55:24 +00:00
Peter Steinberger
c876d24d89 test: expand prompt and update hint coverage 2026-02-16 02:54:06 +00:00
Tag
6802b155a8 fix: stop LLM retry loop when browser control service is unavailable (#17673)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 90f47fe132
Co-authored-by: tag-assistant <260167501+tag-assistant@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-15 21:53:49 -05:00
Peter Steinberger
17a148c8a8 fix: always include long-wait polling guidance in prompt 2026-02-16 03:51:38 +01:00
Peter Steinberger
abd26b6e54 refactor(ui): reuse Control UI bootstrap path constant 2026-02-16 03:50:39 +01:00
Peter Steinberger
8985f23de7 test(gateway): move Control UI http coverage 2026-02-16 03:50:39 +01:00
Peter Steinberger
c6e6023e3a refactor(gateway): share Control UI bootstrap contract and CSP 2026-02-16 03:50:39 +01:00
Peter Steinberger
6e7c1c16e7 test: remove duplicate legacy sessions_spawn e2e file 2026-02-16 03:48:51 +01:00
Peter Steinberger
52e240d10d test(status): add coverage for update summary + timestamps 2026-02-16 02:47:47 +00:00
Peter Steinberger
b6305e9725 test(skills): split installer security coverage 2026-02-16 03:47:28 +01:00
Peter Steinberger
2363e1b085 fix(security): restrict skill download target paths 2026-02-16 03:47:28 +01:00
Peter Steinberger
c6c53437f7 fix(security): scope session tools and webhook secret fallback 2026-02-16 03:47:10 +01:00
Peter Steinberger
fbe6d7c701 ci: include a2ui sources in onboarding docker build 2026-02-16 02:45:00 +00:00
Peter Steinberger
2a53eff856 perf: speed up slack slash handler tests 2026-02-16 02:45:00 +00:00
Peter Steinberger
cd37c52624 perf: speed up slack slash tests 2026-02-16 02:45:00 +00:00
Peter Steinberger
17e5a5015c perf: avoid async cron timer callbacks 2026-02-16 02:45:00 +00:00
Peter Steinberger
8515ae6eea perf: consolidate telegram bot test harness 2026-02-16 02:45:00 +00:00
Peter Steinberger
d1de66b6cf perf: speed up gateway lock tests 2026-02-16 02:45:00 +00:00
Peter Steinberger
7eeba3de85 perf: speed up telegram bot suite setup 2026-02-16 02:45:00 +00:00
Peter Steinberger
38c91c5a13 test: speed up skills-cli integration 2026-02-16 02:45:00 +00:00
Peter Steinberger
88033002ba test: consolidate nodes screen helpers 2026-02-16 02:45:00 +00:00
Peter Steinberger
f835301aed test: consolidate channel helper suites 2026-02-16 02:45:00 +00:00
Peter Steinberger
4d4f693f92 test: consolidate media store header extension coverage 2026-02-16 02:45:00 +00:00
Peter Steinberger
d95be2c384 fix: preserve sandbox allow-all semantics 2026-02-16 02:45:00 +00:00
Peter Steinberger
014d45f7ee test: tighten relay smoke + slack token validation 2026-02-16 02:45:00 +00:00
Peter Steinberger
4d9e310dad test: strengthen ports, tool policy, and note wrapping 2026-02-16 02:45:00 +00:00
Peter Steinberger
f50e1e8015 perf(test): fold gateway discover tests into run-loop 2026-02-16 02:45:00 +00:00
Peter Steinberger
aa1e4962da perf(test): fold doctor legacy migration harness cases 2026-02-16 02:45:00 +00:00
Peter Steinberger
ea07d3fdd8 perf(test): consolidate auth/pty/health mini suites 2026-02-16 02:45:00 +00:00
Peter Steinberger
f142048293 perf(test): fold tool-policy + doctor workspace entrypoints 2026-02-16 02:45:00 +00:00
Peter Steinberger
5fe47e7be6 perf(test): fold ports + terminal note suites 2026-02-16 02:45:00 +00:00
Peter Steinberger
3f44ea244f perf(test): fold health + signal mention tests 2026-02-16 02:45:00 +00:00
Peter Steinberger
6b2f40652f perf(test): consolidate daemon test entrypoints 2026-02-16 02:45:00 +00:00
Peter Steinberger
00e79ac897 perf(test): consolidate pi-embedded helpers e2e suites 2026-02-16 02:45:00 +00:00
Peter Steinberger
efe530acdd perf(test): fold session key utils into routing session key suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
d69496c449 perf(test): fold small config suites into config misc 2026-02-16 02:45:00 +00:00
Peter Steinberger
9386075b7b perf(test): fold node-host runner tests into sanitize env suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
e64dd7b56a perf(test): fold markdown list spacing into nested lists suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
0e4eada580 perf(test): fold telegram update offset store into token suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
65b5dbd6c1 perf(test): fold telegram sent-message cache tests into send suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
e770728cb5 perf(test): fold telegram download tests into fetch suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
cb80901cf9 perf(test): fold cron system event filter into system events suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
3e0076c9ce perf(test): drop redundant index entrypoint tests 2026-02-16 02:45:00 +00:00
Peter Steinberger
02124094bf perf(test): fold acp event mapper tests into client suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
37f030a671 perf(test): fold console prefix tests into logger suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
be3c431011 perf(test): fold gateway config schema tests into config misc 2026-02-16 02:45:00 +00:00
Peter Steinberger
b97c5d6158 perf(test): fold sender identity checks into channel config suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
2b4ebcb570 chore: remove accidental a2ui bundle artifacts 2026-02-16 02:45:00 +00:00
Peter Steinberger
2acc0b0f47 perf(test): fold globals unit tests into logger suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
056070c2bf perf(test): fold cron webhook schema coverage into config misc 2026-02-16 02:44:59 +00:00
Peter Steinberger
04004c5663 perf(test): consolidate models-config provider unit tests 2026-02-16 02:44:59 +00:00
Peter Steinberger
e1350ff976 perf(test): fold imessage rpc client guard into targets suite 2026-02-16 02:44:59 +00:00
Peter Steinberger
8d2029a03d perf(test): fold qr-image tests into web login suite 2026-02-16 02:44:59 +00:00
Peter Steinberger
58ab60c0fc perf(test): fold tls fingerprint normalization into ssrf suite 2026-02-16 02:44:59 +00:00
Peter Steinberger
7c27c2d659 refactor(daemon-cli): share status text styling 2026-02-16 02:42:55 +00:00
Peter Steinberger
c1655982d4 refactor: centralize pre-commit file filtering 2026-02-16 03:42:11 +01:00
Peter Steinberger
91c49dd0ea refactor(status): share registry summary formatting 2026-02-16 02:41:30 +00:00
Peter Steinberger
8eecf97cc5 refactor(cli): share gmail webhook option parsing 2026-02-16 02:39:55 +00:00
Peter Steinberger
46e714058c refactor(subagents): dedupe list row builder 2026-02-16 02:38:00 +00:00
Peter Steinberger
1547bb6a07 refactor(auto-reply): share abort persistence 2026-02-16 02:36:18 +00:00
Peter Steinberger
0c8bb361ca refactor(gateway-tool): share write metadata parsing 2026-02-16 02:36:18 +00:00
seewhy
ddcc7a1a5d fix(discord): dedupe native skill commands by skillName (#17365)
* fix(discord): dedupe native skill commands by skill name

* Changelog: credit Discord skill dedupe

---------

Co-authored-by: yume <yume@yumedeMacBook-Pro.local>
Co-authored-by: Shadow <hi@shadowing.dev>
2026-02-15 20:33:51 -06:00
Peter Steinberger
d9d5b53b42 refactor(logging): share local iso timestamp format 2026-02-16 02:32:59 +00:00
Peter Steinberger
9805ce0097 refactor(memory): reuse cached embedding collector 2026-02-16 02:32:59 +00:00
Peter Steinberger
cf69907015 fix(security): redact Telegram bot tokens in errors 2026-02-16 03:30:53 +01:00
Shakker
09566b1693 fix(discord): preserve channel session keys via channel_id fallbacks (#17622)
* fix(discord): preserve channel session keys via channel_id fallbacks

* docs(changelog): add discord session continuity note

* Tests: cover discord channel_id fallback

---------

Co-authored-by: Shadow <hi@shadowing.dev>
2026-02-15 20:30:17 -06:00
Peter Steinberger
39d5590230 refactor(line): reuse reply chunk deps type 2026-02-16 02:29:07 +00:00
Peter Steinberger
35c5d2be5c refactor(telegram): share group allowFrom resolution 2026-02-16 02:27:01 +00:00
Peter Steinberger
b88f377623 fix: make fast-tool stub type portable 2026-02-16 03:23:45 +01:00
Peter Steinberger
ba84b12535 fix: harden pre-commit hook against option injection 2026-02-16 03:23:45 +01:00
Peter Steinberger
dc9808a674 refactor(gateway): dedupe transcript tail preview 2026-02-16 02:21:39 +00:00
Peter Steinberger
60ad2c2e96 refactor(device-pairing): share token update context 2026-02-16 02:19:53 +00:00
Peter Steinberger
a7cbce1b3d refactor(security): tighten sandbox bind validation 2026-02-16 03:19:50 +01:00
Peter Steinberger
a74251d415 refactor(agents): dedupe fast tool stubs 2026-02-16 02:17:45 +00:00
Peter Steinberger
cbc3de6c97 docs(changelog): fix conflict marker 2026-02-16 03:15:57 +01:00
Peter Steinberger
01b1e350b2 docs: note Control UI XSS fix 2026-02-16 03:15:57 +01:00
Peter Steinberger
3b4096e02e fix(ui): load Control UI bootstrap config via JSON endpoint 2026-02-16 03:15:57 +01:00
Peter Steinberger
adc818db4a fix(gateway): serve Control UI bootstrap config and lock down CSP 2026-02-16 03:15:57 +01:00
Peter Steinberger
568fd337be refactor(web-fetch): dedupe firecrawl fallback 2026-02-16 02:15:02 +00:00
Peter Steinberger
d9ca051a1d refactor(auto-reply): share slash parsing for config/debug 2026-02-16 02:11:12 +00:00
Peter Steinberger
1b6704ef53 docs: update sandbox bind mount guidance 2026-02-16 03:05:16 +01:00
Peter Steinberger
887b209db4 fix(security): harden sandbox docker config validation 2026-02-16 03:04:06 +01:00
Peter Steinberger
d4bdcda324 refactor(nodes-cli): share node.invoke param builder 2026-02-16 02:03:15 +00:00
Peter Steinberger
966957fc66 refactor(nodes-cli): share pending pairing table 2026-02-16 01:58:30 +00:00
Peter Steinberger
555eb3f62c refactor(discord): share member access state 2026-02-16 01:55:40 +00:00
Peter Steinberger
93b9f1ec5f docs(changelog): note prompt path injection hardening 2026-02-16 02:53:40 +01:00
Peter Steinberger
6254e96acf fix(security): harden prompt path sanitization 2026-02-16 02:53:40 +01:00
Peter Steinberger
19f53543d2 refactor(utils): share chunkItems helper 2026-02-16 01:52:30 +00:00
Peter Steinberger
618008b483 refactor(channels): share directory allowFrom parsing 2026-02-16 01:49:16 +00:00
Peter Steinberger
31d1ed351f refactor(channels): dedupe status account bits 2026-02-16 01:47:52 +00:00
Peter Steinberger
22c1210a16 refactor(auto-reply): share directive level resolution 2026-02-16 01:45:51 +00:00
Peter Steinberger
273d70741f refactor(supervisor): share env normalization 2026-02-16 01:41:35 +00:00
Peter Steinberger
07be14c02d refactor(gateway): dedupe chat session abort flow 2026-02-16 01:39:39 +00:00
Peter Steinberger
5b2cb8ba11 refactor(cron): dedupe finished event emit 2026-02-16 01:37:03 +00:00
Peter Steinberger
1d7b2bc9c8 refactor(slack): dedupe slash reply delivery 2026-02-16 01:35:46 +00:00
Peter Steinberger
a881bd41eb refactor(outbound): dedupe plugin outbound context 2026-02-16 01:35:46 +00:00
Onur
cd44a0d01e fix: codex and similar processes keep dying on pty, solved by refactoring process spawning (#14257)
* exec: clean up PTY resources on timeout and exit

* cli: harden resume cleanup and watchdog stalled runs

* cli: productionize PTY and resume reliability paths

* docs: add PTY process supervision architecture plan

* docs: rewrite PTY supervision plan as pre-rewrite baseline

* docs: switch PTY supervision plan to one-go execution

* docs: add one-line root cause to PTY supervision plan

* docs: add OS contracts and test matrix to PTY supervision plan

* docs: define process-supervisor package placement and scope

* docs: tie supervisor plan to existing CI lanes

* docs: place PTY supervisor plan under src/process

* refactor(process): route exec and cli runs through supervisor

* docs(process): refresh PTY supervision plan

* wip

* fix(process): harden supervisor timeout and PTY termination

* fix(process): harden supervisor adapters env and wait handling

* ci: avoid failing formal conformance on comment permissions

* test(ui): fix cron request mock argument typing

* fix(ui): remove leftover conflict marker

* fix: supervise PTY processes (#14257) (openclaw#14257) (thanks @onutc)
2026-02-16 02:32:05 +01:00
Peter Steinberger
a73e7786e7 refactor(cron): share runnable job filter 2026-02-16 01:29:01 +00:00
Peter Steinberger
2679089e9e refactor(cron): dedupe next-run recompute loop 2026-02-16 01:27:40 +00:00
Peter Steinberger
c95a61aa9d refactor(cron): dedupe read-only load flow 2026-02-16 01:26:37 +00:00
Peter Steinberger
73a97ee255 refactor(gateway): share node invoke error handling 2026-02-16 01:25:06 +00:00
Peter Steinberger
b1dca644bc refactor(gateway): share restart request parsing 2026-02-16 01:21:54 +00:00
Peter Steinberger
b743e652c0 refactor(gateway): reuse shared validators + baseHash 2026-02-16 01:19:01 +00:00
Peter Steinberger
71cee673b2 fix(gateway): satisfy server-method lint 2026-02-16 01:15:31 +00:00
Peter Steinberger
dc5d234848 refactor(gateway): share server-method param validation 2026-02-16 01:13:52 +00:00
Peter Steinberger
a5cbd036de refactor(gateway): dedupe wizard param validation 2026-02-16 01:08:36 +00:00
Peter Steinberger
260a514467 refactor(slack): share channel config entry type 2026-02-16 01:06:18 +00:00
Peter Steinberger
067509fa44 refactor(onboarding): dedupe WhatsApp owner allowlist 2026-02-16 01:05:27 +00:00
Peter Steinberger
e84b20a527 refactor(status-issues): share enabled/configured account gate 2026-02-16 01:03:02 +00:00
Peter Steinberger
4aaafe5322 refactor(net): share hostname normalization 2026-02-16 01:01:22 +00:00
Peter Steinberger
d5ee766afe refactor(outbound): dedupe channel handler params 2026-02-16 00:59:42 +00:00
Peter Steinberger
00c91c3678 refactor(outbound): dedupe queued delivery param types 2026-02-16 00:57:28 +00:00
Peter Steinberger
4ab25a2889 refactor(outbound): reuse message gateway call 2026-02-16 00:56:28 +00:00
Advait Paliwal
14fb2c05b1 Gateway/Control UI: preserve partial output on abort (#15026)
* Gateway/Control UI: preserve partial output on abort

* fix: finalize abort partial handling and tests (#15026) (thanks @advaitpaliwal)

---------

Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
2026-02-15 16:55:28 -08:00
Peter Steinberger
57d5a8df86 refactor(outbound): dedupe directory list call 2026-02-16 00:54:37 +00:00
Peter Steinberger
b6871d9c0f refactor(outbound): dedupe attachment hydration 2026-02-16 00:52:27 +00:00
Peter Steinberger
f03ea76db3 fix(slack): tighten threadId type to satisfy lint 2026-02-16 00:49:00 +00:00
Peter Steinberger
753491ab80 refactor(slack): dedupe outbound send flow 2026-02-16 00:48:32 +00:00
Peter Steinberger
d00adfe98c refactor(signal): dedupe outbound media limit resolve 2026-02-16 00:47:19 +00:00
Peter Steinberger
2b2c3a071b refactor(imessage): dedupe outbound media limit resolve 2026-02-16 00:46:18 +00:00
Peter Steinberger
f8fbeb52b0 refactor(protocol): dedupe cron/config schemas 2026-02-16 00:46:11 +00:00
Peter Steinberger
cb46ea037f refactor(models): dedupe set default model updates 2026-02-16 00:43:15 +00:00
Peter Steinberger
dece9e8b07 refactor(update): share package.json readers 2026-02-16 00:41:28 +00:00
Peter Steinberger
32221e194a refactor(probe): share withTimeout 2026-02-16 00:39:11 +00:00
Peter Steinberger
5ecc364d55 fix(daemon): drop unused formatGatewayServiceDescription import 2026-02-16 00:37:19 +00:00
Peter Steinberger
0dbc51aa55 refactor(daemon): share service description resolve 2026-02-16 00:36:43 +00:00
Peter Steinberger
58cf37ceeb refactor(memory): reuse batch utils in gemini 2026-02-16 00:34:10 +00:00
Peter Steinberger
652318e56a refactor(media): share http error handling 2026-02-16 00:32:16 +00:00
Peter Steinberger
d8691ff4ec refactor(memory): share sync progress helpers 2026-02-16 00:29:01 +00:00
Peter Steinberger
8251f7c235 refactor(memory): dedupe batch helpers 2026-02-16 00:26:03 +00:00
Peter Steinberger
ae1880acf6 refactor(frontmatter): share openclaw manifest parsing 2026-02-16 00:23:33 +00:00
Peter Steinberger
fddf8a6f4a perf(test): fold pi extensions runtime registry tests into agents suite 2026-02-16 00:22:36 +00:00
Peter Steinberger
412c1d0af1 perf(test): fold logger import side-effects test into diagnostic suite 2026-02-16 00:21:30 +00:00
Peter Steinberger
166cf6a3e0 fix(web_fetch): cap response body before parsing 2026-02-16 01:21:11 +01:00
Peter Steinberger
fd3d452f1f fix(ci): fix ui cron test mock signature 2026-02-16 00:19:34 +00:00
Peter Steinberger
fdd0e78d1b perf(test): fold exec approvals socket defaults into main suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
60ce38d216 perf(test): drop redundant line signature unit test 2026-02-16 00:18:27 +00:00
Peter Steinberger
acb2a1ce37 perf(test): fold discord voice hardening into web media suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
ba3a0e7adb perf(test): fold gateway server utils into misc suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
3a7b1b36b6 perf(test): consolidate shared utility suites 2026-02-16 00:18:27 +00:00
Peter Steinberger
3830a4b58e perf(test): fold acp session store assertions into mapper suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
6288c51774 perf(test): fold secret equality assertions into audit extra suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
a508c34731 perf(test): fold signal daemon log parsing into probe suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
5baa08ed13 perf(test): fold model-default assertions into command utils suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
55fd88e967 perf(test): consolidate utils parsing helpers 2026-02-16 00:18:27 +00:00
Peter Steinberger
725f63f724 perf(test): fold restart recovery helper into spawn utils suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
c82dc02b4d perf(test): fold tui command parsing into tui suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
2cf060f774 perf(test): consolidate media-understanding misc suites 2026-02-16 00:18:27 +00:00
Peter Steinberger
5529473af9 perf(test): fold browser server-context helper into utils suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
5e3b211d93 perf(test): fold gmail watcher assertions into hooks install suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
3fd40fc5a3 perf(test): fold media constants assertions into mime suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
f934725ccd perf(test): consolidate channel misc suites 2026-02-16 00:18:27 +00:00
Peter Steinberger
5709b30700 perf(test): consolidate config misc suites 2026-02-16 00:18:27 +00:00
Peter Steinberger
2d5004cee4 perf(test): consolidate CLI utility tests 2026-02-16 00:18:27 +00:00
Peter Steinberger
1287abe0b5 perf(test): consolidate browser utility tests 2026-02-16 00:18:27 +00:00
Peter Steinberger
a91bcd2cf4 fix(test): avoid fake-timers hang in gateway lock 2026-02-16 00:18:27 +00:00
Peter Steinberger
67bfe8fb80 perf(test): cut gateway unit suite overhead 2026-02-16 00:18:26 +00:00
Peter Steinberger
be4a490c23 refactor(test): fix update-cli env restore 2026-02-16 00:16:57 +00:00
Peter Steinberger
e9ed5febc5 refactor(test): dedupe token exchange env cleanup 2026-02-16 00:16:00 +00:00
Peter Steinberger
72baa58edd refactor(test): fix copilot env restore 2026-02-16 00:15:20 +00:00
Peter Steinberger
76015aab23 refactor(test): dedupe copilot env restores 2026-02-16 00:14:48 +00:00
Advait Paliwal
115cfb4430 gateway: add cron finished-run webhook (#14535)
* gateway: add cron finished webhook delivery

* config: allow cron webhook in runtime schema

* cron: require notify flag for webhook posts

* ui/docs: add cron notify toggle and webhook docs

* fix: harden cron webhook auth and fill notify coverage (#14535) (thanks @advaitpaliwal)

---------

Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
2026-02-15 16:14:17 -08:00
Peter Steinberger
ab000bc411 refactor(test): dedupe qianfan env restore 2026-02-16 00:13:01 +00:00
Peter Steinberger
e3a93d6705 refactor(test): dedupe safe-bins mocks 2026-02-16 00:12:23 +00:00
Peter Steinberger
7857096d29 refactor(test): reuse env snapshot in model scan 2026-02-16 00:08:35 +00:00
Peter Steinberger
cedd520f25 refactor(test): simplify state dir env helpers 2026-02-16 00:08:00 +00:00
cpojer
4bdb857eca chore: Use proper pnpm caching in one CI step. 2026-02-16 09:07:09 +09:00
Peter Steinberger
997b9ad232 refactor(test): dedupe provider api key env restore 2026-02-16 00:05:02 +00:00
Peter Steinberger
e075a33ca3 refactor(test): simplify oauth/profile env restore 2026-02-16 00:03:54 +00:00
cpojer
c07036e813 chore: Update deps. 2026-02-16 09:03:29 +09:00
Shakker
b562aa6625 fix(gateway): keep boot sessions ephemeral without remapping main 2026-02-16 00:03:21 +00:00
Shakker
fe73878dfc fix(gateway): preserve session mapping across gateway restarts 2026-02-16 00:03:21 +00:00
Peter Steinberger
ee2fa5f411 refactor(test): reuse env snapshots in unit suites 2026-02-16 00:02:32 +00:00
Peter Steinberger
07dea4c6cc refactor(test): dedupe auth choice env cleanup 2026-02-15 23:59:28 +00:00
Peter Steinberger
7bb0b7d1fc refactor(test): simplify config io env snapshot 2026-02-15 23:58:06 +00:00
Peter Steinberger
a90e007d50 refactor(test): reuse env snapshot in gateway ws harness 2026-02-15 23:56:57 +00:00
Peter Steinberger
94e84e6f75 refactor(test): clean up gateway tool env restore 2026-02-15 23:56:06 +00:00
Peter Steinberger
e9c8540e21 refactor(test): simplify model auth env restore 2026-02-15 23:55:11 +00:00
Peter Steinberger
961ca61b0e refactor(test): dedupe onboard auth env cleanup 2026-02-15 23:53:55 +00:00
Peter Steinberger
f809ff5e55 refactor(test): reuse env snapshot helper 2026-02-15 23:51:24 +00:00
Peter Steinberger
d27a763eec refactor(test): reuse env helper in temp home harness 2026-02-15 23:42:20 +00:00
Peter Steinberger
abd009b092 refactor(test): dedupe openresponses server setup 2026-02-15 23:34:52 +00:00
Peter Steinberger
f0e373b82e refactor(test): simplify state dir env restore 2026-02-15 23:34:02 +00:00
Peter Steinberger
35ab521e07 refactor(test): simplify voicewake env cleanup 2026-02-15 23:34:02 +00:00
Peter Steinberger
d8d9d3724f docs(agents): add GHSA patch/publish notes 2026-02-16 00:31:51 +01:00
Peter Steinberger
e3445f59c9 docs(changelog): note inter-session provenance security fix 2026-02-16 00:31:51 +01:00
Peter Steinberger
a68ed3f64c refactor(test): reuse env snapshots in gateway call tests 2026-02-15 23:22:58 +00:00
Peter Steinberger
31980bcaf1 refactor(test): dedupe gateway env restores 2026-02-15 23:18:16 +00:00
Peter Steinberger
70f86e326d refactor(test): reuse shared env snapshots 2026-02-15 23:15:07 +00:00
Peter Steinberger
bed0e07620 fix(cli): clear plugin manifest cache after install 2026-02-15 23:14:42 +00:00
Peter Steinberger
632b71c7f8 fix(test): avoid inheriting process.env in nix config e2e 2026-02-15 23:14:42 +00:00
Peter Steinberger
eef13235ad fix(test): make sessions_spawn e2e harness ordering stable 2026-02-15 23:14:42 +00:00
Peter Steinberger
89155aa6c6 fix(test): load sessions_spawn harness before tools 2026-02-15 23:14:42 +00:00
Peter Steinberger
bbcbabab74 fix(ci): repair e2e mocks and tool schemas 2026-02-15 23:14:42 +00:00
Peter Steinberger
0e2d8b8a1e perf(test): consolidate channel action suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
c5288300a1 perf(test): consolidate reply flow suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
a7f6c95675 perf(test): consolidate slack monitor suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
74294a4653 perf(test): consolidate web auto-reply suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
c59a472ca2 perf(test): consolidate memory tool e2e suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
722bfaa9c9 perf(test): consolidate reply plumbing/state suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
37086d0c3e perf(test): consolidate sessions tool e2e suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
a1c50b4ee3 perf(test): consolidate channel plugin suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
d75cd40787 perf(test): consolidate reply utility suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
34b088ede6 perf(test): consolidate infra outbound suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
36b5f0c9a8 perf(test): consolidate gateway server-methods suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
704c8ed530 perf(test): consolidate sessions config suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
2158b09b9d perf(test): consolidate discord monitor utils 2026-02-15 23:14:42 +00:00
Peter Steinberger
ed276d3e50 perf(test): consolidate inbound reply suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
53ec78319d perf(test): consolidate session suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
51709c63fe perf(test): consolidate model selection suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
f8925b7588 perf(test): consolidate reply commands suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
023091ded3 perf(test): consolidate slack tool-result suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
ce922915ab perf(test): consolidate telegram send suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
f749365b1c perf(test): consolidate telegram create bot suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
4fc72226fa perf(test): speed up slack slash suite 2026-02-15 23:14:42 +00:00
Peter Steinberger
def74465eb perf(test): consolidate runReplyAgent suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
a91553c7cf perf(slack): consolidate slash tests 2026-02-15 23:14:42 +00:00
Peter Steinberger
65ea200c31 refactor(test): share env var helpers 2026-02-15 23:12:57 +00:00
Peter Steinberger
0b56472cf5 refactor(test): dedupe ios/android gateway client id tests 2026-02-15 23:07:50 +00:00
Peter Steinberger
8ba16a894f refactor(test): reuse withGatewayServer in auth/http suites 2026-02-15 23:06:34 +00:00
Peter Steinberger
99909f7bc7 refactor(test): share gateway server start helper 2026-02-15 23:02:27 +00:00
Peter Steinberger
1b455b6d9f refactor(test): dedupe gateway hooks server setup 2026-02-15 22:43:27 +00:00
Peter Steinberger
6b4590be06 fix(agents): stabilize sessions_spawn e2e suite 2026-02-15 22:40:28 +00:00
Tyler Yust
a948212ca7 fix(ui): show session labels in selector and standardize session key prefixes
- Display session labels in the session selector
- Cap selector width to 300px
- Standardize key prefixes and fallback names for subagent and cron job sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 14:20:54 -08:00
Peter Steinberger
d491c789a3 refactor(test): share gateway ws e2e harness 2026-02-15 22:19:08 +00:00
Peter Steinberger
e58884925a refactor(test): reuse pi embedded subscribe session harness 2026-02-15 22:12:07 +00:00
Peter Steinberger
a1ff0e4767 refactor(test): dedupe sessions_spawn thinking assertions 2026-02-15 22:12:02 +00:00
Peter Steinberger
8e7b7a2b22 refactor(test): dedupe commands e2e wizard setup 2026-02-15 22:08:13 +00:00
Peter Steinberger
d9d93485d9 refactor(test): share tool hook handler ctx 2026-02-15 22:04:07 +00:00
Peter Steinberger
5fb4032fb6 refactor(test): share overflow compaction mocks 2026-02-15 22:02:09 +00:00
David Harmeyer
7c822d039b feat(plugins): expose llm input/output hook payloads (openclaw#16724) thanks @SecondThread
Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: SecondThread <18317476+SecondThread@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-15 16:01:00 -06:00
Peter Steinberger
3c6cff5758 refactor(config): share agent sandbox schema 2026-02-15 21:57:23 +00:00
Peter Steinberger
511719424d refactor(test): dedupe terminal restore stubs 2026-02-15 21:55:56 +00:00
Peter Steinberger
8cd20e220f refactor(infra): share jsonl transcript reader 2026-02-15 21:53:12 +00:00
Peter Steinberger
c92bcf24c4 refactor(infra): dedupe device pairing token updates 2026-02-15 21:51:38 +00:00
Tak Hoffman
0c77851516 fix(agents): mark required-param tool errors as non-retryable (#17533)
* Agents: mark missing tool params as non-retryable

* Agents: include all missing required params in tool errors

* Agents: change required-param errors to retry guidance

* Docs: align changelog text for issue #14729 guidance wording
2026-02-15 15:50:44 -06:00
Peter Steinberger
50abdaf33b refactor(infra): dedupe openclaw root candidate scan 2026-02-15 21:48:46 +00:00
Peter Steinberger
012b674f31 refactor(infra): share isTailnetIPv4 helper 2026-02-15 21:47:51 +00:00
Peter Steinberger
c9bb6bd0d8 refactor(infra): extract json file + async lock helpers 2026-02-15 21:46:08 +00:00
Tyler Yust
ff4f59ec90 feat(image-tool): support multiple images in a single tool call (#17512)
* feat(image-tool): support multiple images in a single tool call

- Change 'image' parameter to accept string | string[] (Type.Union)
- Add 'maxImages' parameter (default 5) to cap abuse/token explosion
- Update buildImageContext to create multiple image content parts
- Normalize single string input to array for unified processing
- Keep full backward compatibility: single string works as before
- Update tool descriptions for both vision and non-vision models
- MiniMax VLM falls back to first image (single-image API)
- Details output adapts: 'image' key for single, 'images' for multi

* bump default max images from 5 to 20
2026-02-15 13:45:17 -08:00
Peter Steinberger
27deda2221 fix(test): drop unused gateway e2e PluginRegistry imports 2026-02-15 21:42:35 +00:00
Peter Steinberger
c3812a1ffb refactor(test): share gateway e2e registry helper 2026-02-15 21:41:18 +00:00
Peter Steinberger
84601bf96b fix(test): fix pi embedded subscribe harness typing 2026-02-15 21:34:15 +00:00
Peter Steinberger
aabe4d9b45 refactor(test): reuse env snapshot helper 2026-02-15 21:31:23 +00:00
Peter Steinberger
856e1a3187 refactor(test): share skills e2e helper 2026-02-15 21:29:15 +00:00
Peter Steinberger
5958454710 refactor(test): share auth profile order fixtures 2026-02-15 21:27:07 +00:00
Peter Steinberger
a02e5759cc refactor(test): dedupe pi embedded subscribe e2e harness 2026-02-15 21:18:53 +00:00
Vignesh Natarajan
059573a48d chore (changelog): attribute issues #17515 #17466 #17505 #17404 2026-02-15 13:12:10 -08:00
Vignesh Natarajan
150c5815eb fix (agents): honor configured contextWindow overrides 2026-02-15 13:12:10 -08:00
Vignesh Natarajan
69418cca20 fix (tui): preserve copy-sensitive token wrapping 2026-02-15 13:12:10 -08:00
Peter Steinberger
5c233f4ded fix(ui): drop unused vi in test helper 2026-02-15 21:09:59 +00:00
Peter Steinberger
c623c51cf4 refactor(ui): share app mount hooks 2026-02-15 21:09:32 +00:00
Peter Steinberger
2ac3e780e3 refactor(test): dedupe followup queue fixtures 2026-02-15 21:07:10 +00:00
Peter Steinberger
4920ca65db refactor(ui): dedupe usage session rows 2026-02-15 20:59:13 +00:00
Peter Steinberger
02ff9f43ea refactor(test): dedupe image tool e2e fixtures 2026-02-15 20:54:21 +00:00
Gustavo Madeira Santana
b4f14d6f7a Gateway: hide BOOTSTRAP in agent files after onboarding completes (#17491)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: f95f6dd052
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-15 15:42:43 -05:00
Peter Steinberger
3cd786cc2d refactor(swift): share discovery status text 2026-02-15 20:40:47 +00:00
Peter Steinberger
778959b3dc refactor(ios): dedupe gateway helpers 2026-02-15 20:38:26 +00:00
Peter Steinberger
ef2c66a16b refactor(camera): centralize JPEG transcode cap 2026-02-15 20:33:14 +00:00
Peter Steinberger
b30ed6ca4c refactor(ios): share EventKit auth gating 2026-02-15 20:24:06 +00:00
Peter Steinberger
71009ab1b6 refactor(macos): share tailnet IPv4 detection 2026-02-15 20:22:40 +00:00
Peter Steinberger
c8779ef61d refactor(macos): share pairing alert plumbing 2026-02-15 20:19:55 +00:00
Peter Steinberger
218189318d refactor(swift): share primary IPv4 lookup 2026-02-15 20:17:43 +00:00
Peter Steinberger
f37b1c11e0 refactor(macos): centralize presence system info 2026-02-15 20:12:50 +00:00
Peter Steinberger
375e16170d refactor(macos): dedupe file watcher 2026-02-15 20:07:12 +00:00
Peter Steinberger
3a075f0292 fix(macos): drop duplicate AnyCodable helpers 2026-02-15 20:05:25 +00:00
Peter Steinberger
c75fe7e3cd fix(swift): make SwiftPM tests deterministic 2026-02-15 20:03:48 +00:00
Peter Steinberger
a3419e48ab refactor(swift): dedupe AnyCodable 2026-02-15 20:00:40 +00:00
Peter Steinberger
8ccbd00e1b chore: ignore OpenClawKit SwiftPM artifacts 2026-02-15 20:00:36 +00:00
Peter Steinberger
6c33bd9c67 ci: reduce node test OOM on linux 2026-02-15 19:41:39 +00:00
Peter Steinberger
75f33e92bf fix(web): disallow workspace-* roots without explicit localRoots 2026-02-15 19:40:27 +00:00
Peter Steinberger
59c0b2bb37 refactor(auth): reuse oauth auth result helper 2026-02-15 19:37:40 +00:00
Peter Steinberger
342e9cac03 refactor(status): reuse plugin-sdk status helpers 2026-02-15 19:37:40 +00:00
Peter Steinberger
bdfa2b490b refactor(media): reuse buildAgentMediaPayload 2026-02-15 19:37:40 +00:00
Peter Steinberger
00e63da336 refactor(webhooks): reuse plugin-sdk webhook path helpers 2026-02-15 19:37:40 +00:00
Peter Steinberger
80eb91d9e7 refactor(plugin-sdk): add shared helper utilities 2026-02-15 19:37:40 +00:00
Peter Steinberger
108f0ef8c4 fix(test): remove stale cleanup calls in cron regressions 2026-02-15 19:29:28 +00:00
Peter Steinberger
92f8c0fac3 perf(test): speed up suites and reduce fs churn 2026-02-15 19:29:27 +00:00
Peter Steinberger
8fdde0429e perf(auto-reply): avoid skill scans for inline directives 2026-02-15 19:29:27 +00:00
Peter Steinberger
38f430e133 perf(models): lazy-load heavy deps in models list 2026-02-15 19:29:27 +00:00
Peter Steinberger
5c5af2b14e perf(wizard): lazy-load onboarding deps 2026-02-15 19:29:27 +00:00
Peter Steinberger
c25026f2b3 perf(plugins): lazy-create jiti loader 2026-02-15 19:29:27 +00:00
Peter Steinberger
a6158873f5 refactor(imessage): split monitor inbound processing 2026-02-15 19:29:27 +00:00
Peter Steinberger
a8f3a579d4 perf(telegram): lazy import proxy + timeout deps in audit 2026-02-15 19:29:27 +00:00
Peter Steinberger
a4b958efcd perf(test): cover embedding chunk limits without indexing 2026-02-15 19:29:27 +00:00
Peter Steinberger
e3f4cabf49 perf(test): speed up update-cli unit tests 2026-02-15 19:29:27 +00:00
Peter Steinberger
a742d44133 perf(test): stub config + persistence in subagent registry tests 2026-02-15 19:29:27 +00:00
Peter Steinberger
b2088d2e1d perf(test): speed up process poll timeout tests 2026-02-15 19:29:27 +00:00
Peter Steinberger
88548784ce fix(bluebubbles): use Buffer for multipart body 2026-02-15 19:25:11 +00:00
Peter Steinberger
719280d737 refactor(bluebubbles): share multipart helpers 2026-02-15 19:24:03 +00:00
Peter Steinberger
de103773c7 refactor(tlon): share urbit poke/scry ops 2026-02-15 19:21:42 +00:00
Peter Steinberger
0653e8d2ec refactor(matrix): dedupe group config resolution 2026-02-15 19:21:37 +00:00
Peter Steinberger
699136f89a refactor(msteams): share credential prompt 2026-02-15 19:21:31 +00:00
Peter Steinberger
824901083b refactor(pi): dedupe compaction failure 2026-02-15 19:09:05 +00:00
Peter Steinberger
a2ceadcc2a refactor(gateway): dedupe assistant delta parsing 2026-02-15 19:08:47 +00:00
Peter Steinberger
5248b759fe refactor(shared): reuse isPidAlive 2026-02-15 19:06:54 +00:00
Xinhua Gu
c682634188 fix(discord): role-based allowlist never matches (Carbon Role objects stringify to mentions) (#16369)
* fix(discord): role-based allowlist never matches because Carbon Role objects stringify to mentions

Carbon's GuildMember.roles getter returns Role[] objects, not raw ID strings.
String(Role) produces '<@&123456>' which never matches the plain role IDs
in the guild allowlist config.

Use data.rawMember.roles (raw Discord API string array) instead of
data.member.roles (Carbon Role[] objects) for role ID extraction.

Fixes #16207

* Docs: add discord role allowlist changelog entry

---------

Co-authored-by: Shadow <hi@shadowing.dev>
2026-02-15 13:05:46 -06:00
Peter Steinberger
c7b6d6a14e refactor(plugins): reuse createEmptyPluginRegistry 2026-02-15 19:05:00 +00:00
Peter Steinberger
99fda7b920 refactor(models): share fallback command logic 2026-02-15 19:00:27 +00:00
Peter Steinberger
6a4144f537 refactor(auto-reply): dedupe chunk early returns 2026-02-15 18:55:01 +00:00
Peter Steinberger
9a5e617a55 fix(discord): align message action send parameters 2026-02-15 18:53:24 +00:00
Peter Steinberger
6f2f88d3ad refactor(status): reuse Requirements types 2026-02-15 18:50:36 +00:00
Peter Steinberger
c118f6c688 fix(discord): fix component parsing and modal field typing 2026-02-15 18:50:36 +00:00
Shadow
f92900fc20 Revert "Discord: add preflight role allowlist regression test"
This reverts commit 41f546faa5.
2026-02-15 12:45:46 -06:00
Shadow
99caaef6cc Revert "Docs: add discord role allowlist changelog entry"
This reverts commit 8678b10aef.
2026-02-15 12:45:46 -06:00
Peter Steinberger
137079fc21 refactor(shared): share entry requirements evaluation 2026-02-15 12:45:46 -06:00
Peter Steinberger
a5b87338e5 refactor(onboard): reuse applyAgentDefaultModelPrimary 2026-02-15 18:35:09 +00:00
Shadow
8678b10aef Docs: add discord role allowlist changelog entry 2026-02-15 12:33:31 -06:00
Shadow
41f546faa5 Discord: add preflight role allowlist regression test 2026-02-15 12:33:31 -06:00
Peter Steinberger
95c986dee1 refactor(models): share model picker auth checker 2026-02-15 18:32:18 +00:00
Peter Steinberger
d9c891eb90 refactor(channels): share threading tool context 2026-02-15 18:30:34 +00:00
Peter Steinberger
b2d8b95906 refactor(models): dedupe MiniMax provider models 2026-02-15 18:28:25 +00:00
Peter Steinberger
a2c695126d refactor(browser): reuse CDP fetch helpers 2026-02-15 18:27:02 +00:00
Peter Steinberger
394e69a2f8 refactor(cli): share browser resize output helper 2026-02-15 18:25:47 +00:00
Peter Steinberger
7ef956d224 refactor(browser): share client-actions url helpers 2026-02-15 18:22:10 +00:00
Peter Steinberger
7773c5410b refactor(telegram): share allowFrom normalization 2026-02-15 18:17:05 +00:00
Peter Steinberger
dce3e4bd94 refactor(cli): dedupe hook enable/disable logic 2026-02-15 18:14:03 +00:00
Peter Steinberger
65f8b46c15 fix(ci): stabilize media and session store tests 2026-02-15 18:12:15 +00:00
Peter Steinberger
01ca3da8ee refactor(gateway): share tailscale prompt constants 2026-02-15 18:06:48 +00:00
Peter Steinberger
2e758d3691 refactor(gateway): share node event sessionKey parsing 2026-02-15 18:02:55 +00:00
Peter Steinberger
be9b5cefbd fix(ci): stabilize state-dir dependent tests 2026-02-15 17:57:13 +00:00
Peter Steinberger
813b96a804 refactor(commands): share cleanup plan resolver 2026-02-15 17:49:30 +00:00
Peter Steinberger
1f1e97674f refactor(allowlists): share user entry collection 2026-02-15 17:45:16 +00:00
Peter Steinberger
04f00f8ef2 refactor(commands): share default model applier 2026-02-15 17:41:14 +00:00
Peter Steinberger
9084c4e345 refactor(pi): share session manager runtime registry 2026-02-15 17:39:21 +00:00
Shadow
c6b3736fe7 fix: dedupe probe/token base types (#16986) (thanks @iyoda) 2026-02-15 11:36:54 -06:00
Peter Steinberger
a0e763168f refactor(exec-approvals): share socket default merge 2026-02-15 17:36:08 +00:00
Peter Steinberger
5c88d3c9f1 refactor(media): share fileExists 2026-02-15 17:33:08 +00:00
Shadow
b6069fc68c feat: support per-channel ackReaction config (#17092) (thanks @zerone0x) 2026-02-15 11:30:25 -06:00
Peter Steinberger
b3ef3fca75 refactor(cron): share legacy delivery helpers 2026-02-15 17:29:08 +00:00
Peter Steinberger
25be51967a refactor(channels): share allowlist resolution summary 2026-02-15 17:26:27 +00:00
Peter Steinberger
63ab5bfddc refactor(discord): share component route + ack 2026-02-15 17:23:56 +00:00
Peter Steinberger
b74c3d80cc refactor(shared): dedupe chat content text extraction 2026-02-15 17:21:36 +00:00
Peter Steinberger
ac3db098ab refactor(discord): share component allowlist check 2026-02-15 17:17:03 +00:00
Peter Steinberger
b2c42697dd refactor(discord): reuse preflight param types 2026-02-15 17:14:54 +00:00
Peter Steinberger
cbf6ee3a64 refactor(models): share primary/fallback merge 2026-02-15 17:13:09 +00:00
Peter Steinberger
3ce0e80f57 refactor(commands): dedupe cleanup path resolution 2026-02-15 17:09:12 +00:00
Peter Steinberger
da2fde7b6f refactor(slack): share room context hints 2026-02-15 17:06:17 +00:00
Peter Steinberger
ca4c2b33d7 refactor(auto-reply): share mode-switch events 2026-02-15 17:03:02 +00:00
Peter Steinberger
9f393a045c fix(line): restore bot-message-context types 2026-02-15 16:58:52 +00:00
Peter Steinberger
1ab5fcc325 refactor(line): share source info parsing 2026-02-15 16:57:58 +00:00
Peter Steinberger
c906121ad3 fix(line): build config schema from common base 2026-02-15 16:55:35 +00:00
Peter Steinberger
fabe4807a6 refactor(line): dedupe config schema 2026-02-15 16:55:01 +00:00
Peter Steinberger
6e36d956d6 refactor(config): share agent model schema 2026-02-15 16:53:38 +00:00
Peter Steinberger
9143f33a80 refactor(tools): dedupe alsoAllow merge 2026-02-15 16:52:14 +00:00
Sebastian
b567ba5dfc fix(sandbox): allow registry entries without agent scope 2026-02-15 11:50:16 -05:00
Sebastian
6277698f86 test(discord): fix updated test harness mocks 2026-02-15 11:50:16 -05:00
Sebastian
10feda100e refactor(reply-tests): share harness mock bundle 2026-02-15 11:50:16 -05:00
Sebastian
2da512e24d refactor(agent): centralize fallback run helpers 2026-02-15 11:50:16 -05:00
Peter Steinberger
bf61d94083 refactor(cli): dedupe daemon install finalize 2026-02-15 16:49:38 +00:00
Peter Steinberger
08f16da8d7 refactor(config): dedupe bindings migrations 2026-02-15 16:47:06 +00:00
Peter Steinberger
fe303fc016 refactor(cli): reuse skill missing summary 2026-02-15 16:46:04 +00:00
Peter Steinberger
aa4d212a09 refactor(auto-reply): share cleared exec fields 2026-02-15 16:45:25 +00:00
Peter Steinberger
3783cd3850 refactor(plugins): share empty registry factory 2026-02-15 16:44:00 +00:00
Gustavo Madeira Santana
9adcccadb1 Outbound: scope core send media roots by agent (#17268)
Merged with gates skipped by maintainer request.

Prepared head SHA: 663ac49b3a
2026-02-15 11:43:02 -05:00
Peter Steinberger
b4f16001aa refactor(channels): dedupe discord channel lookup 2026-02-15 16:42:20 +00:00
Peter Steinberger
94eb50658d refactor(sessions): reuse session key classifier 2026-02-15 16:40:49 +00:00
Peter Steinberger
dda3026d13 refactor(line): dedupe schedule card header 2026-02-15 16:39:45 +00:00
Peter Steinberger
3a3bfa7f13 refactor(auto-reply): reuse exec directive clearer 2026-02-15 16:38:49 +00:00
Peter Steinberger
8da99247f1 refactor(routing): dedupe binding match parsing 2026-02-15 16:37:36 +00:00
Peter Steinberger
a767777598 refactor(skills): dedupe env overrides 2026-02-15 16:36:27 +00:00
Peter Steinberger
afa5444242 refactor(sandbox): dedupe sandbox list helpers 2026-02-15 16:35:37 +00:00
Peter Steinberger
5457f6e7e4 refactor(sandbox): dedupe prune loops 2026-02-15 16:33:57 +00:00
Peter Steinberger
d4476c6899 refactor(sandbox): dedupe session resolution 2026-02-15 16:32:51 +00:00
Peter Steinberger
d238483337 refactor(models): dedupe auth order context 2026-02-15 16:32:12 +00:00
Peter Steinberger
f4782e1e73 refactor(agents): dedupe session write lock release 2026-02-15 16:30:01 +00:00
Peter Steinberger
ac75cc3495 refactor(auto-reply): dedupe session touch 2026-02-15 16:27:14 +00:00
Peter Steinberger
c1bf99406f refactor(slack): dedupe onboarding token prompts 2026-02-15 16:26:11 +00:00
Peter Steinberger
910e1e52dd fix(models): type fallback key helper 2026-02-15 16:25:00 +00:00
Peter Steinberger
d4c7b0505f refactor(models): dedupe fallback key parsing 2026-02-15 16:25:00 +00:00
Shadow
9203a2fdb1 Discord: CV2! (#16364) 2026-02-15 10:24:53 -06:00
Peter Steinberger
95355ba25a refactor(agents): dedupe memory tool config 2026-02-15 16:22:59 +00:00
Peter Steinberger
e89c7b7735 refactor(infra): dedupe update checkout step 2026-02-15 16:22:06 +00:00
Peter Steinberger
6b65a055e6 refactor(telegram): dedupe media download 2026-02-15 16:22:06 +00:00
Garnet Liu
cc0bfa0f39 fix(telegram): restore thread_id=1 handling for DMs (regression from 19b8416a8) (openclaw#10942) thanks @garnetlyx
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm test:macmini

Co-authored-by: garnetlyx <12513503+garnetlyx@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-15 10:21:18 -06:00
Peter Steinberger
1843bcf1db refactor(gateway): share host header parsing 2026-02-15 16:15:53 +00:00
Peter Steinberger
933a9945ae refactor(telegram): dedupe group auth checks 2026-02-15 16:12:36 +00:00
Peter Steinberger
234d69f83f refactor(browser): dedupe request record lookup 2026-02-15 16:11:28 +00:00
Peter Steinberger
77db65d669 refactor(hooks): dedupe gmail option types 2026-02-15 16:10:17 +00:00
Peter Steinberger
c3340a3894 refactor(outbound): dedupe delivery mirror type 2026-02-15 16:09:21 +00:00
Peter Steinberger
41d053a06f refactor(discord): dedupe application fetch 2026-02-15 16:08:05 +00:00
Peter Steinberger
47462eed68 refactor(infra): share login shell env exec 2026-02-15 16:06:39 +00:00
Peter Steinberger
e7f65b4aac refactor(infra): dedupe exec allowlist analysis failure 2026-02-15 16:05:49 +00:00
Peter Steinberger
7323953ab0 refactor(gateway): share device signature reject path 2026-02-15 16:04:37 +00:00
Peter Steinberger
cd225c15be refactor(gateway): dedupe wizard status schema 2026-02-15 16:03:10 +00:00
Peter Steinberger
afc333cc5b refactor(slack): dedupe event system-event emit 2026-02-15 16:01:20 +00:00
Peter Steinberger
30eacd36af refactor(test): dedupe slack slash mocks 2026-02-15 15:57:33 +00:00
Mr. Guy
e927fd1e35 fix: allow agent workspace directories in media local roots (#17136)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7545ef1e19
Co-authored-by: MisterGuy420 <255743668+MisterGuy420@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-15 10:53:45 -05:00
Peter Steinberger
0c57f5e62e refactor(test): share google assistant message builders 2026-02-15 15:50:24 +00:00
Peter Steinberger
c6c6e9f741 refactor(test): share sandbox fs bridge builder 2026-02-15 15:47:55 +00:00
Rodrigo Uroz
df95ddc771 Fix/agent session key normalization (openclaw#15707) thanks @rodrigouroz
Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: rodrigouroz <384037+rodrigouroz@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-15 09:46:14 -06:00
Peter Steinberger
75d22b2164 refactor(test): dedupe cron legacy job setup 2026-02-15 15:46:00 +00:00
Peter Steinberger
e687ad15ac refactor(test): share server chat event harness 2026-02-15 15:44:14 +00:00
Peter Steinberger
e683353cab refactor(test): share corrupt session fixture 2026-02-15 15:42:23 +00:00
Peter Steinberger
2b143de554 refactor(test): dedupe ghost reminder assertions 2026-02-15 15:40:43 +00:00
Peter Steinberger
d979c6c089 refactor(test): simplify heartbeat model override tests 2026-02-15 15:36:58 +00:00
Peter Steinberger
ee331e8d55 refactor(test): share heartbeat sandbox 2026-02-15 15:35:24 +00:00
Marcus Widing
ade11ec892 fix(announce): use deterministic idempotency keys to prevent duplicate subagent announces (#17150)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 54bba3cea1
Co-authored-by: widingmarcus-cyber <245375637+widingmarcus-cyber@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-15 10:34:34 -05:00
Peter Steinberger
7ea14a1c87 refactor(test): share status transcript log writer 2026-02-15 15:32:29 +00:00
Sk Akram
1911942363 fix: make sensitive field whitelist case-insensitive (#16148)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: bb2d219e1f
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-15 10:31:48 -05:00
Rodrigo Uroz
6565ec2e53 gateway: return actionable error for send channel webchat (openclaw#15703) thanks @rodrigouroz
Verified:
- pnpm build
- pnpm check (fails on current main with unrelated type errors in src/memory/embedding-manager.test-harness.ts)
- pnpm test:macmini (not run after pnpm check failure)
- pnpm test -- src/gateway/server-methods/send.test.ts

Co-authored-by: rodrigouroz <165576107+rodrigouroz@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-15 09:31:11 -06:00
Peter Steinberger
3d38e56401 refactor(test): dedupe hook transform skip assertions 2026-02-15 15:30:37 +00:00
Gustavo Madeira Santana
2e64cbd1b8 chore(memory): tighten embedding harness types 2026-02-15 10:30:19 -05:00
Gustavo Madeira Santana
88caa4b50c chore(cron): simplify enabled checks for lint 2026-02-15 10:30:19 -05:00
Peter Steinberger
fa4c282f9e refactor(test): dedupe models list provider filter cases 2026-02-15 15:29:00 +00:00
Peter Steinberger
88cac5985e refactor(test): dedupe update runner stable command mocks 2026-02-15 15:27:47 +00:00
Peter Steinberger
0f4036b0f6 refactor(test): share line auto-reply deps 2026-02-15 15:26:17 +00:00
misterdas
c211fd112c fix(subagents): add model fallback support to sessions_spawn tool (#17197)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 5d20c2cd37
Co-authored-by: misterdas <170702047+misterdas@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-15 10:25:47 -05:00
Peter Steinberger
75f3b5069b refactor(test): dedupe telegram bot mention scaffolding 2026-02-15 15:24:40 +00:00
Peter Steinberger
831fb0aea3 refactor(test): dedupe model directive persist setup 2026-02-15 15:22:50 +00:00
Peter Steinberger
7ecc105c3d refactor(test): dedupe monitor inbox quoted reply checks 2026-02-15 15:20:31 +00:00
Peter Steinberger
4f8a2ed2ce refactor(test): dedupe telegram dispatch scaffolding 2026-02-15 15:19:10 +00:00
Peter Steinberger
53ffc309f3 refactor(test): simplify onboarding wizard scaffolding 2026-02-15 15:16:55 +00:00
Peter Steinberger
3e7800befb refactor(test): dedupe onboarding gateway prompter 2026-02-15 15:15:19 +00:00
Peter Steinberger
e2f73650d4 refactor(test): share signal receive harness 2026-02-15 15:14:34 +00:00
Rodrigo Uroz
89dccc79a7 cron: infer payload kind for model-only update patches (openclaw#15664) thanks @rodrigouroz
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check (fails on current origin/main in src/memory/embedding-manager.test-harness.ts; unchanged by this PR)

Co-authored-by: rodrigouroz <384037+rodrigouroz@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-15 09:12:51 -06:00
Peter Steinberger
3c97ec70d1 refactor(test): dedupe followup queue test setup 2026-02-15 15:11:34 +00:00
Peter Steinberger
beffb6fe48 refactor(test): dedupe session-memory hook setup 2026-02-15 15:09:26 +00:00
Peter Steinberger
71c1d09f22 refactor(test): share memory embedding fixture 2026-02-15 15:07:09 +00:00
Peter Steinberger
fe27215747 refactor(test): share web broadcast-groups harness 2026-02-15 15:03:47 +00:00
Ayaan Zaidi
86df160617 fix: telegram stream preview finalizes in place (#17218) (thanks @obviyus) 2026-02-15 20:32:51 +05:30
Ayaan Zaidi
a69e82765f fix(telegram): stream replies in-place without duplicate final sends 2026-02-15 20:32:51 +05:30
Peter Steinberger
8b2a5672be refactor(test): reuse command test harness 2026-02-15 15:01:00 +00:00
Peter Steinberger
d3d82a1c19 refactor(test): share google-shared test helpers 2026-02-15 14:57:15 +00:00
Gustavo Madeira Santana
bd9d35c720 chore: remove defensive logic 2026-02-15 09:54:04 -05:00
Peter Steinberger
723e314e2b fix(ci): avoid vitest TDZ in shared mocks 2026-02-15 14:52:41 +00:00
nathandenherder
878a13d215 fix: don't consume replyPlan reference eagerly for streaming check
The streaming check was calling replyPlan.nextThreadTs() at setup time
to determine if a thread_ts existed, which consumed the first reference
before the deliver callback ran. Use incomingThreadTs/statusThreadTs
directly for the streaming eligibility check instead.
2026-02-07 15:03:12 -05:00
nathandenherder
06efbd231f fix: resolve ChatStreamer import path and TypeScript narrowing issue
- Import ChatStreamer from @slack/web-api/dist/chat-stream.js (not re-exported from index)
- Fix TypeScript control flow narrowing for streamSession used in closure
2026-02-07 15:03:12 -05:00
nathandenherder
6945fbf100 feat(slack): add native text streaming support
Adds support for Slack's Agents & AI Apps text streaming APIs
(chat.startStream, chat.appendStream, chat.stopStream) to deliver
LLM responses as a single updating message instead of separate
messages per block.

Changes:
- New src/slack/streaming.ts with stream lifecycle helpers using
  the SDK's ChatStreamer (client.chatStream())
- New 'streaming' config option on SlackAccountConfig
- Updated dispatch.ts to route block replies through the stream
  when enabled, with graceful fallback to normal delivery
- Docs in docs/channels/slack.md covering setup and requirements

The streaming integration works by intercepting the deliver callback
in the reply dispatcher. When streaming is enabled and a thread
context exists, the first text delivery starts a stream, subsequent
deliveries append to it, and the stream is finalized after dispatch
completes. Media payloads and error cases fall back to normal
message delivery.

Refs:
- https://docs.slack.dev/ai/developing-ai-apps#streaming
- https://docs.slack.dev/reference/methods/chat.startStream
- https://docs.slack.dev/reference/methods/chat.appendStream
- https://docs.slack.dev/reference/methods/chat.stopStream
2026-02-07 15:03:12 -05:00
2976 changed files with 159575 additions and 85760 deletions

View File

@@ -110,7 +110,7 @@ Before any substantive review or prep work, **always rebase the PR branch onto c
- During `prepare-pr`, use concise, action-oriented subjects **without** PR numbers or thanks; reserve `(#<PR>) thanks @<pr-author>` for the final merge/squash commit.
- Group related changes; avoid bundling unrelated refactors.
- Changelog workflow: keep the latest released version at the top (no `Unreleased`); after publishing, bump the version and start a new top section.
- When working on a PR: add a changelog entry with the PR number and thank the contributor (mandatory in this workflow).
- When working on a PR: add a changelog entry line with the PR number `(#<PR>)` and `thanks @<pr-author>` when author metadata is available (mandatory in this workflow).
- When working on an issue: reference the issue in the changelog entry.
- In this workflow, changelog is always required even for internal/test-only changes.

View File

@@ -41,11 +41,12 @@ scripts/pr-merge <PR>
scripts/pr-merge run <PR>
```
3. Ensure output reports:
3. Capture and report these values in a human-readable summary (not raw `key=value` lines):
- `merge_sha=<sha>`
- `merge_author_email=<email>`
- `comment_url=<url>`
- Merge commit SHA
- Merge author email
- Merge completion comment URL
- PR URL
## Steps
@@ -97,3 +98,4 @@ Cleanup is handled by `run` after merge success.
- End in `MERGED`, never `CLOSED`.
- Cleanup only after confirmed merge.
- In final chat output, use labeled lines or bullets; do not paste raw wrapper diagnostics unless debugging.

View File

@@ -74,6 +74,11 @@ jq -r '.changelog' .local/review.json
jq -r '.docs' .local/review.json
```
Changelog gate requirement:
- `CHANGELOG.md` must include a newly added changelog entry line.
- When PR author metadata is available, that same changelog entry line must include `(#<PR>) thanks @<pr-author>`.
4. Commit scoped changes
Use concise, action-oriented subject lines without PR numbers/thanks. The final merge/squash commit is the only place we include PR numbers and contributor thanks.

View File

@@ -37,6 +37,16 @@ OPENCLAW_GATEWAY_TOKEN=change-me-to-a-long-random-token
# ANTHROPIC_API_KEY=sk-ant-...
# GEMINI_API_KEY=...
# OPENROUTER_API_KEY=sk-or-...
# OPENCLAW_LIVE_OPENAI_KEY=sk-...
# OPENCLAW_LIVE_ANTHROPIC_KEY=sk-ant-...
# OPENCLAW_LIVE_GEMINI_KEY=...
# OPENAI_API_KEY_1=...
# ANTHROPIC_API_KEY_1=...
# GEMINI_API_KEY_1=...
# GOOGLE_API_KEY=...
# OPENAI_API_KEYS=sk-1,sk-2
# ANTHROPIC_API_KEYS=sk-ant-1,sk-ant-2
# GEMINI_API_KEYS=key-1,key-2
# Optional additional providers
# ZAI_API_KEY=...

View File

@@ -52,7 +52,7 @@ runs:
if: inputs.install-bun == 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: "1.3.9"
- name: Runtime versions
shell: bash

View File

@@ -13,7 +13,7 @@ jobs:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
id: app-token
@@ -48,7 +48,7 @@ jobs:
label: "r: third-party-extension",
close: true,
message:
"This would be better made as a third-party extension with our SDK that you maintain yourself. Docs: https://docs.openclaw.ai/plugin.",
"Please make this as a third-party plugin that you maintain yourself in your own repo. Docs: https://docs.openclaw.ai/plugin. Feel free to open a PR after to add it to our community plugins page: https://docs.openclaw.ai/plugins/community",
},
{
label: "r: moltbook",

View File

@@ -6,14 +6,14 @@ on:
pull_request:
concurrency:
group: ci-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# Detect docs-only changes to skip heavy jobs (test, build, Windows, macOS, Android).
# Lint and format always run. Fail-safe: if detection fails, run everything.
docs-scope:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
docs_only: ${{ steps.check.outputs.docs_only }}
docs_changed: ${{ steps.check.outputs.docs_changed }}
@@ -33,7 +33,7 @@ jobs:
changed-scope:
needs: [docs-scope]
if: needs.docs-scope.outputs.docs_only != 'true'
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
run_node: ${{ steps.scope.outputs.run_node }}
run_macos: ${{ steps.scope.outputs.run_macos }}
@@ -204,6 +204,14 @@ jobs:
if: matrix.task == 'test' && matrix.runtime == 'node'
run: echo "OPENCLAW_VITEST_REPORT_DIR=$RUNNER_TEMP/vitest-reports" >> "$GITHUB_ENV"
- name: Configure Node test resources
if: matrix.task == 'test' && matrix.runtime == 'node'
run: |
# `pnpm test` runs `scripts/test-parallel.mjs`, which spawns multiple Node processes.
# Default heap limits have been too low on Linux CI (V8 OOM near 4GB).
echo "OPENCLAW_TEST_WORKERS=2" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144" >> "$GITHUB_ENV"
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
run: ${{ matrix.command }}
@@ -236,6 +244,8 @@ jobs:
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Check types and lint and oxfmt
run: pnpm check
@@ -253,6 +263,8 @@ jobs:
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Check docs
run: pnpm check:docs
@@ -361,7 +373,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: "1.3.9"
- name: Runtime versions
run: |
@@ -664,7 +676,8 @@ jobs:
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
# setup-android's sdkmanager currently crashes on JDK 21 in CI.
java-version: 17
- name: Setup Android SDK
uses: android-actions/setup-android@v3

View File

@@ -13,6 +13,10 @@ on:
- ".agents/**"
- "skills/**"
concurrency:
group: docker-release-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

View File

@@ -1,138 +0,0 @@
name: Formal models (informational conformance)
on:
pull_request:
concurrency:
group: formal-conformance-${{ github.event.pull_request.number || github.ref_name }}
cancel-in-progress: true
jobs:
formal_conformance:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout openclaw (PR)
uses: actions/checkout@v4
with:
path: openclaw
- name: Checkout formal models
uses: actions/checkout@v4
with:
repository: vignesh07/clawdbot-formal-models
ref: main
path: clawdbot-formal-models
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Regenerate extracted constants from openclaw
run: |
set -euo pipefail
cd clawdbot-formal-models
export OPENCLAW_REPO_DIR="${GITHUB_WORKSPACE}/openclaw"
node scripts/extract-tool-groups.mjs
node scripts/check-tool-group-alias.mjs
# Drift is about extracted artifacts only; compute it before model checking
# to avoid any incidental file touches affecting the result.
- name: Compute drift (generated/*)
id: drift
run: |
set -euo pipefail
cd clawdbot-formal-models
if git diff --quiet -- generated; then
echo "drift=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "drift=true" >> "$GITHUB_OUTPUT"
git diff -- generated > "${GITHUB_WORKSPACE}/formal-models-drift.diff"
- name: Model check (green suite)
run: |
set -euo pipefail
cd clawdbot-formal-models
make \
precedence groups elevated nodes-policy \
attacker approvals approvals-token nodes-pipeline \
gateway-exposure gateway-exposure-v2 gateway-exposure-v2-protected \
gateway-auth-conformance gateway-auth-tailscale gateway-auth-proxy \
pairing pairing-cap pairing-idempotency pairing-refresh pairing-refresh-race \
ingress-gating ingress-idempotency ingress-dedupe-fallback ingress-trace ingress-trace2 \
routing-isolation routing-precedence routing-identitylinks routing-identity-transitive routing-identity-symmetry routing-identity-channel-override \
routing-thread-parent discord-pluralkit \
ingress-retry session-key-stability session-explosion-bound config-normalization \
queue-drain delivery-route-stability delivery-pipeline retry-termination retry-eventual-success \
no-cross-stream multi-event-eventual-emission \
dedupe-collision-fallback crash-restart-dedupe two-worker-dedupe openclaw-session-key-conformance \
routing-thread-parent-channel-override routing-trirule gateway-auth-proxy-header-spoof \
group-alias-check
- name: Model check (negative suite, expected violations)
continue-on-error: true
run: |
set -euo pipefail
cd clawdbot-formal-models
make -k \
precedence-negative groups-negative elevated-negative nodes-policy-negative \
attacker-negative attacker-nodes-negative attacker-nodes-allowlist-negative attacker-nodes-allowlist-negative \
approvals-negative approvals-token-negative nodes-pipeline-negative \
gateway-exposure-negative gateway-exposure-v2-negative gateway-exposure-v2-protected-negative \
gateway-exposure-v2-unsafe-custom gateway-exposure-v2-unsafe-tailnet gateway-exposure-v2-unsafe-auto \
gateway-auth-conformance-negative gateway-auth-tailscale-negative gateway-auth-proxy-negative \
pairing-negative pairing-cap-negative pairing-idempotency-negative pairing-refresh-negative pairing-refresh-race-negative \
ingress-gating-negative ingress-idempotency-negative ingress-dedupe-fallback-negative ingress-trace-negative ingress-trace2-negative \
routing-isolation-negative routing-precedence-negative routing-identitylinks-negative routing-identity-transitive-negative routing-identity-symmetry-negative routing-identity-channel-override-negative \
routing-thread-parent-negative discord-pluralkit-negative \
ingress-retry-negative session-key-stability-negative config-normalization-negative \
queue-drain delivery-route-stability-negative delivery-pipeline-negative retry-termination-negative retry-eventual-success-negative \
no-cross-stream-negative multi-event-eventual-emission-negative \
dedupe-collision-fallback-negative crash-restart-dedupe-negative two-worker-dedupe-negative openclaw-session-key-conformance-negative \
routing-thread-parent-channel-override-negative routing-trirule-negative gateway-auth-proxy-header-spoof-negative
- name: Upload drift diff artifact
if: steps.drift.outputs.drift == 'true'
uses: actions/upload-artifact@v4
with:
name: formal-models-conformance-drift
path: formal-models-drift.diff
- name: Comment on PR (informational)
if: steps.drift.outputs.drift == 'true'
uses: actions/github-script@v7
with:
script: |
const body = [
'⚠️ **Formal models conformance drift detected**',
'',
'The formal models extracted constants (`generated/*`) do not match this openclaw PR.',
'',
'This check is **informational** (not blocking merges yet).',
'See the `formal-models-conformance-drift` artifact for the diff.',
'',
'If this change is intentional, follow up by updating the formal models repo or regenerating the extracted artifacts there.',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body,
});
- name: Summary
run: |
if [ "${{ steps.drift.outputs.drift }}" = "true" ]; then
echo "Formal conformance drift detected (informational)."
else
echo "Formal conformance: no drift."
fi

View File

@@ -7,8 +7,8 @@ on:
workflow_dispatch:
concurrency:
group: install-smoke-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
group: install-smoke-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
docs-scope:
@@ -33,19 +33,17 @@ jobs:
- name: Checkout CLI
uses: actions/checkout@v4
- name: Setup pnpm (corepack retry)
run: |
set -euo pipefail
corepack enable
for attempt in 1 2 3; do
if corepack prepare pnpm@10.23.0 --activate; then
pnpm -v
exit 0
fi
echo "corepack prepare failed (attempt $attempt/3). Retrying..."
sleep $((attempt * 10))
done
exit 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
check-latest: true
- name: Setup pnpm + cache store
uses: ./.github/actions/setup-pnpm-store-cache
with:
pnpm-version: "10.23.0"
cache-key-suffix: "node22"
- name: Install pnpm deps (minimal)
run: pnpm install --ignore-scripts --frozen-lockfile

View File

@@ -23,7 +23,7 @@ jobs:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
id: app-token
@@ -200,7 +200,7 @@ jobs:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
id: app-token
@@ -440,7 +440,7 @@ jobs:
label-issues:
permissions:
issues: write
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
id: app-token

View File

@@ -14,8 +14,8 @@ on:
- scripts/sandbox-common-setup.sh
concurrency:
group: sandbox-common-smoke-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
group: sandbox-common-smoke-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
sandbox-common-smoke:

View File

@@ -12,7 +12,7 @@ jobs:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
id: app-token
@@ -31,7 +31,7 @@ jobs:
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
exempt-pr-labels: maintainer,no-stale
operations-per-run: 500
operations-per-run: 10000
exempt-all-assignees: true
remove-stale-when-updated: true
stale-issue-message: |

View File

@@ -6,8 +6,8 @@ on:
branches: [main]
concurrency:
group: workflow-sanity-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
group: workflow-sanity-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
no-tabs:

8
.gitignore vendored
View File

@@ -27,6 +27,8 @@ apps/android/.cxx/
*.bun-build
apps/macos/.build/
apps/shared/MoltbotKit/.build/
apps/shared/OpenClawKit/.build/
apps/shared/OpenClawKit/Package.resolved
**/ModuleCache/
bin/
bin/clawdbot-mac
@@ -34,10 +36,13 @@ bin/docs-list
apps/macos/.build-local/
apps/macos/.swiftpm/
apps/shared/MoltbotKit/.swiftpm/
apps/shared/OpenClawKit/.swiftpm/
Core/
apps/ios/*.xcodeproj/
apps/ios/*.xcworkspace/
apps/ios/.swiftpm/
apps/ios/.derivedData/
apps/ios/.local-signing.xcconfig
vendor/
apps/ios/Clawdbot.xcodeproj/
apps/ios/Clawdbot.xcodeproj/**
@@ -84,3 +89,6 @@ USER.md
!.agent/workflows/
/local/
package-lock.json
# Local iOS signing overrides
apps/ios/LocalSigning.xcconfig

View File

@@ -14,6 +14,7 @@
"node_modules/",
"patches/",
"pnpm-lock.yaml/",
"src/auto-reply/reply/export-html/",
"Swabble/",
"vendor/",
],

View File

@@ -11,6 +11,8 @@
"eslint-plugin-unicorn/prefer-array-find": "off",
"eslint/no-await-in-loop": "off",
"eslint/no-new": "off",
"eslint/no-shadow": "off",
"eslint/no-unmodified-loop-condition": "off",
"oxc/no-accumulating-spread": "off",
"oxc/no-async-endpoint-handlers": "off",
"oxc/no-map-spread": "off",
@@ -27,8 +29,9 @@
"extensions/",
"node_modules/",
"patches/",
"pnpm-lock.yaml/",
"pnpm-lock.yaml",
"skills/",
"src/auto-reply/reply/export-html/template.js",
"src/canvas-host/a2ui/a2ui.bundle.js",
"Swabble/",
"vendor/"

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,10 @@
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
- Formatting/linting via Oxlint and Oxfmt; run `pnpm check` before commits.
- Never add `@ts-nocheck` and do not disable `no-explicit-any`; fix root causes and update Oxlint/Oxfmt config only when required.
- Never share class behavior via prototype mutation (`applyPrototypeMixins`, `Object.defineProperty` on `.prototype`, or exporting `Class.prototype` for merges). Use explicit inheritance/composition (`A extends B extends C`) or helper composition so TypeScript can typecheck.
- If this pattern is needed, stop and get explicit approval before shipping; default behavior is to split/refactor into an explicit class hierarchy and keep members strongly typed.
- In tests, prefer per-instance stubs over prototype mutation (`SomeClass.prototype.method = ...`) unless a test explicitly documents why prototype-level patching is required.
- Add brief code comments for tricky or non-obvious logic.
- Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`.
- Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability.
@@ -110,6 +114,7 @@
## Git Notes
- If `git branch -d/-D <branch>` is policy-blocked, delete the local ref directly: `git update-ref -d refs/heads/<branch>`.
- Bulk PR close/reopen safety: if a close action would affect more than 5 PRs, first ask for explicit user confirmation with the exact PR count and target scope/query.
## Security & Configuration Tips
@@ -119,6 +124,19 @@
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
- Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them.
## GHSA (Repo Advisory) Patch/Publish
- Fetch: `gh api /repos/openclaw/openclaw/security-advisories/<GHSA>`
- Latest npm: `npm view openclaw version --userconfig "$(mktemp)"`
- Private fork PRs must be closed:
`fork=$(gh api /repos/openclaw/openclaw/security-advisories/<GHSA> | jq -r .private_fork.full_name)`
`gh pr list -R "$fork" --state open` (must be empty)
- Description newline footgun: write Markdown via heredoc to `/tmp/ghsa.desc.md` (no `"\\n"` strings)
- Build patch JSON via jq: `jq -n --rawfile desc /tmp/ghsa.desc.md '{summary,severity,description:$desc,vulnerabilities:[...]}' > /tmp/ghsa.patch.json`
- Patch + publish: `gh api -X PATCH /repos/openclaw/openclaw/security-advisories/<GHSA> --input /tmp/ghsa.patch.json` (publish = include `"state":"published"`; no `/publish` endpoint)
- If publish fails (HTTP 422): missing `severity`/`description`/`vulnerabilities[]`, or private fork has open PRs
- Verify: re-fetch; ensure `state=published`, `published_at` set; `jq -r .description | rg '\\\\n'` returns nothing
## Troubleshooting
- Rebrand/migration issues or legacy config/service warnings: run `openclaw doctor` (see `docs/gateway/doctor.md`).
@@ -182,3 +200,39 @@
- Publish: `npm publish --access public --otp="<otp>"` (run from the package dir).
- Verify without local npmrc side effects: `npm view <pkg> version --userconfig "$(mktemp)"`.
- Kill the tmux session after publish.
## Plugin Release Fast Path (no core `openclaw` publish)
- Release only already-on-npm plugins. Source list is in `docs/reference/RELEASING.md` under "Current npm plugin list".
- Run all CLI `op` calls and `npm publish` inside tmux to avoid hangs/interruption:
- `tmux new -d -s release-plugins-$(date +%Y%m%d-%H%M%S)`
- `eval "$(op signin --account my.1password.com)"`
- 1Password helpers:
- password used by `npm login`:
`op item get Npmjs --format=json | jq -r '.fields[] | select(.id=="password").value'`
- OTP:
`op read 'op://Private/Npmjs/one-time password?attribute=otp'`
- Fast publish loop (local helper script in `/tmp` is fine; keep repo clean):
- compare local plugin `version` to `npm view <name> version`
- only run `npm publish --access public --otp="<otp>"` when versions differ
- skip if package is missing on npm or version already matches.
- Keep `openclaw` untouched: never run publish from repo root unless explicitly requested.
- Post-check for each release:
- per-plugin: `npm view @openclaw/<name> version --userconfig "$(mktemp)"` should be `2026.2.17`
- core guard: `npm view openclaw version --userconfig "$(mktemp)"` should stay at previous version unless explicitly requested.
## Changelog Release Notes
- When cutting a mac release with beta GitHub prerelease:
- Tag `vYYYY.M.D-beta.N` from the release commit (example: `v2026.2.15-beta.1`).
- Create prerelease with title `openclaw YYYY.M.D-beta.N`.
- Use release notes from `CHANGELOG.md` version section (`Changes` + `Fixes`, no title duplicate).
- Attach at least `OpenClaw-YYYY.M.D.zip` and `OpenClaw-YYYY.M.D.dSYM.zip`; include `.dmg` if available.
- Keep top version entries in `CHANGELOG.md` sorted by impact:
- `### Changes` first.
- `### Fixes` deduped and ranked with user-facing fixes first.
- Before tagging/publishing, run:
- `node --import tsx scripts/release-check.ts`
- `pnpm release:check`
- `pnpm test:install:smoke` or `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke` for non-root smoke path.

View File

@@ -2,28 +2,287 @@
Docs: https://docs.openclaw.ai
## 2026.2.15 (Unreleased)
## 2026.2.18 (Unreleased)
### Changes
- Skills/Security: defense-in-depth security hardening for community skills (ClawHub installs). Adds capability declarations (`shell`, `filesystem`, `network`, `browser`, `sessions`), trust tier classification (builtin/verified/community/local), SKILL.md content scanning (blocks prompt injection, capability inflation, boundary spoofing), skill-aware tool policy enforcement (denies undeclared dangerous tools for community skills), command-dispatch gating, and before-tool-call audit monitoring with session context. Community skills that fail critical scanning are blocked from loading. `openclaw skills list/info/check` now show capabilities, trust tiers, scan results, and runtime policy.
- Skills/Logging: all security-related log entries tagged with `category: "security"` for filtering. Skills CLI commands output structured JSON to the file logger (no more ASCII tables in logs). Web UI Logs tab adds a "Security" filter chip for security-only event views.
- Subagents: nested sub-agents (sub-sub-agents) with configurable depth. Set `agents.defaults.subagents.maxSpawnDepth: 2` to allow sub-agents to spawn their own children. Includes `maxChildrenPerAgent` limit (default 5), depth-aware tool policy, and proper announce chain routing. (#14447) Thanks @tyler6204.
- iOS/Gateway: wake disconnected iOS nodes via APNs before `nodes.invoke` and auto-reconnect gateway sessions on silent push wake to reduce invoke failures while the app is backgrounded. (#20332) Thanks @mbelinky.
- iOS/APNs: add push registration and notification-signing configuration for node delivery. (#20308) Thanks @mbelinky.
- Gateway/APNs: add a push-test pipeline for APNs delivery validation in gateway flows. (#20307) Thanks @mbelinky.
- iOS/Watch: add an Apple Watch companion MVP with watch inbox UI, watch notification relay handling, and gateway command surfaces for watch status/send flows. (#20054) Thanks @mbelinky.
- Gateway/CLI: add paired-device hygiene flows with `device.pair.remove`, plus `openclaw devices remove` and guarded `openclaw devices clear --yes [--pending]` commands for removing paired entries and optionally rejecting pending requests. (#20057) Thanks @mbelinky.
- Skills: harden coding-agent skill guidance by removing shell-command examples that interpolate untrusted issue text directly into command strings.
- Auto-reply/UI: add model fallback lifecycle visibility in verbose logs, /status active-model context with fallback reason, and cohesive WebUI fallback indicators. (#20704) thanks @joshavant.
### Fixes
- Agents/Streaming: keep assistant partial streaming active during reasoning streams, handle native `thinking_*` stream events consistently, dedupe mixed reasoning-end signals, and clear stale mutating tool errors after same-target retry success. (#20635) Thanks @obviyus.
- Gateway/Auth: default unresolved gateway auth to token mode with startup auto-generation/persistence of `gateway.auth.token`, while allowing explicit `gateway.auth.mode: "none"` for intentional open loopback setups. (#20686) thanks @gumadeiras.
- Browser/Relay: require gateway-token auth on both `/extension` and `/cdp`, and align Chrome extension setup to use a single `gateway.auth.token` input for relay authentication. Thanks @tdjackey for reporting.
- Gateway/Hooks: run BOOT.md startup checks per configured agent scope, including per-agent session-key resolution, startup-hook regression coverage, and non-success boot outcome logging for diagnosability. (#20569) thanks @mcaxtr.
- Telegram: unify message-like inbound handling so `message` and `channel_post` share the same dedupe/access/media pipeline and remain behaviorally consistent. (#20591) Thanks @obviyus.
- Heartbeat/Cron: skip interval heartbeats when `HEARTBEAT.md` is missing or empty and no tagged cron events are queued, while preserving cron-event fallback for queued tagged reminders. (#20461) thanks @vikpos.
- Telegram/Agents: gate exec/bash tool-failure warnings behind verbose mode so default Telegram replies stay clean while verbose sessions still surface diagnostics. (#20560) Thanks @obviyus.
- Gateway/Daemon: forward `TMPDIR` into installed service environments so macOS LaunchAgent gateway runs can open SQLite temp/journal files reliably instead of failing with `SQLITE_CANTOPEN`. (#20512) Thanks @Clawborn.
- Agents/Billing: include the active model that produced a billing error in user-facing billing messages (for example, `OpenAI (gpt-5.3)`) across payload, failover, and lifecycle error paths, so users can identify exactly which key needs credits. (#20510) Thanks @echoVic.
- iOS/Screen: move `WKWebView` lifecycle ownership into `ScreenWebView` coordinator and explicit attach/detach flow to reduce gesture/lifecycle crash risk (`__NSArrayM insertObject:atIndex:` paths) during screen tab updates. (#20366) Thanks @ngutman.
- Gateway/TUI: honor `agents.defaults.blockStreamingDefault` for `chat.send` by removing the hardcoded block-streaming disable override, so replies can use configured block-mode delivery. (#19693) Thanks @neipor.
- Protocol/Apple: regenerate Swift gateway models for `push.test` so `pnpm protocol:check` stays green on main. Thanks @mbelinky.
- Canvas/A2UI: improve bundled-asset resolution and empty-state handling so UI fallbacks render reliably. (#20312) Thanks @mbelinky.
- UI/Sessions: accept the canonical main session-key alias in Chat UI flows so main-session routing stays consistent. (#20311) Thanks @mbelinky.
- iOS/Onboarding: prevent pairing-status flicker during auto-resume by keeping resumed state transitions stable. (#20310) Thanks @mbelinky.
- OpenClawKit/Protocol: preserve JSON boolean literals (`true`/`false`) when bridging through `AnyCodable` so Apple client RPC params no longer re-encode booleans as `1`/`0`. Thanks @mbelinky.
- iOS/Onboarding: stabilize pairing and reconnect behavior by resetting stale pairing request state on manual retry, disconnecting both operator and node gateways on operator failure, and avoiding duplicate pairing loops from operator transport identity attachment. (#20056) Thanks @mbelinky.
- Browser/Relay: reuse an already-running extension relay when the relay port is occupied by another OpenClaw process, while still failing on non-relay port collisions to avoid masking unrelated listeners. (#20035) Thanks @mbelinky.
- Telegram/Cron/Heartbeat: honor explicit Telegram topic targets in cron and heartbeat delivery (`<chatId>:topic:<threadId>`) so scheduled sends land in the configured topic instead of the last active thread. (#19367) Thanks @Lukavyi.
- iOS/Signing: restore local auto-selected signing-team overrides during iOS project generation by wiring `.local-signing.xcconfig` into the active signing config and emitting `OPENCLAW_DEVELOPMENT_TEAM` in local signing setup. (#19993) Thanks @ngutman.
- Commands/Doctor: avoid rewriting invalid configs with new `gateway.auth.token` defaults during repair and only write when real config changes are detected, preventing accidental token duplication and backup churn.
- Sandbox/Registry: serialize container and browser registry writes with shared file locks and atomic replacement to prevent lost updates and delete rollback races from desyncing `sandbox list`, `prune`, and `recreate --all`. Thanks @kexinoh.
- Security/Exec: require `tools.exec.safeBins` binaries to resolve from trusted bin directories (system defaults plus gateway startup `PATH`) so PATH-hijacked trojan binaries cannot bypass allowlist checks. Thanks @jackhax for reporting.
- Cron/Webhooks: protect cron webhook POST delivery with SSRF-guarded outbound fetch (`fetchWithSsrFGuard`) to block private/metadata destinations before request dispatch. Thanks @Adam55A-code.
- Security/Net: block SSRF bypass via NAT64 (`64:ff9b::/96`, `64:ff9b:1::/48`), 6to4 (`2002::/16`), and Teredo (`2001:0000::/32`) IPv6 transition addresses, and fail closed on IPv6 parse errors. Thanks @jackhax.
## 2026.2.17
### Changes
- Agents/Anthropic: add opt-in 1M context beta header support for Opus/Sonnet via model `params.context1m: true` (maps to `anthropic-beta: context-1m-2025-08-07`).
- Agents/Models: support Anthropic Sonnet 4.6 (`anthropic/claude-sonnet-4-6`) across aliases/defaults with forward-compat fallback when upstream catalogs still only expose Sonnet 4.5.
- Commands/Subagents: add `/subagents spawn` for deterministic subagent activation from chat commands. (#18218) Thanks @JoshuaLelon.
- Agents/Subagents: add an accepted response note for `sessions_spawn` explaining polling subagents are disabled for one-off calls. Thanks @tyler6204.
- Agents/Subagents: prefix spawned subagent task messages with context to preserve source information in downstream handling. Thanks @tyler6204.
- iOS/Share: add an iOS share extension that forwards shared URL/text/image content directly to gateway `agent.request`, with delivery-route fallback and optional receipt acknowledgements. (#19424) Thanks @mbelinky.
- iOS/Talk: add a `Background Listening` toggle that keeps Talk Mode active while the app is backgrounded (off by default for battery safety). Thanks @zeulewan.
- iOS/Talk: add a `Voice Directive Hint` toggle for Talk Mode prompts so users can disable ElevenLabs voice-switching instructions to save tokens when not needed. (#18250) Thanks @zeulewan.
- iOS/Talk: harden barge-in behavior by disabling interrupt-on-speech when output route is built-in speaker/receiver, reducing false interruptions from local TTS bleed-through. Thanks @zeulewan.
- Slack: add native single-message text streaming with Slack `chat.startStream`/`appendStream`/`stopStream`; keep reply threading aligned with `replyToMode`, default streaming to enabled, and fall back to normal delivery when streaming fails. (#9972) Thanks @natedenh.
- Slack: add configurable streaming modes for draft previews. (#18555) Thanks @Solvely-Colin.
- Telegram/Agents: add inline button `style` support (`primary|success|danger`) across message tool schema, Telegram action parsing, send pipeline, and runtime prompt guidance. (#18241) Thanks @obviyus.
- Telegram: surface user message reactions as system events, with configurable `channels.telegram.reactionNotifications` scope. (#10075) Thanks @Glucksberg.
- iMessage: support `replyToId` on outbound text/media sends and normalize leading `[[reply_to:<id>]]` tags so replies target the intended iMessage. Thanks @tyler6204.
- Tool Display/Web UI: add intent-first tool detail views and exec summaries. (#18592) Thanks @xdLawless2.
- Discord: expose native `/exec` command options (host/security/ask/node) so Discord slash commands get autocomplete and structured inputs. Thanks @thewilloftheshadow.
- Discord: allow reusable interactive components with `components.reusable=true` so buttons, selects, and forms can be used multiple times before expiring. Thanks @thewilloftheshadow.
- Discord: add per-button `allowedUsers` allowlist for interactive components to restrict who can click buttons. Thanks @thewilloftheshadow.
- Cron/Gateway: separate per-job webhook delivery (`delivery.mode = "webhook"`) from announce delivery, enforce valid HTTP(S) webhook URLs, and keep a temporary legacy `notify + cron.webhook` fallback for stored jobs. (#17901) Thanks @advaitpaliwal.
- Cron/CLI: add deterministic default stagger for recurring top-of-hour cron schedules (including 6-field seconds cron), auto-migrate existing jobs to persisted `schedule.staggerMs`, and add `openclaw cron add/edit --stagger <duration>` plus `--exact` overrides for per-job timing control.
- Cron: log per-run model/provider usage telemetry in cron run logs/webhooks and add a local usage report script for aggregating token usage by job. (#18172) Thanks @HankAndTheCrew.
- Tools/Web: add URL allowlists for `web_search` and `web_fetch`. (#18584) Thanks @smartprogrammer93.
- Browser: add `extraArgs` config for custom Chrome launch arguments. (#18443) Thanks @JayMishra-source.
- Voice Call: pre-cache inbound greeting TTS for faster first playback. (#18447) Thanks @JayMishra-source.
- Skills: compact skill file `<location>` paths in the system prompt by replacing home-directory prefixes with `~`, and add targeted compaction tests for prompt serialization behavior. (#14776) Thanks @bitfish3.
- Skills: refine skill-description routing boundaries with explicit "Use when"/"NOT for" guidance for coding-agent/github/weather, and clarify PTY/browser fallback wording. (#14577) Thanks @DylanWoodAkers.
- Auto-reply/Prompts: include trusted inbound `message_id` in conversation metadata payloads for downstream targeting workflows. Thanks @tyler6204.
- Auto-reply: include `sender_id` in trusted inbound metadata so moderation workflows can target the sender without relying on untrusted text. (#18303) Thanks @crimeacs.
- UI/Sessions: avoid duplicating typed session prefixes in display names (for example `Subagent Subagent ...`). Thanks @tyler6204.
- Agents/Z.AI: enable `tool_stream` by default for real-time tool call streaming, with opt-out via `params.tool_stream: false`. (#18173) Thanks @tianxiao1430-jpg.
- Plugins: add `before_agent_start` model/provider overrides before resolution. (#18568) Thanks @natefikru.
- Mattermost: add emoji reaction actions plus reaction event notifications, including an explicit boolean `remove` flag to avoid accidental removals. (#18608) Thanks @echo931.
- Memory/Search: add FTS fallback plus query expansion for memory search. (#18304) Thanks @irchelper.
- Agents/Models: support per-model `thinkingDefault` overrides in model config. (#18152) Thanks @wu-tian807.
- Agents: enable `llms.txt` discovery in default behavior. (#18158) Thanks @yolo-maxi.
- Extensions/Auth: add OpenAI Codex CLI auth provider integration. (#18009) Thanks @jiteshdhamaniya.
- Feishu: add Bitable create-app/create-field tools for automation workflows. (#17963) Thanks @gaowanqi08141999.
- Docker: add optional `OPENCLAW_INSTALL_BROWSER` build arg to preinstall Chromium + Xvfb in the Docker image, avoiding runtime Playwright installs. (#18449)
### Fixes
- Tests/Telegram: add regression coverage for command-menu sync that asserts all `setMyCommands` entries are Telegram-safe and hyphen-normalized across native/custom/plugin command sources. (#19703) Thanks @obviyus.
- Agents/Image: collapse resize diagnostics to one line per image and include visible pixel/byte size details in the log message for faster triage.
- Agents/Subagents: preemptively guard accumulated tool-result context before model calls by truncating oversized outputs and compacting oldest tool-result messages to avoid context-window overflow crashes. Thanks @tyler6204.
- Agents/Subagents/CLI: fail `sessions_spawn` when subagent model patching is rejected, allow subagent model patch defaults from `subagents.model`, and keep `sessions list`/`status` model reporting aligned to runtime model resolution. (#18660) Thanks @robbyczgw-cla.
- Agents/Subagents: add explicit subagent guidance to recover from `[compacted: tool output removed to free context]` / `[truncated: output exceeded context limit]` markers by re-reading with smaller chunks instead of full-file `cat`. Thanks @tyler6204.
- Agents/Tools: make `read` auto-page across chunks (when no explicit `limit` is provided) and scale its per-call output budget from model `contextWindow`, so larger contexts can read more before context guards kick in. Thanks @tyler6204.
- Agents/Tools: strip duplicated `read` truncation payloads from tool-result `details` and make pre-call context guarding account for heavy tool-result metadata, so repeated `read` calls no longer bypass compaction and overflow model context windows. Thanks @tyler6204.
- Reply threading: keep reply context sticky across streamed/split chunks and preserve `replyToId` on all chunk sends across shared and channel-specific delivery paths (including iMessage, BlueBubbles, Telegram, Discord, and Matrix), so follow-up bubbles stay attached to the same referenced message. Thanks @tyler6204.
- Gateway/Agent: defer transient lifecycle `error` snapshots with a short grace window so `agent.wait` does not resolve early during retry/failover. Thanks @tyler6204.
- Gateway/Presence: centralize presence snapshot broadcasts and unify runtime version precedence (`OPENCLAW_VERSION` > `OPENCLAW_SERVICE_VERSION` > `npm_package_version`) so self-presence and websocket `hello-ok` report consistent versions.
- Hooks/Automation: bridge outbound/inbound message lifecycle into internal hook events (`message:received`, `message:sent`) with session-key correlation guards, while keeping per-payload success/error reporting accurate for chunked and best-effort deliveries. (PR #9387)
- Media understanding: honor `agents.defaults.imageModel` during auto-discovery so implicit image analysis uses configured primary/fallback image models. (PR #7607)
- iOS/Onboarding: stop auth Step 3 retry-loop churn by pausing reconnect attempts on unauthorized/missing-token gateway errors and keeping auth/pairing issue state sticky during manual retry. (#19153) Thanks @mbelinky.
- Voice-call: auto-end calls when media streams disconnect to prevent stuck active calls. (#18435) Thanks @JayMishra-source.
- Voice call/Gateway: prevent overlapping closed-loop turn races with per-call turn locking, route transcript dedupe via source-aware fingerprints with strict cache eviction bounds, and harden `voicecall latency` stats for large logs without spread-operator stack overflow. (#19140) Thanks @mbelinky.
- iOS/Chat: route ChatSheet RPCs through the operator session instead of the node session to avoid node-role authorization failures for `chat.history`, `chat.send`, and `sessions.list`. (#19320) Thanks @mbelinky.
- macOS/Update: correct the Sparkle appcast version for 2026.2.15 so updates are offered again. (#18201)
- Gateway/Auth: clear stale device-auth tokens after device token mismatch errors so re-paired clients can re-auth. (#18201)
- Telegram: enable DM voice-note transcription with CLI fallback handling. (#18564) Thanks @thhuang.
- Telegram/Polls: restore Telegram poll action wiring in channel handlers. (#18122) Thanks @akyourowngames.
- WebChat: strip reply/audio directive tags from rendered chat output. (#18093) Thanks @aldoeliacim.
- Discord: honor configured HTTP proxy for app-id and allowlist REST resolution. (#17958) Thanks @k2009.
- BlueBubbles: add fallback path to recover outbound `message_id` from `fromMe` webhooks when platform message IDs are missing. Thanks @tyler6204.
- BlueBubbles: match outbound message-id fallback recovery by chat identifier as well as account context. Thanks @tyler6204.
- BlueBubbles: include sender identifier in untrusted conversation metadata for conversation info payloads. Thanks @tyler6204.
- Security/Exec: fix the OC-09 credential-theft path via environment-variable injection. (#18048) Thanks @aether-ai-agent.
- Security/Config: confine `$include` resolution to the top-level config directory, harden traversal/symlink checks with cross-platform-safe path containment, and add doctor hints for invalid escaped include paths. (#18652) Thanks @aether-ai-agent.
- Providers: improve error messaging for unconfigured local `ollama`/`vllm` providers. (#18183) Thanks @arosstale.
- TTS: surface all provider errors instead of only the last error in aggregated failures. (#17964) Thanks @ikari-pl.
- CLI/Doctor/Configure: skip gateway auth checks for loopback-only setups. (#18407) Thanks @sggolakiya.
- CLI/Doctor: reconcile gateway service-token drift after re-pair flows. (#18525) Thanks @norunners.
- Process/Windows: disable detached spawn in exec runs to prevent empty command output. (#18067) Thanks @arosstale.
- Process: gracefully terminate process trees with SIGTERM before SIGKILL. (#18626) Thanks @sauerdaniel.
- Sessions/Windows: use atomic session-store writes to prevent context loss on Windows. (#18347) Thanks @twcwinston.
- Agents/Image: validate base64 image payloads before provider submission. (#18263) Thanks @sriram369.
- Models CLI: validate catalog entries in `openclaw models set`. (#18129) Thanks @carrotRakko.
- Usage: isolate last-turn totals in token usage reporting to avoid mixed-turn totals. (#18052) Thanks @arosstale.
- Cron: resolve `accountId` from agent bindings in isolated sessions. (#17996) Thanks @simonemacario.
- Gateway/HTTP: preserve unbracketed IPv6 `Host` headers when normalizing requests. (#18061) Thanks @Clawborn.
- Sandbox: fix workspace-directory orphaning during SHA-1 -> SHA-256 slug migration. (#18523) Thanks @yinghaosang.
- Ollama/Qwen: handle Qwen 3 reasoning field format in Ollama responses. (#18631) Thanks @mr-sk.
- OpenAI/Transcripts: always drop orphaned reasoning blocks from transcript repair. (#18632) Thanks @TySabs.
- Fix types in all tests. Typecheck the whole repository.
- Gateway/Channels: wire `gateway.channelHealthCheckMinutes` into strict config validation, treat implicit account status as managed for health checks, and harden channel auto-restart flow (preserve restart-attempt caps across crash loops, propagate enabled/configured runtime flags, and stop pending restart backoff after manual stop). Thanks @steipete.
- Gateway/WebChat: hard-cap `chat.history` oversized payloads by truncating high-cost fields and replacing over-budget entries with placeholders, so history fetches stay within configured byte limits and avoid chat UI freezes. (#18505)
- UI/Usage: replace lingering undefined `var(--text-muted)` usage with `var(--muted)` in usage date-range and chart styles to keep muted text visible across themes. (#17975) Thanks @jogelin.
- UI/Usage: preserve selected-range totals when timeline data is downsampled by bucket-aggregating timeseries points (instead of dropping intermediate points), so filtered tokens/cost stay accurate. (#17959) Thanks @jogelin.
- UI/Sessions: refresh the sessions table only after successful deletes and preserve delete errors on cancel/failure paths, so deleted sessions disappear automatically without masking delete failures. (#18507)
- Scripts/UI/Windows: fix `pnpm ui:*` spawn `EINVAL` failures by restoring shell-backed launch for `.cmd`/`.bat` runners, narrowing shell usage to launcher types that require it, and rejecting unsafe forwarded shell metacharacters in UI script args. (#18594)
- Hooks/Session-memory: recover `/new` conversation summaries when session pointers are reset-path or missing `sessionFile`, and consistently prefer the newest `.jsonl.reset.*` transcript candidate for fallback extraction. (#18088)
- Auto-reply/Sessions: prevent stale thread ID leakage into non-thread sessions so replies stay in the main DM after topic interactions. (#18528) Thanks @j2h4u.
- Slack: restrict forwarded-attachment ingestion to explicit shared-message attachments and skip non-Slack forwarded `image_url` fetches, preventing non-forward attachment unfurls from polluting inbound agent context while preserving forwarded message handling.
- Feishu: detect bot mentions in post messages with embedded docs when `message.mentions` is empty. (#18074) Thanks @popomore.
- Agents/Sessions: align session lock watchdog hold windows with run and compaction timeout budgets (plus grace), preventing valid long-running turns from being force-unlocked mid-run while still recovering hung lock owners. (#18060)
- Cron: preserve default model fallbacks for cron agent runs when only `model.primary` is overridden, so failover still follows configured fallbacks unless explicitly cleared with `fallbacks: []`. (#18210) Thanks @mahsumaktas.
- Cron: route text-only announce output through the main session announce flow via runSubagentAnnounceFlow so cron text-only output remains visible to the initiating session. Thanks @tyler6204.
- Cron: treat `timeoutSeconds: 0` as no-timeout (not clamped to 1), ensuring long-running cron runs are not prematurely terminated. Thanks @tyler6204.
- Cron announce injection now targets the session determined by delivery config (`to` + channel) instead of defaulting to the current session. Thanks @tyler6204.
- Cron/Heartbeat: canonicalize session-scoped reminder `sessionKey` routing and preserve explicit flat `sessionKey` cron tool inputs, preventing enqueue/wake namespace drift for session-targeted reminders. (#18637) Thanks @vignesh07.
- Cron/Webhooks: reuse existing session IDs for webhook/cron runs when the session key is stable and still fresh, preserving conversation history. (#18031) Thanks @Operative-001.
- Cron: prevent spin loops when cron jobs complete within the scheduled second by advancing the next run and enforcing a minimum refire gap. (#18073) Thanks @widingmarcus-cyber.
- OpenClawKit/iOS ChatUI: accept canonical session-key completion events for local pending runs and preserve message IDs across history refreshes, preventing stuck "thinking" state and message flicker after gateway replies. (#18165) Thanks @mbelinky.
- iOS/Onboarding: add QR-first onboarding wizard with setup-code deep link support, pairing/auth issue guidance, and device-pair QR generation improvements for Telegram/Web/TUI fallback flows. (#18162) Thanks @mbelinky and @Marvae.
- iOS/Gateway: stabilize connect/discovery state handling, add onboarding reset recovery in Settings, and fix iOS gateway-controller coverage for command-surface and last-connection persistence behavior. (#18164) Thanks @mbelinky.
- iOS/Talk: harden mobile talk config handling by ignoring redacted/env-placeholder API keys, support secure local keychain override, improve accessibility motion/contrast behavior in status UI, and tighten ATS to local-network allowance. (#18163) Thanks @mbelinky.
- iOS/Location: restore the significant location monitor implementation (service hooks + protocol surface + ATS key alignment) after merge drift so iOS builds compile again. (#18260) Thanks @ngutman.
- iOS/Signing: auto-select local Apple Development team during iOS project generation/build, prefer the canonical OpenClaw team when available, and support local per-machine signing overrides without committing team IDs. (#18421) Thanks @ngutman.
- Discord/Telegram: make per-account message action gates effective for both action listing and execution, and preserve top-level gate restrictions when account overrides only specify a subset of `actions` keys (account key -> base key -> default fallback). (#18494)
- Telegram: keep DM-topic replies and draft previews in the originating private-chat topic by preserving positive `message_thread_id` values for DM threads. (#18586) Thanks @sebslight.
- Telegram: preserve private-chat topic `message_thread_id` on outbound sends (message/sticker/poll), keep thread-not-found retry fallback, and avoid masking `chat not found` routing errors. (#18993) Thanks @obviyus.
- Discord: prevent duplicate media delivery when the model uses the `message send` tool with media, by skipping media extraction from messaging tool results since the tool already sent the message directly. (#18270)
- Discord: route `audioAsVoice` auto-replies through the voice message API so opt-in audio renders as voice messages. (#18041) Thanks @zerone0x.
- Discord: skip auto-thread creation in forum/media/voice/stage channels and keep group session last-route metadata fresh to avoid invalid thread API errors and lost follow-up sends. (#18098) Thanks @Clawborn.
- Discord/Commands: normalize `commands.allowFrom` entries with `user:`/`discord:`/`pk:` prefixes and `<@id>` mentions so command authorization matches Discord allowlist behavior. (#18042)
- Telegram: keep draft-stream preview replies attached to the user message for `replyToMode: "all"` in groups and DMs, preserving threaded reply context from preview through finalization. (#17880) Thanks @yinghaosang.
- Telegram: prevent streaming final replies from being overwritten by later final/error payloads, and suppress fallback tool-error warnings when a recovered assistant answer already exists after tool calls. (#17883) Thanks @Marvae and @obviyus.
- Telegram: debounce the first draft-stream preview update (30-char threshold) and finalize short responses by editing the stop-time preview message, improving first push notifications and avoiding duplicate final sends. (#18148) Thanks @Marvae.
- Telegram: disable block streaming when `channels.telegram.streamMode` is `off`, preventing newline/content-block replies from splitting into multiple messages. (#17679) Thanks @saivarunk.
- Telegram: keep `streamMode: "partial"` draft previews in a single message across assistant-message/reasoning boundaries, preventing duplicate preview bubbles during partial-mode tool-call turns. (#18956) Thanks @obviyus.
- Telegram: normalize native command names for Telegram menu registration (`-` -> `_`) to avoid `BOT_COMMAND_INVALID` command-menu wipeouts, and log failed command syncs instead of silently swallowing them. (#19257) Thanks @akramcodez.
- Telegram: route non-abort slash commands on the normal chat/topic sequential lane while keeping true abort requests (`/stop`, `stop`) on the control lane, preventing command/reply race conditions from control-lane bypass. (#17899) Thanks @obviyus.
- Telegram: ignore `<media:...>` placeholder lines when extracting `MEDIA:` tool-result paths, preventing false local-file reads and dropped replies. (#18510) Thanks @yinghaosang.
- Telegram: skip retries when inbound media `getFile` fails with Telegram's 20MB limit and continue processing message text, avoiding dropped messages for oversized attachments. (#18531) Thanks @brandonwise.
- Telegram: clear stored polling offsets when bot tokens change or accounts are deleted, preventing stale offsets after token rotations. (#18233)
- Telegram: enable `autoSelectFamily` by default on Node.js 22+ so IPv4 fallback works on broken IPv6 networks. (#18272) Thanks @nacho9900.
- Auto-reply/TTS: keep tool-result media delivery enabled in group chats and native command sessions (while still suppressing tool summary text) so `NO_REPLY` follow-ups do not drop successful TTS audio. (#17991) Thanks @zerone0x.
- Agents/Tools: deliver tool-result media even when verbose tool output is off so media attachments are not dropped. (#16679)
- Discord: optimize reaction notification handling to skip unnecessary message fetches in `off`/`all`/`allowlist` modes, streamline reaction routing, and improve reaction emoji formatting. (#18248) Thanks @thewilloftheshadow and @victorGPT.
- CLI/Pairing: make `openclaw qr --remote` prefer `gateway.remote.url` over tailscale/public URL resolution and register the `openclaw clawbot qr` legacy alias path. (#18091)
- CLI/QR: restore fail-fast validation for `openclaw qr --remote` when neither `gateway.remote.url` nor tailscale `serve`/`funnel` is configured, preventing unusable remote pairing QR flows. (#18166) Thanks @mbelinky.
- CLI: fix parent/subcommand option collisions across gateway, daemon, update, ACP, and browser command flows, while preserving legacy `browser set headers --json <payload>` compatibility.
- CLI/Doctor: ensure `openclaw doctor --fix --non-interactive --yes` exits promptly after completion so one-shot automation no longer hangs. (#18502)
- CLI/Doctor: auto-repair `dmPolicy="open"` configs missing wildcard allowlists and write channel-correct repair paths (including `channels.googlechat.dm.allowFrom`) so `openclaw doctor --fix` no longer leaves Google Chat configs invalid after attempted repair. (#18544)
- CLI/Doctor: detect gateway service token drift when the gateway token is only provided via environment variables, keeping service repairs aligned after token rotation.
- Gateway/Update: prevent restart crash loops after failed self-updates by restarting only on successful updates, stopping early on failed install/build steps, and running `openclaw doctor --fix` during updates to sanitize config. (#18131) Thanks @RamiNoodle733.
- Gateway/Update: preserve update.run restart delivery context so post-update status replies route back to the initiating channel/thread. (#18267) Thanks @yinghaosang.
- CLI/Update: run a standalone restart helper after updates, honoring service-name overrides and reporting restart initiation separately from confirmed restarts. (#18050)
- CLI/Daemon: warn when a gateway restart sees a stale service token so users can reinstall with `openclaw gateway install --force`, and skip drift warnings for non-gateway service restarts. (#18018)
- CLI/Daemon: prefer the active version-manager Node when installing daemons and include macOS version-manager bin directories in the service PATH so launchd services resolve user-managed runtimes.
- CLI/Status: fix `openclaw status --all` token summaries for bot-token-only channels so Mattermost/Zalo no longer show a bot+app warning. (#18527) Thanks @echo931.
- CLI/Configure: make the `/model picker` allowlist prompt searchable with tokenized matching in `openclaw configure` so users can filter huge model lists by typing terms like `gpt-5.2 openai/`. (#19010) Thanks @bjesuiter.
- CLI/Message: preserve `--components` JSON payloads in `openclaw message send` so Discord component payloads are no longer dropped. (#18222) Thanks @saurabhchopade.
- Voice Call: add an optional stale call reaper (`staleCallReaperSeconds`) to end stuck calls when enabled. (#18437)
- Auto-reply/Subagents: propagate group context (`groupId`, `groupChannel`, `space`) when spawning via `/subagents spawn`, matching tool-triggered subagent spawn behavior.
- Subagents: route nested announce results back to the parent session after the parent run ends, falling back only when the parent session is deleted. (#18043) Thanks @tyler6204.
- Subagents: cap announce retry loops with max attempts and expiry to prevent infinite retry spam after deferred announces. (#18444)
- Agents/Tools/exec: add a preflight guard that detects likely shell env var injection (e.g. `$DM_JSON`, `$TMPDIR`) in Python/Node scripts before execution, preventing recurring cron failures and wasted tokens when models emit mixed shell+language source. (#12836)
- Agents/Tools/exec: treat normal non-zero exit codes as completed and append the exit code to tool output to avoid false tool-failure warnings. (#18425)
- Agents/Tools: make loop detection progress-aware and phased by hard-blocking known `process(action=poll|log)` no-progress loops, warning on generic identical-call repeats, warning + no-progress-blocking ping-pong alternation loops (10/20), coalescing repeated warning spam into threshold buckets (including canonical ping-pong pairs), adding a global circuit breaker at 30 no-progress repeats, and emitting structured diagnostic `tool.loop` warning/error events for loop actions. (#16808) Thanks @akramcodez and @beca-oc.
- Agents/Hooks: preserve the `before_tool_call` wrapped-marker across abort-signal tool wrapping so the hook runs once per tool call in normal agent sessions. (#16852) Thanks @sreuter.
- Agents/Tests: add `before_message_write` persistence regression coverage for block/mutate behavior (including synthetic tool-result flushes) and thrown-hook fallback persistence. (#18197) Thanks @shakkernerd
- Agents/Tools: scope the `message` tool schema to the active channel so Telegram uses `buttons` and Discord uses `components`. (#18215) Thanks @obviyus.
- Agents/Image tool: replace Anthropic-incompatible union schema with explicit `image` (single) and `images` (multi) parameters, keeping tool schemas `anyOf`/`oneOf`/`allOf`-free while preserving multi-image analysis support. (#18551, #18566) Thanks @aldoeliacim.
- Agents/Models: probe the primary model when its auth-profile cooldown is near expiry (with per-provider throttling), so runs recover from temporary rate limits without staying on fallback models until restart. (#17478) Thanks @PlayerGhost.
- Agents/Failover: classify provider abort stop-reason errors (`Unhandled stop reason: abort`, `stop reason: abort`, `reason: abort`) as timeout-class failures so configured model fallback chains trigger instead of surfacing raw abort failures. (#18618) Thanks @sauerdaniel.
- Models/CLI: sync auth-profiles credentials into agent `auth.json` before registry availability checks so `openclaw models list --all` reports auth correctly for API-key/token providers, normalize provider-id aliases when bridging credentials, and skip expired token mirrors. (#18610, #18615)
- Agents/Context: raise default total bootstrap prompt cap from `24000` to `150000` chars (keeping `bootstrapMaxChars` at `20000`), include total-cap visibility in `/context`, and mark truncation from injected-vs-raw sizes so total-cap clipping is reflected accurately.
- Memory/QMD: scope managed collection names per agent and precreate glob-backed collection directories before registration, preventing cross-agent collection clobbering and startup ENOENT failures in fresh workspaces. (#17194) Thanks @jonathanadams96.
- Cron: preserve per-job schedule-error isolation in post-run maintenance recompute so malformed sibling jobs no longer abort persistence of successful runs. (#17852) Thanks @pierreeurope.
- Gateway/Config: prevent `config.patch` object-array merges from falling back to full-array replacement when some patch entries lack `id`, so partial `agents.list` updates no longer drop unrelated agents. (#17989) Thanks @stakeswky.
- Gateway/Auth: trim whitespace around trusted proxy entries before matching so configured proxies with stray spaces still authorize. (#18084) Thanks @Clawborn.
- Config/Discord: require string IDs in Discord allowlists, keep onboarding inputs string-only, and add doctor repair for numeric entries. (#18220) Thanks @thewilloftheshadow.
- Security/Sessions: create new session transcript JSONL files with user-only (`0o600`) permissions and extend `openclaw security audit --fix` to remediate existing transcript file permissions.
- Sessions/Maintenance: archive transcripts when pruning stale sessions, clean expired media in subdirectories, and purge `.deleted` transcript archives after the prune window to prevent disk leaks. (#18538)
- Infra/Fetch: ensure foreign abort-signal listener cleanup never masks original fetch successes/failures, while still preventing detached-finally unhandled rejection noise in `wrapFetchWithAbortSignal`. Thanks @Jackten.
- Heartbeat: allow suppressing tool error warning payloads during heartbeat runs via a new heartbeat config flag. (#18497) Thanks @thewilloftheshadow.
- Heartbeat: include sender metadata (From/To/Provider) in heartbeat prompts so model context matches the delivery target. (#18532) Thanks @dinakars777.
- Heartbeat/Telegram: strip configured `responsePrefix` before heartbeat ack detection (with boundary-safe matching) so prefixed `HEARTBEAT_OK` replies are correctly suppressed instead of leaking into DMs. (#18602)
## 2026.2.15
### Changes
- Discord: unlock rich interactive agent prompts with Components v2 (buttons, selects, modals, and attachment-backed file blocks) so for native interaction through Discord. Thanks @thewilloftheshadow.
- Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow.
- Plugins: expose `llm_input` and `llm_output` hook payloads so extensions can observe prompt/input context and model output usage details. (#16724) Thanks @SecondThread.
- Subagents: nested sub-agents (sub-sub-agents) with configurable depth. Set `agents.defaults.subagents.maxSpawnDepth: 2` to allow sub-agents to spawn their own children. Includes `maxChildrenPerAgent` limit (default 5), depth-aware tool policy, and proper announce chain routing. (#14447) Thanks @tyler6204.
- Slack/Discord/Telegram: add per-channel ack reaction overrides (account/channel-level) to support platform-specific emoji formats. (#17092) Thanks @zerone0x.
- Telegram: add `channel_post` inbound support for channel-based bot-to-bot wake/trigger flows, with channel allowlist gating and message/media batching parity.
- Cron/Gateway: add finished-run webhook delivery toggle (`notify`) and dedicated webhook auth token support (`cron.webhookToken`) for outbound cron webhook posts. (#14535) Thanks @advaitpaliwal.
- Channels: deduplicate probe/token resolution base types across core + extensions while preserving per-channel error typing. (#16986) Thanks @iyoda and @thewilloftheshadow.
- Memory: add MMR (Maximal Marginal Relevance) re-ranking for hybrid search diversity. Configurable via `memorySearch.query.hybrid.mmr`. Thanks @rodrigouroz.
- Memory: add opt-in temporal decay for hybrid search scoring, with configurable half-life via `memorySearch.query.hybrid.temporalDecay`. Thanks @rodrigouroz.
### Fixes
- Discord: send initial content when creating non-forum threads so `thread-create` content is delivered. (#18117) Thanks @zerone0x.
- Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh.
- Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent.
- Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent.
- Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh.
- Gateway/Security: redact sensitive session/path details from `status` responses for non-admin clients; full details remain available to `operator.admin`. (#8590) Thanks @fr33d3m0n.
- Gateway/Control UI: preserve requested operator scopes for Control UI bypass modes (`allowInsecureAuth` / `dangerouslyDisableDeviceAuth`) when device identity is unavailable, preventing false `missing scope` failures on authenticated LAN/HTTP operator sessions. (#17682) Thanks @leafbird.
- LINE/Security: fail closed on webhook startup when channel token or channel secret is missing, and treat LINE accounts as configured only when both are present. (#17587) Thanks @davidahmann.
- Skills/Security: restrict `download` installer `targetDir` to the per-skill tools directory to prevent arbitrary file writes. Thanks @Adam55A-code.
- Skills/Linux: harden go installer fallback on apt-based systems by handling root/no-sudo environments safely, doing best-effort apt index refresh, and returning actionable errors instead of failing with spawn errors. (#17687) Thanks @mcrolly.
- Web Fetch/Security: cap downloaded response body size before HTML parsing to prevent memory exhaustion from oversized or deeply nested pages. Thanks @xuemian168.
- Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving `passwordFile` path exemptions, preventing accidental redaction of non-secret config values like `maxTokens` and IRC password-file paths. (#16042) Thanks @akramcodez.
- Dev tooling: harden git `pre-commit` hook against option injection from malicious filenames (for example `--force`), preventing accidental staging of ignored files. Thanks @mrthankyou.
- Gateway/Agent: reject malformed `agent:`-prefixed session keys (for example, `agent:main`) in `agent` and `agent.identity.get` instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz.
- Gateway/Chat: harden `chat.send` inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.
- Gateway/Send: return an actionable error when `send` targets internal-only `webchat`, guiding callers to use `chat.send` or a deliverable channel. (#15703) Thanks @rodrigouroz.
- Gateway/Commands: keep webchat command authorization on the internal `webchat` context instead of inferring another provider from channel allowlists, fixing dropped `/new`/`/status` commands in Control UI when channel allowlists are configured. (#7189) Thanks @karlisbergmanis-lv.
- Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing `script-src 'self'`. Thanks @Adam55A-code.
- Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent.
- Agents/Sandbox: clarify system prompt path guidance so sandbox `bash/exec` uses container paths (for example `/workspace`) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot.
- Agents/Context: apply configured model `contextWindow` overrides after provider discovery so `lookupContextTokens()` honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.
- Agents/Context: derive `lookupContextTokens()` from auth-available model metadata and keep the smallest discovered context window for duplicate model ids, preventing cross-provider cache collisions from overestimating session context limits. (#17586) Thanks @githabideri and @vignesh07.
- Agents/OpenAI: force `store=true` for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.
- Memory/FTS: make `buildFtsQuery` Unicode-aware so non-ASCII queries (including CJK) produce keyword tokens instead of falling back to vector-only search. (#17672) Thanks @KinGP5471.
- Auto-reply/Compaction: resolve `memory/YYYY-MM-DD.md` placeholders with timezone-aware runtime dates and append a `Current time:` line to memory-flush turns, preventing wrong-year memory filenames without making the system prompt time-variant. (#17603, #17633) Thanks @nicholaspapadam-wq and @vignesh07.
- Auth/Cooldowns: auto-expire stale auth profile cooldowns when `cooldownUntil` or `disabledUntil` timestamps have passed, and reset `errorCount` so the next transient failure does not immediately escalate to a disproportionately long cooldown. Handles `cooldownUntil` and `disabledUntil` independently. (#3604) Thanks @nabbilkhan.
- Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.
- Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204.
- Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone.
- Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.
- Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.
- Agents/Tools: scope the `message` tool schema to the active channel so Telegram uses `buttons` and Discord uses `components`. (#18215) Thanks @obviyus.
- Telegram: omit `message_thread_id` for DM sends/draft previews and keep forum-topic handling (`id=1` general omitted, non-general kept), preventing DM failures with `400 Bad Request: message thread not found`. (#10942) Thanks @garnetlyx.
- Telegram: replace inbound `<media:audio>` placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.
- Telegram: retry inbound media `getFile` calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.
- Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.
- Discord: preserve channel session continuity when runtime payloads omit `message.channelId` by falling back to event/raw `channel_id` values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as `sessionKey=unknown`. (#17622) Thanks @shakkernerd.
- Discord: dedupe native skill commands by skill name in multi-agent setups to prevent duplicated slash commands with `_2` suffixes. (#17365) Thanks @seewhyme.
- Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu.
- Discord: skip text-based exec approval forwarding in favor of Discord's component-based approval UI. Thanks @thewilloftheshadow.
- Web UI/Agents: hide `BOOTSTRAP.md` in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.
- Memory/QMD: scope managed collection names per agent and precreate glob-backed collection directories before registration, preventing cross-agent collection clobbering and startup ENOENT failures in fresh workspaces. (#17194) Thanks @jonathanadams96.
- Gateway/Memory: initialize QMD startup sync for every configured agent (not just the default agent), so `memory.qmd.update.onBoot` is effective across multi-agent setups. (#17663) Thanks @HenryLoenwind.
- Auto-reply/WhatsApp/TUI/Web: when a final assistant message is `NO_REPLY` and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show `NO_REPLY` placeholders. (#7010) Thanks @Morrowind-Xie.
- Cron: infer `payload.kind="agentTurn"` for model-only `cron.update` payload patches, so partial agent-turn updates do not fail validation when `kind` is omitted. (#15664) Thanks @rodrigouroz.
- TUI: make searchable-select filtering and highlight rendering ANSI-aware so queries ignore hidden escape codes and no longer corrupt ANSI styling sequences during match highlighting. (#4519) Thanks @bee4come.
- TUI/Windows: coalesce rapid single-line submit bursts in Git Bash into one multiline message as a fallback when bracketed paste is unavailable, preventing pasted multiline text from being split into multiple sends. (#4986) Thanks @adamkane.
- TUI: suppress false `(no output)` placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07.
- Auto-reply/WhatsApp/TUI/Web: when a final assistant message is `NO_REPLY` and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show `NO_REPLY` placeholders. (#7010) Thanks @Morrowind-Xie.
- Gateway/Chat: harden `chat.send` inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.
- Gateway/Security: redact sensitive session/path details from `status` responses for non-admin clients; full details remain available to `operator.admin`. (#8590) Thanks @fr33d3m0n.
- Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.
- Agents/OpenAI: force `store=true` for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.
- TUI: preserve copy-sensitive long tokens (URLs/paths/file-like identifiers) during wrapping and overflow sanitization so wrapped output no longer inserts spaces that corrupt copy/paste values. (#17515, #17466, #17505) Thanks @abe238, @trevorpan, and @JasonCry.
- CLI/Build: make legacy daemon CLI compatibility shim generation tolerant of minimal tsdown daemon export sets, while preserving restart/register compatibility aliases and surfacing explicit errors for unavailable legacy daemon commands. Thanks @vignesh07.
- Telegram: replace inbound `<media:audio>` placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.
- Telegram: retry inbound media `getFile` calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.
## 2026.2.14
@@ -38,6 +297,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Security/Sessions/Telegram: restrict session tool targeting by default to the current session tree (`tools.sessions.visibility`, default `tree`) with sandbox clamping, and pass configured per-account Telegram webhook secrets in webhook mode when no explicit override is provided. Thanks @aether-ai-agent.
- CLI/Plugins: ensure `openclaw message send` exits after successful delivery across plugin-backed channels so one-shot sends do not hang. (#16491) Thanks @yinghaosang.
- CLI/Plugins: run registered plugin `gateway_stop` hooks before `openclaw message` exits (success and failure paths), so plugin-backed channels can clean up one-shot CLI resources. (#16580) Thanks @gumadeiras.
- WhatsApp: honor per-account `dmPolicy` overrides (account-level settings now take precedence over channel defaults for inbound DMs). (#10082) Thanks @mcaxtr.
@@ -77,9 +337,11 @@ Docs: https://docs.openclaw.ai
- Gateway/Subagents: preserve queued announce items and summary state on delivery errors, retry failed announce drains, and avoid dropping unsent announcements on timeout/failure. (#16729) Thanks @Clawdette-Workspace.
- Gateway/Config: make `config.patch` merge object arrays by `id` (for example `agents.list`) instead of replacing the whole array, so partial agent updates do not silently delete unrelated agents. (#6766) Thanks @lightclient.
- Webchat/Prompts: stop injecting direct-chat `conversation_label` into inbound untrusted metadata context blocks, preventing internal label noise from leaking into visible chat replies. (#16556) Thanks @nberardi.
- Auto-reply/Prompts: include trusted inbound `message_id`, `chat_id`, `reply_to_id`, and optional `message_id_full` metadata fields so action tools (for example reactions) can target the triggering message without relying on user text. (#17662) Thanks @MaikiMolto.
- Gateway/Sessions: abort active embedded runs and clear queued session work before `sessions.reset`, returning unavailable if the run does not stop in time. (#16576) Thanks @Grynn.
- Sessions/Agents: harden transcript path resolution for mismatched agent context by preserving explicit store roots and adding safe absolute-path fallback to the correct agent sessions directory. (#16288) Thanks @robbyczgw-cla.
- Agents: add a safety timeout around embedded `session.compact()` to ensure stalled compaction runs settle and release blocked session lanes. (#16331) Thanks @BinHPdev.
- Agents/Tools: make required-parameter validation errors list missing fields and instruct: "Supply correct parameters before retrying," reducing repeated invalid tool-call loops (for example `read({})`). (#14729)
- Agents: keep unresolved mutating tool failures visible until the same action retry succeeds, scope mutation-error surfacing to mutating calls (including `session_status` model changes), and dedupe duplicate failure warnings in outbound replies. (#16131) Thanks @Swader.
- Agents/Process/Bootstrap: preserve unbounded `process log` offset-only pagination (default tail applies only when both `offset` and `limit` are omitted) and enforce strict `bootstrapTotalMaxChars` budgeting across injected bootstrap content (including markers), skipping additional injection when remaining budget is too small. (#16539) Thanks @CharlieGreenman.
- Agents/Workspace: persist bootstrap onboarding state so partially initialized workspaces recover missing `BOOTSTRAP.md` once, while completed onboarding keeps BOOTSTRAP deleted even if runtime files are later recreated. Thanks @gumadeiras.
@@ -91,6 +353,7 @@ Docs: https://docs.openclaw.ai
- Tools/Write/Edit: normalize structured text-block arguments for `content`/`oldText`/`newText` before filesystem edits, preventing JSON-like file corruption and false “exact text not found” misses from block-form params. (#16778) Thanks @danielpipernz.
- Ollama/Agents: avoid forcing `<final>` tag enforcement for Ollama models, which could suppress all output as `(no output)`. (#16191) Thanks @Glucksberg.
- Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238.
- Agents/Process: supervise PTY/child process lifecycles with explicit ownership, cancellation, timeouts, and deterministic cleanup, preventing Codex/Pi PTY sessions from dying or stalling on resume. (#14257) Thanks @onutc.
- Skills: watch `SKILL.md` only when refreshing skills snapshot to avoid file-descriptor exhaustion in large data trees. (#11325) Thanks @household-bard.
- Memory/QMD: make `memory status` read-only by skipping QMD boot update/embed side effects for status-only manager checks.
- Memory/QMD: keep original QMD failures when builtin fallback initialization fails (for example missing embedding API keys), instead of replacing them with fallback init errors.
@@ -104,6 +367,7 @@ Docs: https://docs.openclaw.ai
- Memory/QMD: make QMD result JSON parsing resilient to noisy command output by extracting the first JSON array from noisy `stdout`.
- Memory/QMD: treat prefixed `no results found` marker output as an empty result set in qmd JSON parsing. (#11302) Thanks @blazerui.
- Memory/QMD: avoid multi-collection `query` ranking corruption by running one `qmd query -c <collection>` per managed collection and merging by best score (also used for `search`/`vsearch` fallback-to-query). (#16740) Thanks @volarian-vai.
- Memory/QMD: rebind managed collections when existing collection metadata drifts (including sessions name-only listings), preventing non-default agents from reusing another agent's `sessions` collection path. (#17194) Thanks @jonathanadams96.
- Memory/QMD: make `openclaw memory index` verify and print the active QMD index file path/size, and fail when QMD leaves a missing or zero-byte index artifact after an update. (#16775) Thanks @Shunamxiao.
- Memory/QMD: detect null-byte `ENOTDIR` update failures, rebuild managed collections once, and retry update to self-heal corrupted collection metadata. (#12919) Thanks @jorgejhms.
- Memory/QMD/Security: add `rawKeyPrefix` support for QMD scope rules and preserve legacy `keyPrefix: "agent:..."` matching, preventing scoped deny bypass when operators match agent-prefixed session keys.
@@ -120,6 +384,8 @@ Docs: https://docs.openclaw.ai
- Sandbox/Prompts: show the sandbox container workdir as the prompt working directory and clarify host-path usage for file tools, preventing host-path `exec` failures in sandbox sessions. (#16790) Thanks @carrotRakko.
- Media/Security: allow local media reads from OpenClaw state `workspace/` and `sandboxes/` roots by default so generated workspace media can be delivered without unsafe global path bypasses. (#15541) Thanks @lanceji.
- Media/Security: harden local media allowlist bypasses by requiring an explicit `readFile` override when callers mark paths as validated, and reject filesystem-root `localRoots` entries. (#16739)
- Media/Security: allow outbound local media reads from the active agent workspace (including `workspace-<agentId>`) via agent-scoped local roots, avoiding broad global allowlisting of all per-agent workspaces. (#17136) Thanks @MisterGuy420.
- Outbound/Media: thread explicit `agentId` through core `sendMessage` direct-delivery path so agent-scoped local media roots apply even when mirror metadata is absent. (#17268) Thanks @gumadeiras.
- Discord/Security: harden voice message media loading (SSRF + allowed-local-root checks) so tool-supplied paths/URLs cannot be used to probe internal URLs or read arbitrary local files.
- Security/BlueBubbles: require explicit `mediaLocalRoots` allowlists for local outbound media path reads to prevent local file disclosure. (#16322) Thanks @mbelinky.
- Security/BlueBubbles: reject ambiguous shared-path webhook routing when multiple webhook targets match the same guid/password.
@@ -135,6 +401,7 @@ Docs: https://docs.openclaw.ai
- Security/Media: stream and bound URL-backed input media fetches to prevent memory exhaustion from oversized responses. Thanks @vincentkoc.
- Security/Skills: harden archive extraction for download-installed skills to prevent path traversal outside the target directory. Thanks @markmusson.
- Security/Slack: compute command authorization for DM slash commands even when `dmPolicy=open`, preventing unauthorized users from running privileged commands via DM. Thanks @christos-eth.
- Security/Pairing: scope pairing allowlist writes/reads to channel accounts (for example `telegram:yy`), and propagate account-aware pairing approvals so multi-account channels do not share a single per-channel pairing allowFrom store. (#17631) Thanks @crazytan.
- Security/iMessage: keep DM pairing-store identities out of group allowlist authorization (prevents cross-context command authorization). Thanks @vincentkoc.
- Security/Google Chat: deprecate `users/<email>` allowlists (treat `users/...` as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc.
- Security/Google Chat: reject ambiguous shared-path webhook routing when multiple webhook targets verify successfully (prevents cross-account policy-context misrouting). Thanks @vincentkoc.
@@ -190,6 +457,7 @@ Docs: https://docs.openclaw.ai
- Docs/Hooks: update hooks documentation URLs to the new `/automation/hooks` location. (#16165) Thanks @nicholascyh.
- Security/Audit: warn when `gateway.tools.allow` re-enables default-denied tools over HTTP `POST /tools/invoke`, since this can increase RCE blast radius if the gateway is reachable.
- Security/Plugins/Hooks: harden npm-based installs by restricting specs to registry packages only, passing `--ignore-scripts` to `npm pack`, and cleaning up temp install directories.
- Security/Sessions: preserve inter-session input provenance for routed prompts so delegated/internal sessions are not treated as direct external user instructions. Thanks @anbecker.
- Feishu: stop persistent Typing reaction on NO_REPLY/suppressed runs by wiring reply-dispatcher cleanup to remove typing indicators. (#15464) Thanks @arosstale.
- Agents: strip leading empty lines from `sanitizeUserFacingText` output and normalize whitespace-only outputs to empty text. (#16158) Thanks @mcinteerj.
- BlueBubbles: gracefully degrade when Private API is disabled by filtering private-only actions, skipping private-only reactions/reply effects, and avoiding private reply markers so non-private flows remain usable. (#16002) Thanks @L-U-C-K-Y.
@@ -281,6 +549,7 @@ Docs: https://docs.openclaw.ai
- Tools/web_search: support `freshness` for the Perplexity provider by mapping `pd`/`pw`/`pm`/`py` to Perplexity `search_recency_filter` values and including freshness in the Perplexity cache key. (#15343) Thanks @echoVic.
- Clawdock: avoid Zsh readonly variable collisions in helper scripts. (#15501) Thanks @nkelner.
- Memory: switch default local embedding model to the QAT `embeddinggemma-300m-qat-Q8_0` variant for better quality at the same footprint. (#15429) Thanks @azade-c.
- Docs/Discord: expand quick setup and clarify guild workspace guidance. (#20088) Thanks @pejmanjohn, @thewilloftheshadow.
- Docs/Mermaid: remove hardcoded Mermaid init theme blocks from four docs diagrams so dark mode inherits readable theme defaults. (#15157) Thanks @heytulsiprasad.
- Security/Pairing: generate 256-bit base64url device and node pairing tokens and use byte-safe constant-time verification to avoid token-compare edge-case failures. (#16535) Thanks @FaizanKolega, @gumadeiras.
@@ -320,6 +589,7 @@ Docs: https://docs.openclaw.ai
- Configure/Gateway: reject literal `"undefined"`/`"null"` token input and validate gateway password prompt values to avoid invalid password-mode configs. (#13767) Thanks @omair445.
- Gateway: handle async `EPIPE` on stdout/stderr during shutdown. (#13414) Thanks @keshav55.
- Gateway/Control UI: resolve missing dashboard assets when `openclaw` is installed globally via symlink-based Node managers (nvm/fnm/n/Homebrew). (#14919) Thanks @aynorica.
- Gateway/Control UI: keep partial assistant output visible when runs are aborted, and persist aborted partials to session transcripts for follow-up context.
- Cron: use requested `agentId` for isolated job auth resolution. (#13983) Thanks @0xRaini.
- Cron: prevent cron jobs from skipping execution when `nextRunAtMs` advances. (#14068) Thanks @WalterSumbon.
- Cron: pass `agentId` to `runHeartbeatOnce` for main-session jobs. (#14140) Thanks @ishikawa-pro.
@@ -349,6 +619,7 @@ Docs: https://docs.openclaw.ai
- Browser: add Chrome launch flag `--disable-blink-features=AutomationControlled` to reduce `navigator.webdriver` automation detection issues on reCAPTCHA-protected sites. (#10735) Thanks @Milofax.
- Heartbeat: filter noise-only system events so scheduled reminder notifications do not fire when cron runs carry only heartbeat markers. (#13317) Thanks @pvtclawn.
- Signal: render mention placeholders as `@uuid`/`@phone` so mention gating and Clawdbot targeting work. (#2013) Thanks @alexgleason.
- Agents/Reminders: guard reminder promises by appending a note when no `cron.add` succeeded in the turn, so users know nothing was scheduled. (#18588) Thanks @vignesh07.
- Discord: omit empty content fields for media-only messages while preserving caption whitespace. (#9507) Thanks @leszekszpunar.
- Onboarding/Providers: add Z.AI endpoint-specific auth choices (`zai-coding-global`, `zai-coding-cn`, `zai-global`, `zai-cn`) and expand default Z.AI model wiring. (#13456) Thanks @tomsun28.
- Onboarding/Providers: update MiniMax API default/recommended models from M2.1 to M2.5, add M2.5/M2.5-Lightning model entries, and include `minimax-m2.5` in modern model filtering. (#14865) Thanks @adao-max.
@@ -392,6 +663,7 @@ Docs: https://docs.openclaw.ai
- Commands: add `commands.allowFrom` config for separate command authorization, allowing operators to restrict slash commands to specific users while keeping chat open to others. (#12430) Thanks @thewilloftheshadow.
- Docker: add ClawDock shell helpers for Docker workflows. (#12817) Thanks @Olshansk.
- Gateway: periodic channel health monitor auto-restarts stuck, crashed, or silently-stopped channels. Configurable via `gateway.channelHealthCheckMinutes` (default: 5, set to 0 to disable). (#7053, #4302)
- iOS: alpha node app + setup-code onboarding. (#11756) Thanks @mbelinky.
- Channels: comprehensive BlueBubbles and channel cleanup. (#11093) Thanks @tyler6204.
- Channels: IRC first-class channel support. (#11482) Thanks @vignesh07.
@@ -455,6 +727,7 @@ Docs: https://docs.openclaw.ai
- Thinking: allow xhigh for `github-copilot/gpt-5.2-codex` and `github-copilot/gpt-5.2`. (#11646) Thanks @LatencyTDH.
- Thinking: honor `/think off` for reasoning-capable models. (#9564) Thanks @liuy.
- Discord: support forum/media thread-create starter messages, wire `message thread create --message`, and harden routing. (#10062) Thanks @jarvis89757.
- Discord: download attachments from forwarded messages. (#17049) Thanks @pip-nomel, @thewilloftheshadow.
- Paths: structurally resolve `OPENCLAW_HOME`-derived home paths and fix Windows drive-letter handling in tool meta shortening. (#12125) Thanks @mcaxtr.
- Memory: set Voyage embeddings `input_type` for improved retrieval. (#10818) Thanks @mcinteerj.
- Memory: disable async batch embeddings by default for memory indexing (opt-in via `agents.defaults.memorySearch.remote.batch.enabled`). (#13069) Thanks @mcinteerj.

View File

@@ -5,6 +5,7 @@ Welcome to the lobster tank! 🦞
## Quick Links
- **GitHub:** https://github.com/openclaw/openclaw
- **Vision:** [`VISION.md`](VISION.md)
- **Discord:** https://discord.gg/qkhbAGHRBT
- **X/Twitter:** [@steipete](https://x.com/steipete) / [@openclaw](https://x.com/openclaw)
@@ -13,24 +14,33 @@ Welcome to the lobster tank! 🦞
- **Peter Steinberger** - Benevolent Dictator
- GitHub: [@steipete](https://github.com/steipete) · X: [@steipete](https://x.com/steipete)
- **Shadow** - Discord + Slack subsystem
- **Shadow** - Discord subsystem, Discord admin, Clawhub, all community moderation
- GitHub: [@thewilloftheshadow](https://github.com/thewilloftheshadow) · X: [@4shad0wed](https://x.com/4shad0wed)
- **Vignesh** - Memory (QMD), formal modeling, TUI, and Lobster
- **Vignesh** - Memory (QMD), formal modeling, TUI, IRC, and Lobster
- GitHub: [@vignesh07](https://github.com/vignesh07) · X: [@\_vgnsh](https://x.com/_vgnsh)
- **Jos** - Telegram, API, Nix mode
- GitHub: [@joshp123](https://github.com/joshp123) · X: [@jjpcodes](https://x.com/jjpcodes)
- **Ayaan Zaidi** - Telegram subsystem, iOS app
- GitHub: [@obviyus](https://github.com/obviyus) · X: [@0bviyus](https://x.com/0bviyus)
- **Tyler Yust** - Agents/subagents, cron, BlueBubbles, macOS app
- GitHub: [@tyler6204](https://github.com/tyler6204) · X: [@tyleryust](https://x.com/tyleryust)
- **Mariano Belinky** - iOS app, Security
- GitHub: [@mbelinky](https://github.com/mbelinky) · X: [@belimad](https://x.com/belimad)
- **Seb Slight** - Docs, Agent Reliability, Runtime Hardening
- GitHub: [@sebslight](https://github.com/sebslight) · X: [@sebslig](https://x.com/sebslig)
- **Christoph Nakazawa** - JS Infra
- GitHub: [@cpojer](https://github.com/cpojer) · X: [@cnakazawa](https://x.com/cnakazawa)
- **Gustavo Madeira Santana** - Multi-agents, CLI, web UI
- GitHub: [@gumadeiras](https://github.com/gumadeiras) · X: [@gumadeiras](https://x.com/gumadeiras)
- **Maximilian Nussbaumer** - DevOps, CI, Code Sanity
- GitHub: [@quotentiroler](https://github.com/quotentiroler) · X: [@quotentiroler](https://x.com/quotentiroler)
## How to Contribute
1. **Bugs & small fixes** → Open a PR!
@@ -42,7 +52,7 @@ Welcome to the lobster tank! 🦞
- Test locally with your OpenClaw instance
- Run tests: `pnpm build && pnpm check && pnpm test`
- Ensure CI checks pass
- Keep PRs focused (one thing per PR)
- Keep PRs focused (one thing per PR; do not mix unrelated concerns)
- Describe what & why
## Control UI Decorators
@@ -84,6 +94,26 @@ We are currently prioritizing:
Check the [GitHub Issues](https://github.com/openclaw/openclaw/issues) for "good first issue" labels!
## Maintainers
We're selectively expanding the maintainer team.
If you're an experienced contributor who wants to help shape OpenClaw's direction — whether through code, docs, or community — we'd like to hear from you.
Being a maintainer is a responsibility, not an honorary title. We expect active, consistent involvement — triaging issues, reviewing PRs, and helping move the project forward.
Still interested? Email contributing@openclaw.ai with:
- Links to your PRs on OpenClaw (if you don't have any, start there first)
- Links to open source projects you maintain or actively contribute to
- Your GitHub, Discord, and X/Twitter handles
- A brief intro: background, experience, and areas of interest
- Languages you speak and where you're based
- How much time you can realistically commit
We welcome people across all skill sets — engineering, documentation, community management, and more.
We review every human-only-written application carefully and add maintainers slowly and deliberately.
Please allow a few weeks for a response.
## Report a Vulnerability
We take security reports seriously. Report vulnerabilities directly to the repository where the issue lives:

View File

@@ -23,6 +23,19 @@ COPY scripts ./scripts
RUN pnpm install --frozen-lockfile
# Optionally install Chromium and Xvfb for browser automation.
# Build with: docker build --build-arg OPENCLAW_INSTALL_BROWSER=1 ...
# Adds ~300MB but eliminates the 60-90s Playwright install on every container start.
# Must run after pnpm install so playwright-core is available in node_modules.
ARG OPENCLAW_INSTALL_BROWSER=""
RUN if [ -n "$OPENCLAW_INSTALL_BROWSER" ]; then \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb && \
node /app/node_modules/playwright-core/cli.js install --with-deps chromium && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \
fi
COPY . .
RUN pnpm build
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)

View File

@@ -23,7 +23,7 @@ It answers you on the channels you already use (WhatsApp, Telegram, Slack, Disco
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/start/faq) · [Wizard](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/start/faq) · [Wizard](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
Preferred setup: run the onboarding wizard (`openclaw onboard`) in your terminal.
The wizard guides you step by step through setting up the gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
@@ -546,4 +546,5 @@ Thanks to all clawtributors:
<a href="https://github.com/0xJonHoldsCrypto"><img src="https://avatars.githubusercontent.com/u/81202085?v=4&s=48" width="48" height="48" alt="0xJonHoldsCrypto" title="0xJonHoldsCrypto"/></a> <a href="https://github.com/aaronn"><img src="https://avatars.githubusercontent.com/u/1653630?v=4&s=48" width="48" height="48" alt="aaronn" title="aaronn"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/atalovesyou"><img src="https://avatars.githubusercontent.com/u/3534502?v=4&s=48" width="48" height="48" alt="atalovesyou" title="atalovesyou"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/jiulingyun"><img src="https://avatars.githubusercontent.com/u/126459548?v=4&s=48" width="48" height="48" alt="jiulingyun" title="jiulingyun"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a>
<a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a>
<a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
<a href="https://github.com/AkashKobal"><img src="https://avatars.githubusercontent.com/u/98216083?v=4" width="48" height="48" alt="Akash Kobal" title="Akash Kobal"/></a>
</p>

110
VISION.md Normal file
View File

@@ -0,0 +1,110 @@
## OpenClaw Vision
OpenClaw is the AI that actually does things.
It runs on your devices, in your channels, with your rules.
This document explains the current state and direction of the project.
We are still early, so iteration is fast.
Project overview and developer docs: [`README.md`](README.md)
Contribution guide: [`CONTRIBUTING.md`](CONTRIBUTING.md)
OpenClaw started as a personal playground to learn AI and build something genuinely useful:
an assistant that can run real tasks on a real computer.
It evolved through several names and shells: Warelay -> Clawdbot -> Moltbot -> OpenClaw.
The goal: a personal assistant that is easy to use, supports a wide range of platforms, and respects privacy and security.
The current focus is:
Priority:
- Security and safe defaults
- Bug fixes and stability
- Setup reliability and first-run UX
Next priorities:
- Supporting all major model providers
- Improving support for major messaging channels (and adding a few high-demand ones)
- Performance and test infrastructure
- Better computer-use and agent harness capabilities
- Ergonomics across CLI and web frontend
- Companion apps on macOS, iOS, Android, Windows, and Linux
Contribution rules:
- One PR = one issue/topic. Do not bundle multiple unrelated fixes/features.
- PRs over ~5,000 changed lines are reviewed only in exceptional circumstances.
- Do not open large batches of tiny PRs at once; each PR has review cost.
- For very small related fixes, grouping into one focused PR is encouraged.
## Security
Security in OpenClaw is a deliberate tradeoff: strong defaults without killing capability.
The goal is to stay powerful for real work while making risky paths explicit and operator-controlled.
Canonical security policy and reporting:
- [`SECURITY.md`](SECURITY.md)
We prioritize secure defaults, but also expose clear knobs for trusted high-power workflows.
## Plugins & Memory
OpenClaw has an extensive plugin API.
Core stays lean; optional capability should usually ship as plugins.
Preferred plugin path is npm package distribution plus local extension loading for development.
If you build a plugin, host and maintain it in your own repository.
The bar for adding optional plugins to core is intentionally high.
Plugin docs: [`docs/tools/plugin.md`](docs/tools/plugin.md)
Community plugin listing + PR bar: https://docs.openclaw.ai/plugins/community
Memory is a special plugin slot where only one memory plugin can be active at a time.
Today we ship multiple memory options; over time we plan to converge on one recommended default path.
### Skills
We still ship some bundled skills for baseline UX.
New skills should be published to ClawHub first (`clawhub.ai`), not added to core by default.
Core skill additions should be rare and require a strong product or security reason.
### MCP Support
OpenClaw supports MCP through `mcporter`: https://github.com/steipete/mcporter
This keeps MCP integration flexible and decoupled from core runtime:
- add or change MCP servers without restarting the gateway
- keep core tool/context surface lean
- reduce MCP churn impact on core stability and security
For now, we prefer this bridge model over building first-class MCP runtime into core.
If there is an MCP server or feature `mcporter` does not support yet, please open an issue there.
### Setup
OpenClaw is currently terminal-first by design.
This keeps setup explicit: users see docs, auth, permissions, and security posture up front.
Long term, we want easier onboarding flows as hardening matures.
We do not want convenience wrappers that hide critical security decisions from users.
### Why TypeScript?
OpenClaw is primarily an orchestration system: prompts, tools, protocols, and integrations.
TypeScript was chosen to keep OpenClaw hackable by default.
It is widely known, fast to iterate in, and easy to read, modify, and extend.
## What We Will Not Merge (For Now)
- New core skills when they can live on ClawHub
- Full-doc translation sets for all docs (deferred; we plan AI-generated translations later)
- Commercial service integrations that do not clearly fit the model-provider category
- Wrapper channels around already supported channels without a clear capability or security gap
- First-class MCP runtime in core when `mcporter` already provides the integration path
- Agent-hierarchy frameworks (manager-of-managers / nested planner trees) as a default architecture
- Heavy orchestration layers that duplicate existing agent and tool infrastructure
This list is a roadmap guardrail, not a law of physics.
Strong user demand and strong technical rationale can change it.

View File

@@ -140,6 +140,74 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.14/OpenClaw-2026.2.14.zip" length="22914034" type="application/octet-stream" sparkle:edSignature="lR3nuq46/akMIN8RFDpMkTE0VOVoDVG53Xts589LryMGEtUvJxRQDtHBXfx7ZvToTq6CFKG+L5Kq/4rUspMoAQ=="/>
</item>
<item>
<title>2026.2.15</title>
<pubDate>Mon, 16 Feb 2026 05:04:34 +0100</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>202602150</sparkle:version>
<sparkle:shortVersionString>2026.2.15</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.2.15</h2>
<h3>Changes</h3>
<ul>
<li>Discord: unlock rich interactive agent prompts with Components v2 (buttons, selects, modals, and attachment-backed file blocks) so for native interaction through Discord. Thanks @thewilloftheshadow.</li>
<li>Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow.</li>
<li>Plugins: expose <code>llm_input</code> and <code>llm_output</code> hook payloads so extensions can observe prompt/input context and model output usage details. (#16724) Thanks @SecondThread.</li>
<li>Subagents: nested sub-agents (sub-sub-agents) with configurable depth. Set <code>agents.defaults.subagents.maxSpawnDepth: 2</code> to allow sub-agents to spawn their own children. Includes <code>maxChildrenPerAgent</code> limit (default 5), depth-aware tool policy, and proper announce chain routing. (#14447) Thanks @tyler6204.</li>
<li>Slack/Discord/Telegram: add per-channel ack reaction overrides (account/channel-level) to support platform-specific emoji formats. (#17092) Thanks @zerone0x.</li>
<li>Cron/Gateway: add finished-run webhook delivery toggle (<code>notify</code>) and dedicated webhook auth token support (<code>cron.webhookToken</code>) for outbound cron webhook posts. (#14535) Thanks @advaitpaliwal.</li>
<li>Channels: deduplicate probe/token resolution base types across core + extensions while preserving per-channel error typing. (#16986) Thanks @iyoda and @thewilloftheshadow.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh.</li>
<li>Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent.</li>
<li>Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent.</li>
<li>Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh.</li>
<li>Gateway/Security: redact sensitive session/path details from <code>status</code> responses for non-admin clients; full details remain available to <code>operator.admin</code>. (#8590) Thanks @fr33d3m0n.</li>
<li>Gateway/Control UI: preserve requested operator scopes for Control UI bypass modes (<code>allowInsecureAuth</code> / <code>dangerouslyDisableDeviceAuth</code>) when device identity is unavailable, preventing false <code>missing scope</code> failures on authenticated LAN/HTTP operator sessions. (#17682) Thanks @leafbird.</li>
<li>LINE/Security: fail closed on webhook startup when channel token or channel secret is missing, and treat LINE accounts as configured only when both are present. (#17587) Thanks @davidahmann.</li>
<li>Skills/Security: restrict <code>download</code> installer <code>targetDir</code> to the per-skill tools directory to prevent arbitrary file writes. Thanks @Adam55A-code.</li>
<li>Skills/Linux: harden go installer fallback on apt-based systems by handling root/no-sudo environments safely, doing best-effort apt index refresh, and returning actionable errors instead of failing with spawn errors. (#17687) Thanks @mcrolly.</li>
<li>Web Fetch/Security: cap downloaded response body size before HTML parsing to prevent memory exhaustion from oversized or deeply nested pages. Thanks @xuemian168.</li>
<li>Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving <code>passwordFile</code> path exemptions, preventing accidental redaction of non-secret config values like <code>maxTokens</code> and IRC password-file paths. (#16042) Thanks @akramcodez.</li>
<li>Dev tooling: harden git <code>pre-commit</code> hook against option injection from malicious filenames (for example <code>--force</code>), preventing accidental staging of ignored files. Thanks @mrthankyou.</li>
<li>Gateway/Agent: reject malformed <code>agent:</code>-prefixed session keys (for example, <code>agent:main</code>) in <code>agent</code> and <code>agent.identity.get</code> instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz.</li>
<li>Gateway/Chat: harden <code>chat.send</code> inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.</li>
<li>Gateway/Send: return an actionable error when <code>send</code> targets internal-only <code>webchat</code>, guiding callers to use <code>chat.send</code> or a deliverable channel. (#15703) Thanks @rodrigouroz.</li>
<li>Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing <code>script-src 'self'</code>. Thanks @Adam55A-code.</li>
<li>Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent.</li>
<li>Agents/Sandbox: clarify system prompt path guidance so sandbox <code>bash/exec</code> uses container paths (for example <code>/workspace</code>) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot.</li>
<li>Agents/Context: apply configured model <code>contextWindow</code> overrides after provider discovery so <code>lookupContextTokens()</code> honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.</li>
<li>Agents/Context: derive <code>lookupContextTokens()</code> from auth-available model metadata and keep the smallest discovered context window for duplicate model ids, preventing cross-provider cache collisions from overestimating session context limits. (#17586) Thanks @githabideri and @vignesh07.</li>
<li>Agents/OpenAI: force <code>store=true</code> for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.</li>
<li>Memory/FTS: make <code>buildFtsQuery</code> Unicode-aware so non-ASCII queries (including CJK) produce keyword tokens instead of falling back to vector-only search. (#17672) Thanks @KinGP5471.</li>
<li>Auto-reply/Compaction: resolve <code>memory/YYYY-MM-DD.md</code> placeholders with timezone-aware runtime dates and append a <code>Current time:</code> line to memory-flush turns, preventing wrong-year memory filenames without making the system prompt time-variant. (#17603, #17633) Thanks @nicholaspapadam-wq and @vignesh07.</li>
<li>Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.</li>
<li>Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204.</li>
<li>Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone.</li>
<li>Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.</li>
<li>Subagents/Models: preserve <code>agents.defaults.model.fallbacks</code> when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.</li>
<li>Telegram: omit <code>message_thread_id</code> for DM sends/draft previews and keep forum-topic handling (<code>id=1</code> general omitted, non-general kept), preventing DM failures with <code>400 Bad Request: message thread not found</code>. (#10942) Thanks @garnetlyx.</li>
<li>Telegram: replace inbound <code><media:audio></code> placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.</li>
<li>Telegram: retry inbound media <code>getFile</code> calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.</li>
<li>Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.</li>
<li>Discord: preserve channel session continuity when runtime payloads omit <code>message.channelId</code> by falling back to event/raw <code>channel_id</code> values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as <code>sessionKey=unknown</code>. (#17622) Thanks @shakkernerd.</li>
<li>Discord: dedupe native skill commands by skill name in multi-agent setups to prevent duplicated slash commands with <code>_2</code> suffixes. (#17365) Thanks @seewhyme.</li>
<li>Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu.</li>
<li>Web UI/Agents: hide <code>BOOTSTRAP.md</code> in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.</li>
<li>Auto-reply/WhatsApp/TUI/Web: when a final assistant message is <code>NO_REPLY</code> and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show <code>NO_REPLY</code> placeholders. (#7010) Thanks @Morrowind-Xie.</li>
<li>Cron: infer <code>payload.kind="agentTurn"</code> for model-only <code>cron.update</code> payload patches, so partial agent-turn updates do not fail validation when <code>kind</code> is omitted. (#15664) Thanks @rodrigouroz.</li>
<li>TUI: make searchable-select filtering and highlight rendering ANSI-aware so queries ignore hidden escape codes and no longer corrupt ANSI styling sequences during match highlighting. (#4519) Thanks @bee4come.</li>
<li>TUI/Windows: coalesce rapid single-line submit bursts in Git Bash into one multiline message as a fallback when bracketed paste is unavailable, preventing pasted multiline text from being split into multiple sends. (#4986) Thanks @adamkane.</li>
<li>TUI: suppress false <code>(no output)</code> placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07.</li>
<li>TUI: preserve copy-sensitive long tokens (URLs/paths/file-like identifiers) during wrapping and overflow sanitization so wrapped output no longer inserts spaces that corrupt copy/paste values. (#17515, #17466, #17505) Thanks @abe238, @trevorpan, and @JasonCry.</li>
<li>CLI/Build: make legacy daemon CLI compatibility shim generation tolerant of minimal tsdown daemon export sets, while preserving restart/register compatibility aliases and surfacing explicit errors for unavailable legacy daemon commands. Thanks @vignesh07.</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.2.15/OpenClaw-2026.2.15.zip" length="22896513" type="application/octet-stream" sparkle:edSignature="MLGsd2NeHXFRH1Or0bFQnAjqfuuJDuhl1mvKFIqTQcRvwbeyvOyyLXrqSbmaOgJR3wBQBKLs6jYQ9dQ/3R8RCg=="/>
</item>
<item>
<title>2026.2.13</title>
<pubDate>Sat, 14 Feb 2026 04:30:23 +0100</pubDate>
@@ -241,101 +309,5 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.13/OpenClaw-2026.2.13.zip" length="22902077" type="application/octet-stream" sparkle:edSignature="RpkwlPtB2yN7UOYZWfthV5grhDUcbhcHMeicdRA864Vo/P0Hnq5aHKmSvcbWkjHut96TC57bX+AeUrL7txpLCg=="/>
</item>
<item>
<title>2026.2.12</title>
<pubDate>Fri, 13 Feb 2026 03:17:54 +0100</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>9500</sparkle:version>
<sparkle:shortVersionString>2026.2.12</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.2.12</h2>
<h3>Changes</h3>
<ul>
<li>CLI: add <code>openclaw logs --local-time</code> to display log timestamps in local timezone. (#13818) Thanks @xialonglee.</li>
<li>Telegram: render blockquotes as native <code><blockquote></code> tags instead of stripping them. (#14608)</li>
<li>Config: avoid redacting <code>maxTokens</code>-like fields during config snapshot redaction, preventing round-trip validation failures in <code>/config</code>. (#14006) Thanks @constansino.</li>
</ul>
<h3>Breaking</h3>
<ul>
<li>Hooks: <code>POST /hooks/agent</code> now rejects payload <code>sessionKey</code> overrides by default. To keep fixed hook context, set <code>hooks.defaultSessionKey</code> (recommended with <code>hooks.allowedSessionKeyPrefixes: ["hook:"]</code>). If you need legacy behavior, explicitly set <code>hooks.allowRequestSessionKey: true</code>. Thanks @alpernae for reporting.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Gateway/OpenResponses: harden URL-based <code>input_file</code>/<code>input_image</code> handling with explicit SSRF deny policy, hostname allowlists (<code>files.urlAllowlist</code> / <code>images.urlAllowlist</code>), per-request URL input caps (<code>maxUrlParts</code>), blocked-fetch audit logging, and regression coverage/docs updates.</li>
<li>Security: fix unauthenticated Nostr profile API remote config tampering. (#13719) Thanks @coygeek.</li>
<li>Security: remove bundled soul-evil hook. (#14757) Thanks @Imccccc.</li>
<li>Security/Audit: add hook session-routing hardening checks (<code>hooks.defaultSessionKey</code>, <code>hooks.allowRequestSessionKey</code>, and prefix allowlists), and warn when HTTP API endpoints allow explicit session-key routing.</li>
<li>Security/Sandbox: confine mirrored skill sync destinations to the sandbox <code>skills/</code> root and stop using frontmatter-controlled skill names as filesystem destination paths. Thanks @1seal.</li>
<li>Security/Web tools: treat browser/web content as untrusted by default (wrapped outputs for browser snapshot/tabs/console and structured external-content metadata for web tools), and strip <code>toolResult.details</code> from model-facing transcript/compaction inputs to reduce prompt-injection replay risk.</li>
<li>Security/Hooks: harden webhook and device token verification with shared constant-time secret comparison, and add per-client auth-failure throttling for hook endpoints (<code>429</code> + <code>Retry-After</code>). Thanks @akhmittra.</li>
<li>Security/Browser: require auth for loopback browser control HTTP routes, auto-generate <code>gateway.auth.token</code> when browser control starts without auth, and add a security-audit check for unauthenticated browser control. Thanks @tcusolle.</li>
<li>Sessions/Gateway: harden transcript path resolution and reject unsafe session IDs/file paths so session operations stay within agent sessions directories. Thanks @akhmittra.</li>
<li>Gateway: raise WS payload/buffer limits so 5,000,000-byte image attachments work reliably. (#14486) Thanks @0xRaini.</li>
<li>Logging/CLI: use local timezone timestamps for console prefixing, and include <code>±HH:MM</code> offsets when using <code>openclaw logs --local-time</code> to avoid ambiguity. (#14771) Thanks @0xRaini.</li>
<li>Gateway: drain active turns before restart to prevent message loss. (#13931) Thanks @0xRaini.</li>
<li>Gateway: auto-generate auth token during install to prevent launchd restart loops. (#13813) Thanks @cathrynlavery.</li>
<li>Gateway: prevent <code>undefined</code>/missing token in auth config. (#13809) Thanks @asklee-klawd.</li>
<li>Gateway: handle async <code>EPIPE</code> on stdout/stderr during shutdown. (#13414) Thanks @keshav55.</li>
<li>Gateway/Control UI: resolve missing dashboard assets when <code>openclaw</code> is installed globally via symlink-based Node managers (nvm/fnm/n/Homebrew). (#14919) Thanks @aynorica.</li>
<li>Cron: use requested <code>agentId</code> for isolated job auth resolution. (#13983) Thanks @0xRaini.</li>
<li>Cron: prevent cron jobs from skipping execution when <code>nextRunAtMs</code> advances. (#14068) Thanks @WalterSumbon.</li>
<li>Cron: pass <code>agentId</code> to <code>runHeartbeatOnce</code> for main-session jobs. (#14140) Thanks @ishikawa-pro.</li>
<li>Cron: re-arm timers when <code>onTimer</code> fires while a job is still executing. (#14233) Thanks @tomron87.</li>
<li>Cron: prevent duplicate fires when multiple jobs trigger simultaneously. (#14256) Thanks @xinhuagu.</li>
<li>Cron: isolate scheduler errors so one bad job does not break all jobs. (#14385) Thanks @MarvinDontPanic.</li>
<li>Cron: prevent one-shot <code>at</code> jobs from re-firing on restart after skipped/errored runs. (#13878) Thanks @lailoo.</li>
<li>Heartbeat: prevent scheduler stalls on unexpected run errors and avoid immediate rerun loops after <code>requests-in-flight</code> skips. (#14901) Thanks @joeykrug.</li>
<li>Cron: honor stored session model overrides for isolated-agent runs while preserving <code>hooks.gmail.model</code> precedence for Gmail hook sessions. (#14983) Thanks @shtse8.</li>
<li>Logging/Browser: fall back to <code>os.tmpdir()/openclaw</code> for default log, browser trace, and browser download temp paths when <code>/tmp/openclaw</code> is unavailable.</li>
<li>WhatsApp: convert Markdown bold/strikethrough to WhatsApp formatting. (#14285) Thanks @Raikan10.</li>
<li>WhatsApp: allow media-only sends and normalize leading blank payloads. (#14408) Thanks @karimnaguib.</li>
<li>WhatsApp: default MIME type for voice messages when Baileys omits it. (#14444) Thanks @mcaxtr.</li>
<li>Telegram: handle no-text message in model picker editMessageText. (#14397) Thanks @0xRaini.</li>
<li>Telegram: surface REACTION_INVALID as non-fatal warning. (#14340) Thanks @0xRaini.</li>
<li>BlueBubbles: fix webhook auth bypass via loopback proxy trust. (#13787) Thanks @coygeek.</li>
<li>Slack: change default replyToMode from "off" to "all". (#14364) Thanks @nm-de.</li>
<li>Slack: detect control commands when channel messages start with bot mention prefixes (for example, <code>@Bot /new</code>). (#14142) Thanks @beefiker.</li>
<li>Signal: enforce E.164 validation for the Signal bot account prompt so mistyped numbers are caught early. (#15063) Thanks @Duartemartins.</li>
<li>Discord: process DM reactions instead of silently dropping them. (#10418) Thanks @mcaxtr.</li>
<li>Discord: respect replyToMode in threads. (#11062) Thanks @cordx56.</li>
<li>Heartbeat: filter noise-only system events so scheduled reminder notifications do not fire when cron runs carry only heartbeat markers. (#13317) Thanks @pvtclawn.</li>
<li>Signal: render mention placeholders as <code>@uuid</code>/<code>@phone</code> so mention gating and Clawdbot targeting work. (#2013) Thanks @alexgleason.</li>
<li>Discord: omit empty content fields for media-only messages while preserving caption whitespace. (#9507) Thanks @leszekszpunar.</li>
<li>Onboarding/Providers: add Z.AI endpoint-specific auth choices (<code>zai-coding-global</code>, <code>zai-coding-cn</code>, <code>zai-global</code>, <code>zai-cn</code>) and expand default Z.AI model wiring. (#13456) Thanks @tomsun28.</li>
<li>Onboarding/Providers: update MiniMax API default/recommended models from M2.1 to M2.5, add M2.5/M2.5-Lightning model entries, and include <code>minimax-m2.5</code> in modern model filtering. (#14865) Thanks @adao-max.</li>
<li>Ollama: use configured <code>models.providers.ollama.baseUrl</code> for model discovery and normalize <code>/v1</code> endpoints to the native Ollama API root. (#14131) Thanks @shtse8.</li>
<li>Voice Call: pass Twilio stream auth token via <code><Parameter></code> instead of query string. (#14029) Thanks @mcwigglesmcgee.</li>
<li>Feishu: pass <code>Buffer</code> directly to the Feishu SDK upload APIs instead of <code>Readable.from(...)</code> to avoid form-data upload failures. (#10345) Thanks @youngerstyle.</li>
<li>Feishu: trigger mention-gated group handling only when the bot itself is mentioned (not just any mention). (#11088) Thanks @openperf.</li>
<li>Feishu: probe status uses the resolved account context for multi-account credential checks. (#11233) Thanks @onevcat.</li>
<li>Feishu DocX: preserve top-level converted block order using <code>firstLevelBlockIds</code> when writing/appending documents. (#13994) Thanks @Cynosure159.</li>
<li>Feishu plugin packaging: remove <code>workspace:*</code> <code>openclaw</code> dependency from <code>extensions/feishu</code> and sync lockfile for install compatibility. (#14423) Thanks @jackcooper2015.</li>
<li>CLI/Wizard: exit with code 1 when <code>configure</code>, <code>agents add</code>, or interactive <code>onboard</code> wizards are canceled, so <code>set -e</code> automation stops correctly. (#14156) Thanks @0xRaini.</li>
<li>Media: strip <code>MEDIA:</code> lines with local paths instead of leaking as visible text. (#14399) Thanks @0xRaini.</li>
<li>Config/Cron: exclude <code>maxTokens</code> from config redaction and honor <code>deleteAfterRun</code> on skipped cron jobs. (#13342) Thanks @niceysam.</li>
<li>Config: ignore <code>meta</code> field changes in config file watcher. (#13460) Thanks @brandonwise.</li>
<li>Cron: use requested <code>agentId</code> for isolated job auth resolution. (#13983) Thanks @0xRaini.</li>
<li>Cron: pass <code>agentId</code> to <code>runHeartbeatOnce</code> for main-session jobs. (#14140) Thanks @ishikawa-pro.</li>
<li>Cron: prevent cron jobs from skipping execution when <code>nextRunAtMs</code> advances. (#14068) Thanks @WalterSumbon.</li>
<li>Cron: re-arm timers when <code>onTimer</code> fires while a job is still executing. (#14233) Thanks @tomron87.</li>
<li>Cron: prevent duplicate fires when multiple jobs trigger simultaneously. (#14256) Thanks @xinhuagu.</li>
<li>Cron: isolate scheduler errors so one bad job does not break all jobs. (#14385) Thanks @MarvinDontPanic.</li>
<li>Cron: prevent one-shot <code>at</code> jobs from re-firing on restart after skipped/errored runs. (#13878) Thanks @lailoo.</li>
<li>Daemon: suppress <code>EPIPE</code> error when restarting LaunchAgent. (#14343) Thanks @0xRaini.</li>
<li>Antigravity: add opus 4.6 forward-compat model and bypass thinking signature sanitization. (#14218) Thanks @jg-noncelogic.</li>
<li>Agents: prevent file descriptor leaks in child process cleanup. (#13565) Thanks @KyleChen26.</li>
<li>Agents: prevent double compaction caused by cache TTL bypassing guard. (#13514) Thanks @taw0002.</li>
<li>Agents: use last API call's cache tokens for context display instead of accumulated sum. (#13805) Thanks @akari-musubi.</li>
<li>Agents: keep followup-runner session <code>totalTokens</code> aligned with post-compaction context by using last-call usage and shared token-accounting logic. (#14979) Thanks @shtse8.</li>
<li>Hooks/Plugins: wire 9 previously unwired plugin lifecycle hooks into core runtime paths (session, compaction, gateway, and outbound message hooks). (#14882) Thanks @shtse8.</li>
<li>Hooks/Tools: dispatch <code>before_tool_call</code> and <code>after_tool_call</code> hooks from both tool execution paths with rebased conflict fixes. (#15012) Thanks @Patrick-Barletta, @Takhoffman.</li>
<li>Discord: allow channel-edit to archive/lock threads and set auto-archive duration. (#5542) Thanks @stumct.</li>
<li>Discord tests: use a partial @buape/carbon mock in slash command coverage. (#13262) Thanks @arosstale.</li>
<li>Tests: update thread ID handling in Slack message collection tests. (#14108) Thanks @swizzmagik.</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.2.12/OpenClaw-2026.2.12.zip" length="22877692" type="application/octet-stream" sparkle:edSignature="TGylTM4/7Lab+qp1nuPeOAmEVV1WkafXUPub8ws0z/0mYfbVygRuiev+u3zdPjQWhLnGYTgRgKVyW+kB2+Q2BQ=="/>
</item>
</channel>
</rss>

View File

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

View File

@@ -3,3 +3,7 @@ parent_config: ../../.swiftlint.yml
included:
- Sources
- ../shared/ClawdisNodeKit/Sources
type_body_length:
warning: 900
error: 1300

View File

@@ -0,0 +1,18 @@
// Shared iOS signing defaults for local development + CI.
OPENCLAW_IOS_DEFAULT_TEAM = Y5PE65HELJ
OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM)
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.ios.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.ios.watchkitapp.extension
// 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_IDENTITY = Apple Development
DEVELOPMENT_TEAM = $(OPENCLAW_IOS_SELECTED_TEAM)
// Let Xcode manage provisioning for the selected local team.
PROVISIONING_PROFILE_SPECIFIER =

View File

@@ -0,0 +1,14 @@
// Copy to LocalSigning.xcconfig for personal local signing overrides.
// This file is only an example and should stay committed.
OPENCLAW_CODE_SIGN_STYLE = Automatic
OPENCLAW_DEVELOPMENT_TEAM = P5Z8X89DJL
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios.test.mariano
OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.ios.test.mariano.share
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.ios.test.mariano.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.ios.test.mariano.watchkitapp.extension
// Leave empty with automatic signing.
OPENCLAW_APP_PROFILE =
OPENCLAW_SHARE_PROFILE =

View File

@@ -1,66 +1,110 @@
# OpenClaw (iOS)
# OpenClaw iOS (Super Alpha)
This is an **alpha** iOS app that connects to an OpenClaw Gateway as a `role: node`.
NO TEST FLIGHT AVAILABLE AT THIS POINT
Expect rough edges:
This iPhone app is super-alpha and internal-use only. It connects to an OpenClaw Gateway as a `role: node`.
- UI and onboarding are changing quickly.
- Background behavior is not stable yet (foreground app is the supported mode right now).
- Permissions are opt-in and the app should be treated as sensitive while we harden it.
## Distribution Status
## What It Does
NO TEST FLIGHT AVAILABLE AT THIS POINT
- Connects to a Gateway over `ws://` / `wss://`
- Pairs a new device (approved from your bot)
- Exposes phone services as node commands (camera, location, photos, calendar, reminders, etc; gated by iOS permissions)
- Provides Talk + Chat surfaces (alpha)
- Current distribution: local/manual deploy from source via Xcode.
- App Store flow is not part of the current internal development path.
## Pairing (Recommended Flow)
## Super-Alpha Disclaimer
If your Gateway has the `device-pair` plugin installed:
- Breaking changes are expected.
- UI and onboarding flows can change without migration guarantees.
- Foreground use is the only reliable mode right now.
- Treat this build as sensitive while permissions and background behavior are still being hardened.
1. In Telegram, message your bot: `/pair`
2. Copy the **setup code** message
3. On iOS: OpenClaw → Settings → Gateway → paste setup code → Connect
4. Back in Telegram: `/pair approve`
## Exact Xcode Manual Deploy Flow
## Build And Run
Prereqs:
- Xcode (current stable)
- `pnpm`
- `xcodegen`
From the repo root:
1. Prereqs:
- Xcode 16+
- `pnpm`
- `xcodegen`
- Apple Development signing set up in Xcode
2. From repo root:
```bash
pnpm install
./scripts/ios-configure-signing.sh
cd apps/ios
xcodegen generate
open OpenClaw.xcodeproj
```
3. In Xcode:
- Scheme: `OpenClaw`
- Destination: connected iPhone (recommended for real behavior)
- Build configuration: `Debug`
- Run (`Product` -> `Run`)
4. If signing fails on a personal team:
- Use unique local bundle IDs via `apps/ios/LocalSigning.xcconfig`.
- Start from `apps/ios/LocalSigning.xcconfig.example`.
Shortcut command (same flow + open project):
```bash
pnpm ios:open
```
Then in Xcode:
## APNs Expectations For Local/Manual Builds
1. Select the `OpenClaw` scheme
2. Select a simulator or a connected device
3. Run
- The app calls `registerForRemoteNotifications()` at launch.
- `apps/ios/Sources/OpenClaw.entitlements` sets `aps-environment` to `development`.
- APNs token registration to gateway happens only after gateway connection (`push.apns.register`).
- 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`).
- Debug builds register as APNs sandbox; Release builds use production.
If you're using a personal Apple Development team, you may need to change the bundle identifier in Xcode to a unique value so signing succeeds.
## What Works Now (Concrete)
## Build From CLI
- Pairing via setup code flow (`/pair` then `/pair approve` in Telegram).
- Gateway connection via discovery or manual host/port with TLS fingerprint trust prompt.
- Chat + Talk surfaces through the operator gateway session.
- iPhone node commands in foreground: camera snap/clip, canvas present/navigate/eval/snapshot, screen record, location, contacts, calendar, reminders, photos, motion, local notifications.
- Share extension deep-link forwarding into the connected gateway session.
```bash
pnpm ios:build
```
## Known Issues / Limitations / Problems
## Tests
- Foreground-first: iOS can suspend sockets in background; reconnect recovery is still being tuned.
- Background command limits are strict: `canvas.*`, `camera.*`, `screen.*`, and `talk.*` are blocked when backgrounded.
- Background location requires `Always` location permission.
- Pairing/auth errors intentionally pause reconnect loops until a human fixes auth/pairing state.
- Voice Wake and Talk contend for the same microphone; Talk suppresses wake capture while active.
- APNs reliability depends on local signing/provisioning/topic alignment.
- Expect rough UX edges and occasional reconnect churn during active development.
```bash
cd apps/ios
xcodegen generate
xcodebuild test -project OpenClaw.xcodeproj -scheme OpenClaw -destination "platform=iOS Simulator,name=iPhone 17"
```
## Current In-Progress Workstream
## Shared Code
Automatic wake/reconnect hardening:
- `apps/shared/OpenClawKit` contains the shared transport/types used by the iOS app.
- improve wake/resume behavior across scene transitions
- reduce dead-socket states after background -> foreground
- tighten node/operator session reconnect coordination
- reduce manual recovery steps after transient network failures
## Debugging Checklist
1. Confirm build/signing baseline:
- regenerate project (`xcodegen generate`)
- verify selected team + bundle IDs
2. In app `Settings -> Gateway`:
- confirm status text, server, and remote address
- verify whether status shows pairing/auth gating
3. If pairing is required:
- run `/pair approve` from Telegram, then reconnect
4. If discovery is flaky:
- enable `Discovery Debug Logs`
- inspect `Settings -> Gateway -> Discovery Logs`
5. If network path is unclear:
- switch to manual host/port + TLS in Gateway Advanced settings
6. In Xcode console, filter for subsystem/category signals:
- `ai.openclaw.ios`
- `GatewayDiag`
- `APNs registration failed`
7. Validate background expectations:
- repro in foreground first
- then test background transitions and confirm reconnect on return

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>OpenClaw Share</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.18</string>
<key>CFBundleVersion</key>
<string>20260218</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>10</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,548 @@
import Foundation
import OpenClawKit
import os
import UIKit
import UniformTypeIdentifiers
final class ShareViewController: UIViewController {
private struct ShareAttachment: Codable {
var type: String
var mimeType: String
var fileName: String
var content: String
}
private struct ExtractedShareContent {
var payload: SharedContentPayload
var attachments: [ShareAttachment]
}
private let logger = Logger(subsystem: "ai.openclaw.ios", category: "ShareExtension")
private var statusLabel: UILabel?
private let draftTextView = UITextView()
private let sendButton = UIButton(type: .system)
private let cancelButton = UIButton(type: .system)
private var didPrepareDraft = false
private var isSending = false
private var pendingAttachments: [ShareAttachment] = []
override func viewDidLoad() {
super.viewDidLoad()
self.preferredContentSize = CGSize(width: UIScreen.main.bounds.width, height: 420)
self.setupUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard !self.didPrepareDraft else { return }
self.didPrepareDraft = true
Task { await self.prepareDraft() }
}
private func setupUI() {
self.view.backgroundColor = .systemBackground
self.draftTextView.translatesAutoresizingMaskIntoConstraints = false
self.draftTextView.font = .preferredFont(forTextStyle: .body)
self.draftTextView.backgroundColor = UIColor.secondarySystemBackground
self.draftTextView.layer.cornerRadius = 10
self.draftTextView.textContainerInset = UIEdgeInsets(top: 12, left: 10, bottom: 12, right: 10)
self.sendButton.translatesAutoresizingMaskIntoConstraints = false
self.sendButton.setTitle("Send to OpenClaw", for: .normal)
self.sendButton.titleLabel?.font = .preferredFont(forTextStyle: .headline)
self.sendButton.addTarget(self, action: #selector(self.handleSendTap), for: .touchUpInside)
self.sendButton.isEnabled = false
self.cancelButton.translatesAutoresizingMaskIntoConstraints = false
self.cancelButton.setTitle("Cancel", for: .normal)
self.cancelButton.addTarget(self, action: #selector(self.handleCancelTap), for: .touchUpInside)
let buttons = UIStackView(arrangedSubviews: [self.cancelButton, self.sendButton])
buttons.translatesAutoresizingMaskIntoConstraints = false
buttons.axis = .horizontal
buttons.alignment = .fill
buttons.distribution = .fillEqually
buttons.spacing = 12
self.view.addSubview(self.draftTextView)
self.view.addSubview(buttons)
NSLayoutConstraint.activate([
self.draftTextView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 14),
self.draftTextView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 14),
self.draftTextView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -14),
self.draftTextView.bottomAnchor.constraint(equalTo: buttons.topAnchor, constant: -12),
buttons.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 14),
buttons.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -14),
buttons.bottomAnchor.constraint(equalTo: self.view.keyboardLayoutGuide.topAnchor, constant: -8),
buttons.heightAnchor.constraint(equalToConstant: 44),
])
}
private func prepareDraft() async {
let traceId = UUID().uuidString
ShareGatewayRelaySettings.saveLastEvent("Share opened.")
self.showStatus("Preparing share…")
self.logger.info("share begin trace=\(traceId, privacy: .public)")
let extracted = await self.extractSharedContent()
let payload = extracted.payload
self.pendingAttachments = extracted.attachments
self.logger.info(
"share payload trace=\(traceId, privacy: .public) titleChars=\(payload.title?.count ?? 0) textChars=\(payload.text?.count ?? 0) hasURL=\(payload.url != nil) imageAttachments=\(self.pendingAttachments.count)"
)
let message = self.composeDraft(from: payload)
await MainActor.run {
self.draftTextView.text = message
self.sendButton.isEnabled = true
self.draftTextView.becomeFirstResponder()
}
if message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
ShareGatewayRelaySettings.saveLastEvent("Share ready: waiting for message input.")
self.showStatus("Add a message, then tap Send.")
} else {
ShareGatewayRelaySettings.saveLastEvent("Share ready: draft prepared.")
self.showStatus("Edit text, then tap Send.")
}
}
@objc
private func handleSendTap() {
guard !self.isSending else { return }
Task { await self.sendCurrentDraft() }
}
@objc
private func handleCancelTap() {
self.extensionContext?.completeRequest(returningItems: nil)
}
private func sendCurrentDraft() async {
let message = await MainActor.run { self.draftTextView.text ?? "" }
let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
ShareGatewayRelaySettings.saveLastEvent("Share blocked: message is empty.")
self.showStatus("Message is empty.")
return
}
await MainActor.run {
self.isSending = true
self.sendButton.isEnabled = false
self.cancelButton.isEnabled = false
}
self.showStatus("Sending to OpenClaw gateway…")
ShareGatewayRelaySettings.saveLastEvent("Sending to gateway…")
do {
try await self.sendMessageToGateway(trimmed, attachments: self.pendingAttachments)
ShareGatewayRelaySettings.saveLastEvent(
"Sent to gateway (\(trimmed.count) chars, \(self.pendingAttachments.count) attachment(s)).")
self.showStatus("Sent to OpenClaw.")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) {
self.extensionContext?.completeRequest(returningItems: nil)
}
} catch {
self.logger.error("share send failed reason=\(error.localizedDescription, privacy: .public)")
ShareGatewayRelaySettings.saveLastEvent("Send failed: \(error.localizedDescription)")
self.showStatus("Send failed: \(error.localizedDescription)")
await MainActor.run {
self.isSending = false
self.sendButton.isEnabled = true
self.cancelButton.isEnabled = true
}
}
}
private func sendMessageToGateway(_ message: String, attachments: [ShareAttachment]) async throws {
guard let config = ShareGatewayRelaySettings.loadConfig() else {
throw NSError(
domain: "OpenClawShare",
code: 10,
userInfo: [NSLocalizedDescriptionKey: "OpenClaw is not connected to a gateway yet."])
}
guard let url = URL(string: config.gatewayURLString) else {
throw NSError(
domain: "OpenClawShare",
code: 11,
userInfo: [NSLocalizedDescriptionKey: "Invalid saved gateway URL."])
}
let gateway = GatewayNodeSession()
defer {
Task { await gateway.disconnect() }
}
let makeOptions: (String) -> GatewayConnectOptions = { clientId in
GatewayConnectOptions(
role: "node",
scopes: [],
caps: [],
commands: [],
permissions: [:],
clientId: clientId,
clientMode: "node",
clientDisplayName: "OpenClaw Share",
includeDeviceIdentity: false)
}
do {
try await gateway.connect(
url: url,
token: config.token,
password: config.password,
connectOptions: makeOptions("openclaw-ios"),
sessionBox: nil,
onConnected: {},
onDisconnected: { _ in },
onInvoke: { req in
BridgeInvokeResponse(
id: req.id,
ok: false,
error: OpenClawNodeError(
code: .invalidRequest,
message: "share extension does not support node invoke"))
})
} catch {
let expectsLegacyClientId = self.shouldRetryWithLegacyClientId(error)
guard expectsLegacyClientId else { throw error }
try await gateway.connect(
url: url,
token: config.token,
password: config.password,
connectOptions: makeOptions("moltbot-ios"),
sessionBox: nil,
onConnected: {},
onDisconnected: { _ in },
onInvoke: { req in
BridgeInvokeResponse(
id: req.id,
ok: false,
error: OpenClawNodeError(
code: .invalidRequest,
message: "share extension does not support node invoke"))
})
}
struct AgentRequestPayload: Codable {
var message: String
var sessionKey: String?
var thinking: String
var deliver: Bool
var attachments: [ShareAttachment]?
var receipt: Bool
var receiptText: String?
var to: String?
var channel: String?
var timeoutSeconds: Int?
var key: String?
}
let deliveryChannel = config.deliveryChannel?.trimmingCharacters(in: .whitespacesAndNewlines)
let deliveryTo = config.deliveryTo?.trimmingCharacters(in: .whitespacesAndNewlines)
let canDeliverToRoute = (deliveryChannel?.isEmpty == false) && (deliveryTo?.isEmpty == false)
let params = AgentRequestPayload(
message: message,
sessionKey: config.sessionKey,
thinking: "low",
deliver: canDeliverToRoute,
attachments: attachments.isEmpty ? nil : attachments,
receipt: canDeliverToRoute,
receiptText: canDeliverToRoute ? "Just received your iOS share + request, working on it." : nil,
to: canDeliverToRoute ? deliveryTo : nil,
channel: canDeliverToRoute ? deliveryChannel : nil,
timeoutSeconds: nil,
key: UUID().uuidString)
let data = try JSONEncoder().encode(params)
guard let json = String(data: data, encoding: .utf8) else {
throw NSError(
domain: "OpenClawShare",
code: 12,
userInfo: [NSLocalizedDescriptionKey: "Failed to encode chat payload."])
}
struct NodeEventParams: Codable {
var event: String
var payloadJSON: String
}
let eventData = try JSONEncoder().encode(NodeEventParams(event: "agent.request", payloadJSON: json))
guard let nodeEventParams = String(data: eventData, encoding: .utf8) else {
throw NSError(
domain: "OpenClawShare",
code: 13,
userInfo: [NSLocalizedDescriptionKey: "Failed to encode node event payload."])
}
_ = try await gateway.request(method: "node.event", paramsJSON: nodeEventParams, timeoutSeconds: 25)
}
private func shouldRetryWithLegacyClientId(_ error: Error) -> Bool {
if let gatewayError = error as? GatewayResponseError {
let code = gatewayError.code.lowercased()
let message = gatewayError.message.lowercased()
let pathValue = (gatewayError.details["path"]?.value as? String)?.lowercased() ?? ""
let mentionsClientIdPath =
message.contains("/client/id") || message.contains("client id")
|| pathValue.contains("/client/id")
let isInvalidConnectParams =
(code.contains("invalid") && code.contains("connect"))
|| message.contains("invalid connect params")
if isInvalidConnectParams && mentionsClientIdPath {
return true
}
}
let text = error.localizedDescription.lowercased()
return text.contains("invalid connect params")
&& (text.contains("/client/id") || text.contains("client id"))
}
private func showStatus(_ text: String) {
DispatchQueue.main.async {
let label: UILabel
if let existing = self.statusLabel {
label = existing
} else {
let newLabel = UILabel()
newLabel.translatesAutoresizingMaskIntoConstraints = false
newLabel.numberOfLines = 0
newLabel.textAlignment = .center
newLabel.font = .preferredFont(forTextStyle: .body)
newLabel.textColor = .label
newLabel.backgroundColor = UIColor.systemBackground.withAlphaComponent(0.92)
newLabel.layer.cornerRadius = 12
newLabel.clipsToBounds = true
newLabel.layoutMargins = UIEdgeInsets(top: 12, left: 14, bottom: 12, right: 14)
self.view.addSubview(newLabel)
NSLayoutConstraint.activate([
newLabel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 18),
newLabel.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -18),
newLabel.bottomAnchor.constraint(equalTo: self.sendButton.topAnchor, constant: -10),
])
self.statusLabel = newLabel
label = newLabel
}
label.text = " \(text) "
}
}
private func composeDraft(from payload: SharedContentPayload) -> String {
var lines: [String] = []
let title = self.sanitizeDraftFragment(payload.title)
let text = self.sanitizeDraftFragment(payload.text)
let url = payload.url?.absoluteString.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if let title, !title.isEmpty { lines.append(title) }
if let text, !text.isEmpty { lines.append(text) }
if !url.isEmpty { lines.append(url) }
return lines.joined(separator: "\n\n")
}
private func sanitizeDraftFragment(_ raw: String?) -> String? {
guard let raw else { return nil }
let banned = [
"shared from ios.",
"text:",
"shared attachment(s):",
"please help me with this.",
"please help me with this.w",
]
let cleanedLines = raw
.components(separatedBy: .newlines)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { line in
guard !line.isEmpty else { return false }
let lowered = line.lowercased()
return !banned.contains { lowered == $0 || lowered.hasPrefix($0) }
}
let cleaned = cleanedLines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
return cleaned.isEmpty ? nil : cleaned
}
private func extractSharedContent() async -> ExtractedShareContent {
guard let items = self.extensionContext?.inputItems as? [NSExtensionItem] else {
return ExtractedShareContent(
payload: SharedContentPayload(title: nil, url: nil, text: nil),
attachments: [])
}
var title: String?
var sharedURL: URL?
var sharedText: String?
var imageCount = 0
var videoCount = 0
var fileCount = 0
var unknownCount = 0
var attachments: [ShareAttachment] = []
let maxImageAttachments = 3
for item in items {
if title == nil {
title = item.attributedTitle?.string ?? item.attributedContentText?.string
}
for provider in item.attachments ?? [] {
if sharedURL == nil {
sharedURL = await self.loadURL(from: provider)
}
if sharedText == nil {
sharedText = await self.loadText(from: provider)
}
if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
imageCount += 1
if attachments.count < maxImageAttachments,
let attachment = await self.loadImageAttachment(from: provider, index: attachments.count)
{
attachments.append(attachment)
}
} else if provider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
videoCount += 1
} else if provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) {
fileCount += 1
} else {
unknownCount += 1
}
}
}
_ = imageCount
_ = videoCount
_ = fileCount
_ = unknownCount
return ExtractedShareContent(
payload: SharedContentPayload(title: title, url: sharedURL, text: sharedText),
attachments: attachments)
}
private func loadImageAttachment(from provider: NSItemProvider, index: Int) async -> ShareAttachment? {
let imageUTI = self.preferredImageTypeIdentifier(from: provider) ?? UTType.image.identifier
guard let rawData = await self.loadDataValue(from: provider, typeIdentifier: imageUTI) else {
return nil
}
let maxBytes = 5_000_000
guard let image = UIImage(data: rawData),
let data = self.normalizedJPEGData(from: image, maxBytes: maxBytes)
else {
return nil
}
return ShareAttachment(
type: "image",
mimeType: "image/jpeg",
fileName: "shared-image-\(index + 1).jpg",
content: data.base64EncodedString())
}
private func preferredImageTypeIdentifier(from provider: NSItemProvider) -> String? {
for identifier in provider.registeredTypeIdentifiers {
guard let utType = UTType(identifier) else { continue }
if utType.conforms(to: .image) {
return identifier
}
}
return nil
}
private func normalizedJPEGData(from image: UIImage, maxBytes: Int) -> Data? {
var quality: CGFloat = 0.9
while quality >= 0.4 {
if let data = image.jpegData(compressionQuality: quality), data.count <= maxBytes {
return data
}
quality -= 0.1
}
guard let fallback = image.jpegData(compressionQuality: 0.35) else { return nil }
if fallback.count <= maxBytes { return fallback }
return nil
}
private func loadURL(from provider: NSItemProvider) async -> URL? {
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
if let url = await self.loadURLValue(
from: provider,
typeIdentifier: UTType.url.identifier)
{
return url
}
}
if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
if let text = await self.loadTextValue(from: provider, typeIdentifier: UTType.text.identifier),
let url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines)),
url.scheme != nil
{
return url
}
}
return nil
}
private func loadText(from provider: NSItemProvider) async -> String? {
if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
if let text = await self.loadTextValue(from: provider, typeIdentifier: UTType.plainText.identifier) {
return text
}
}
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
if let url = await self.loadURLValue(from: provider, typeIdentifier: UTType.url.identifier) {
return url.absoluteString
}
}
return nil
}
private func loadURLValue(from provider: NSItemProvider, typeIdentifier: String) async -> URL? {
await withCheckedContinuation { continuation in
provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in
if let url = item as? URL {
continuation.resume(returning: url)
return
}
if let str = item as? String, let url = URL(string: str) {
continuation.resume(returning: url)
return
}
if let ns = item as? NSString, let url = URL(string: ns as String) {
continuation.resume(returning: url)
return
}
continuation.resume(returning: nil)
}
}
}
private func loadTextValue(from provider: NSItemProvider, typeIdentifier: String) async -> String? {
await withCheckedContinuation { continuation in
provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in
if let text = item as? String {
continuation.resume(returning: text)
return
}
if let text = item as? NSString {
continuation.resume(returning: text as String)
return
}
if let text = item as? NSAttributedString {
continuation.resume(returning: text.string)
return
}
continuation.resume(returning: nil)
}
}
}
private func loadDataValue(from provider: NSItemProvider, typeIdentifier: String) async -> Data? {
await withCheckedContinuation { continuation in
provider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { data, _ in
continuation.resume(returning: data)
}
}
}
}

17
apps/ios/Signing.xcconfig Normal file
View File

@@ -0,0 +1,17 @@
// Default signing values for shared/repo builds.
// Auto-selected local team overrides live in .local-signing.xcconfig (git-ignored).
// Manual local overrides can go in LocalSigning.xcconfig (git-ignored).
OPENCLAW_CODE_SIGN_STYLE = Manual
OPENCLAW_DEVELOPMENT_TEAM = Y5PE65HELJ
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios
OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.ios.share
OPENCLAW_APP_PROFILE = ai.openclaw.ios Development
OPENCLAW_SHARE_PROFILE = ai.openclaw.ios.share Development
// Keep local includes after defaults: xcconfig is evaluated top-to-bottom,
// so later assignments in local files override the defaults above.
#include? ".local-signing.xcconfig"
#include? "LocalSigning.xcconfig"

View File

@@ -6,7 +6,7 @@ final class CalendarService: CalendarServicing {
func events(params: OpenClawCalendarEventsParams) async throws -> OpenClawCalendarEventsPayload {
let store = EKEventStore()
let status = EKEventStore.authorizationStatus(for: .event)
let authorized = await Self.ensureAuthorization(store: store, status: status)
let authorized = EventKitAuthorization.allowsRead(status: status)
guard authorized else {
throw NSError(domain: "Calendar", code: 1, userInfo: [
NSLocalizedDescriptionKey: "CALENDAR_PERMISSION_REQUIRED: grant Calendar permission",
@@ -39,7 +39,7 @@ final class CalendarService: CalendarServicing {
func add(params: OpenClawCalendarAddParams) async throws -> OpenClawCalendarAddPayload {
let store = EKEventStore()
let status = EKEventStore.authorizationStatus(for: .event)
let authorized = await Self.ensureWriteAuthorization(store: store, status: status)
let authorized = EventKitAuthorization.allowsWrite(status: status)
guard authorized else {
throw NSError(domain: "Calendar", code: 2, userInfo: [
NSLocalizedDescriptionKey: "CALENDAR_PERMISSION_REQUIRED: grant Calendar permission",
@@ -95,38 +95,6 @@ final class CalendarService: CalendarServicing {
return OpenClawCalendarAddPayload(event: payload)
}
private static func ensureAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool {
switch status {
case .authorized:
return true
case .notDetermined:
// Dont prompt during node.invoke; prompts block the invoke and lead to timeouts.
return false
case .restricted, .denied:
return false
case .fullAccess:
return true
case .writeOnly:
return false
@unknown default:
return false
}
}
private static func ensureWriteAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool {
switch status {
case .authorized, .fullAccess, .writeOnly:
return true
case .notDetermined:
// Dont prompt during node.invoke; prompts block the invoke and lead to timeouts.
return false
case .restricted, .denied:
return false
@unknown default:
return false
}
}
private static func resolveCalendar(
store: EKEventStore,
calendarId: String?,

View File

@@ -93,14 +93,10 @@ actor CameraController {
}
withExtendedLifetime(delegate) {}
let maxPayloadBytes = 5 * 1024 * 1024
// Base64 inflates payloads by ~4/3; cap encoded bytes so the payload stays under 5MB (API limit).
let maxEncodedBytes = (maxPayloadBytes / 4) * 3
let res = try JPEGTranscoder.transcodeToJPEG(
imageData: rawData,
let res = try PhotoCapture.transcodeJPEGForGateway(
rawData: rawData,
maxWidthPx: maxWidth,
quality: quality,
maxBytes: maxEncodedBytes)
quality: quality)
return (
format: format.rawValue,
@@ -335,8 +331,8 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat
func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?)
{
error: Error?
) {
guard !self.didResume else { return }
self.didResume = true
@@ -364,8 +360,8 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat
func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings,
error: Error?)
{
error: Error?
) {
guard let error else { return }
guard !self.didResume else { return }
self.didResume = true

View File

@@ -2,8 +2,10 @@ import OpenClawChatUI
import OpenClawKit
import OpenClawProtocol
import Foundation
import OSLog
struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable {
private static let logger = Logger(subsystem: "ai.openclaw", category: "ios.chat.transport")
private let gateway: GatewayNodeSession
init(gateway: GatewayNodeSession) {
@@ -33,10 +35,8 @@ struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable {
}
func setActiveSessionKey(_ sessionKey: String) async throws {
struct Subscribe: Codable { var sessionKey: String }
let data = try JSONEncoder().encode(Subscribe(sessionKey: sessionKey))
let json = String(data: data, encoding: .utf8)
await self.gateway.sendEvent(event: "chat.subscribe", payloadJSON: json)
// Operator clients receive chat events without node-style subscriptions.
// (chat.subscribe is a node event, not an operator RPC method.)
}
func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
@@ -54,6 +54,7 @@ struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable {
idempotencyKey: String,
attachments: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse
{
Self.logger.info("chat.send start sessionKey=\(sessionKey, privacy: .public) len=\(message.count, privacy: .public) attachments=\(attachments.count, privacy: .public)")
struct Params: Codable {
var sessionKey: String
var message: String
@@ -72,8 +73,15 @@ struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable {
idempotencyKey: idempotencyKey)
let data = try JSONEncoder().encode(params)
let json = String(data: data, encoding: .utf8)
let res = try await self.gateway.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 35)
return try JSONDecoder().decode(OpenClawChatSendResponse.self, from: res)
do {
let res = try await self.gateway.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 35)
let decoded = try JSONDecoder().decode(OpenClawChatSendResponse.self, from: res)
Self.logger.info("chat.send ok runId=\(decoded.runId, privacy: .public)")
return decoded
} catch {
Self.logger.error("chat.send failed \(error.localizedDescription, privacy: .public)")
throw error
}
}
func requestHealth(timeoutMs: Int) async throws -> Bool {

View File

@@ -0,0 +1,34 @@
import EventKit
enum EventKitAuthorization {
static func allowsRead(status: EKAuthorizationStatus) -> Bool {
switch status {
case .authorized, .fullAccess:
return true
case .writeOnly:
return false
case .notDetermined:
// Dont prompt during node.invoke; prompts block the invoke and lead to timeouts.
return false
case .restricted, .denied:
return false
@unknown default:
return false
}
}
static func allowsWrite(status: EKAuthorizationStatus) -> Bool {
switch status {
case .authorized, .fullAccess, .writeOnly:
return true
case .notDetermined:
// Dont prompt during node.invoke; prompts block the invoke and lead to timeouts.
return false
case .restricted, .denied:
return false
@unknown default:
return false
}
}
}

View File

@@ -72,32 +72,55 @@ final class GatewayConnectionController {
}
}
func connect(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async {
func allowAutoConnectAgain() {
self.didAutoConnect = false
self.maybeAutoConnect()
}
func restartDiscovery() {
self.discovery.stop()
self.didAutoConnect = false
self.discovery.start()
self.updateFromDiscovery()
}
/// Returns `nil` when a connect attempt was started, otherwise returns a user-facing error.
func connectWithDiagnostics(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async -> String? {
await self.connectDiscoveredGateway(gateway)
}
private func connectDiscoveredGateway(
_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async
_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async -> String?
{
let instanceId = UserDefaults.standard.string(forKey: "node.instanceId")?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if instanceId.isEmpty {
return "Missing instanceId (node.instanceId). Try restarting the app."
}
let token = GatewaySettingsStore.loadGatewayToken(instanceId: instanceId)
let password = GatewaySettingsStore.loadGatewayPassword(instanceId: instanceId)
// Resolve the service endpoint (SRV/A/AAAA). TXT is unauthenticated; do not route via TXT.
guard let target = await self.resolveServiceEndpoint(gateway.endpoint) else { return }
guard let target = await self.resolveServiceEndpoint(gateway.endpoint) else {
return "Failed to resolve the discovered gateway endpoint."
}
let stableID = gateway.stableID
// Discovery is a LAN operation; refuse unauthenticated plaintext connects.
let tlsRequired = true
let stored = GatewayTLSStore.loadFingerprint(stableID: stableID)
guard gateway.tlsEnabled || stored != nil else { return }
guard gateway.tlsEnabled || stored != nil else {
return "Discovered gateway is missing TLS and no trusted fingerprint is stored."
}
if tlsRequired, stored == nil {
guard let url = self.buildGatewayURL(host: target.host, port: target.port, useTLS: true)
else { return }
guard let fp = await self.probeTLSFingerprint(url: url) else { return }
else { return "Failed to build TLS URL for trust verification." }
guard let fp = await self.probeTLSFingerprint(url: url) else {
return "Failed to read TLS fingerprint from discovered gateway."
}
self.pendingTrustConnect = (url: url, stableID: stableID, isManual: false)
self.pendingTrustPrompt = TrustPrompt(
stableID: stableID,
@@ -107,7 +130,7 @@ final class GatewayConnectionController {
fingerprintSha256: fp,
isManual: false)
self.appModel?.gatewayStatusText = "Verify gateway TLS fingerprint"
return
return nil
}
let tlsParams = stored.map { fp in
@@ -118,7 +141,7 @@ final class GatewayConnectionController {
host: target.host,
port: target.port,
useTLS: tlsParams?.required == true)
else { return }
else { return "Failed to build discovered gateway URL." }
GatewaySettingsStore.saveLastGatewayConnectionDiscovered(stableID: stableID, useTLS: true)
self.didAutoConnect = true
self.startAutoConnect(
@@ -127,6 +150,11 @@ final class GatewayConnectionController {
tls: tlsParams,
token: token,
password: password)
return nil
}
func connect(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async {
_ = await self.connectWithDiagnostics(gateway)
}
func connectManual(host: String, port: Int, useTLS: Bool) async {
@@ -490,6 +518,125 @@ final class GatewayConnectionController {
}
}
private func resolveHostPortFromBonjourEndpoint(_ endpoint: NWEndpoint) async -> (host: String, port: Int)? {
switch endpoint {
case let .hostPort(host, port):
return (host: host.debugDescription, port: Int(port.rawValue))
case let .service(name, type, domain, _):
return await Self.resolveBonjourServiceToHostPort(name: name, type: type, domain: domain)
default:
return nil
}
}
private static func resolveBonjourServiceToHostPort(
name: String,
type: String,
domain: String,
timeoutSeconds: TimeInterval = 3.0
) async -> (host: String, port: Int)? {
// NetService callbacks are delivered via a run loop. If we resolve from a thread without one,
// we can end up never receiving callbacks, which in turn leaks the continuation and leaves
// the UI stuck "connecting". Keep the whole lifecycle on the main run loop and always
// resume the continuation exactly once (timeout/cancel safe).
@MainActor
final class Resolver: NSObject, @preconcurrency NetServiceDelegate {
private var cont: CheckedContinuation<(host: String, port: Int)?, Never>?
private let service: NetService
private var timeoutTask: Task<Void, Never>?
private var finished = false
init(cont: CheckedContinuation<(host: String, port: Int)?, Never>, service: NetService) {
self.cont = cont
self.service = service
super.init()
}
func start(timeoutSeconds: TimeInterval) {
self.service.delegate = self
self.service.schedule(in: .main, forMode: .default)
// NetService has its own timeout, but we keep a manual one as a backstop in case
// callbacks never arrive (e.g. local network permission issues).
self.timeoutTask = Task { @MainActor [weak self] in
guard let self else { return }
let ns = UInt64(max(0.1, timeoutSeconds) * 1_000_000_000)
try? await Task.sleep(nanoseconds: ns)
self.finish(nil)
}
self.service.resolve(withTimeout: timeoutSeconds)
}
func netServiceDidResolveAddress(_ sender: NetService) {
self.finish(Self.extractHostPort(sender))
}
func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
_ = errorDict // currently best-effort; callers surface a generic failure
self.finish(nil)
}
private func finish(_ result: (host: String, port: Int)?) {
guard !self.finished else { return }
self.finished = true
self.timeoutTask?.cancel()
self.timeoutTask = nil
self.service.stop()
self.service.remove(from: .main, forMode: .default)
let c = self.cont
self.cont = nil
c?.resume(returning: result)
}
private static func extractHostPort(_ svc: NetService) -> (host: String, port: Int)? {
let port = svc.port
if let host = svc.hostName?.trimmingCharacters(in: .whitespacesAndNewlines), !host.isEmpty {
return (host: host, port: port)
}
guard let addrs = svc.addresses else { return nil }
for addrData in addrs {
let host = addrData.withUnsafeBytes { ptr -> String? in
guard let base = ptr.baseAddress, !ptr.isEmpty else { return nil }
var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let rc = getnameinfo(
base.assumingMemoryBound(to: sockaddr.self),
socklen_t(ptr.count),
&buffer,
socklen_t(buffer.count),
nil,
0,
NI_NUMERICHOST)
guard rc == 0 else { return nil }
return String(cString: buffer)
}
if let host, !host.isEmpty {
return (host: host, port: port)
}
}
return nil
}
}
return await withCheckedContinuation { cont in
Task { @MainActor in
let service = NetService(domain: domain, type: type, name: name)
let resolver = Resolver(cont: cont, service: service)
// Keep the resolver alive for the lifetime of the NetService resolve.
objc_setAssociatedObject(service, "resolver", resolver, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
resolver.start(timeoutSeconds: timeoutSeconds)
}
}
}
private func buildGatewayURL(host: String, port: Int, useTLS: Bool) -> URL? {
let scheme = useTLS ? "wss" : "ws"
var components = URLComponents()
@@ -582,6 +729,9 @@ final class GatewayConnectionController {
if locationMode != .off { caps.append(OpenClawCapability.location.rawValue) }
caps.append(OpenClawCapability.device.rawValue)
if WatchMessagingService.isSupportedOnDevice() {
caps.append(OpenClawCapability.watch.rawValue)
}
caps.append(OpenClawCapability.photos.rawValue)
caps.append(OpenClawCapability.contacts.rawValue)
caps.append(OpenClawCapability.calendar.rawValue)
@@ -625,6 +775,10 @@ final class GatewayConnectionController {
commands.append(OpenClawDeviceCommand.status.rawValue)
commands.append(OpenClawDeviceCommand.info.rawValue)
}
if caps.contains(OpenClawCapability.watch.rawValue) {
commands.append(OpenClawWatchCommand.status.rawValue)
commands.append(OpenClawWatchCommand.notify.rawValue)
}
if caps.contains(OpenClawCapability.photos.rawValue) {
commands.append(OpenClawPhotosCommand.latest.rawValue)
}
@@ -675,6 +829,12 @@ final class GatewayConnectionController {
permissions["motion"] =
motionStatus == .authorized || pedometerStatus == .authorized
let watchStatus = WatchMessagingService.currentStatusSnapshot()
permissions["watchSupported"] = watchStatus.supported
permissions["watchPaired"] = watchStatus.paired
permissions["watchAppInstalled"] = watchStatus.appInstalled
permissions["watchReachable"] = watchStatus.reachable
return permissions
}

View File

@@ -0,0 +1,71 @@
import Foundation
enum GatewayConnectionIssue: Equatable {
case none
case tokenMissing
case unauthorized
case pairingRequired(requestId: String?)
case network
case unknown(String)
var requestId: String? {
if case let .pairingRequired(requestId) = self {
return requestId
}
return nil
}
var needsAuthToken: Bool {
switch self {
case .tokenMissing, .unauthorized:
return true
default:
return false
}
}
var needsPairing: Bool {
if case .pairingRequired = self { return true }
return false
}
static func detect(from statusText: String) -> Self {
let trimmed = statusText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return .none }
let lower = trimmed.lowercased()
if lower.contains("pairing required") || lower.contains("not_paired") || lower.contains("not paired") {
return .pairingRequired(requestId: self.extractRequestId(from: trimmed))
}
if lower.contains("gateway token missing") {
return .tokenMissing
}
if lower.contains("unauthorized") {
return .unauthorized
}
if lower.contains("connection refused") ||
lower.contains("timed out") ||
lower.contains("network is unreachable") ||
lower.contains("cannot find host") ||
lower.contains("could not connect")
{
return .network
}
if lower.hasPrefix("gateway error:") {
return .unknown(trimmed)
}
return .none
}
private static func extractRequestId(from statusText: String) -> String? {
let marker = "requestId:"
guard let range = statusText.range(of: marker) else { return nil }
let suffix = statusText[range.upperBound...]
let trimmed = suffix.trimmingCharacters(in: .whitespacesAndNewlines)
let end = trimmed.firstIndex(where: { ch in
ch == ")" || ch.isWhitespace || ch == "," || ch == ";"
}) ?? trimmed.endIndex
let id = String(trimmed[..<end]).trimmingCharacters(in: .whitespacesAndNewlines)
return id.isEmpty ? nil : id
}
}

View File

@@ -136,43 +136,9 @@ final class GatewayDiscoveryModel {
}
private func updateStatusText() {
let states = Array(self.statesByDomain.values)
if states.isEmpty {
self.statusText = self.browsers.isEmpty ? "Idle" : "Setup"
return
}
if let failed = states.first(where: { state in
if case .failed = state { return true }
return false
}) {
if case let .failed(err) = failed {
self.statusText = "Failed: \(err)"
return
}
}
if let waiting = states.first(where: { state in
if case .waiting = state { return true }
return false
}) {
if case let .waiting(err) = waiting {
self.statusText = "Waiting: \(err)"
return
}
}
if states.contains(where: { if case .ready = $0 { true } else { false } }) {
self.statusText = "Searching…"
return
}
if states.contains(where: { if case .setup = $0 { true } else { false } }) {
self.statusText = "Setup"
return
}
self.statusText = "Searching…"
self.statusText = GatewayDiscoveryStatusText.make(
states: Array(self.statesByDomain.values),
hasBrowsers: !self.browsers.isEmpty)
}
private static func prettyState(_ state: NWBrowser.State) -> String {

View File

@@ -0,0 +1,113 @@
import SwiftUI
struct GatewayQuickSetupSheet: View {
@Environment(NodeAppModel.self) private var appModel
@Environment(GatewayConnectionController.self) private var gatewayController
@Environment(\.dismiss) private var dismiss
@AppStorage("onboarding.quickSetupDismissed") private var quickSetupDismissed: Bool = false
@State private var connecting: Bool = false
@State private var connectError: String?
var body: some View {
NavigationStack {
VStack(alignment: .leading, spacing: 16) {
Text("Connect to a Gateway?")
.font(.title2.bold())
if let candidate = self.bestCandidate {
VStack(alignment: .leading, spacing: 6) {
Text(verbatim: candidate.name)
.font(.headline)
Text(verbatim: candidate.debugID)
.font(.footnote)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 2) {
// Use verbatim strings so Bonjour-provided values can't be interpreted as
// localized format strings (which can crash with Objective-C exceptions).
Text(verbatim: "Discovery: \(self.gatewayController.discoveryStatusText)")
Text(verbatim: "Status: \(self.appModel.gatewayStatusText)")
Text(verbatim: "Node: \(self.appModel.nodeStatusText)")
Text(verbatim: "Operator: \(self.appModel.operatorStatusText)")
}
.font(.footnote)
.foregroundStyle(.secondary)
}
.padding(12)
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 14))
Button {
self.connectError = nil
self.connecting = true
Task {
let err = await self.gatewayController.connectWithDiagnostics(candidate)
await MainActor.run {
self.connecting = false
self.connectError = err
// If we kicked off a connect, leave the sheet up so the user can see status evolve.
}
}
} label: {
Group {
if self.connecting {
HStack(spacing: 8) {
ProgressView().progressViewStyle(.circular)
Text("Connecting…")
}
} else {
Text("Connect")
}
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.disabled(self.connecting)
if let connectError {
Text(connectError)
.font(.footnote)
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
Button {
self.dismiss()
} label: {
Text("Not now")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.disabled(self.connecting)
Toggle("Dont show this again", isOn: self.$quickSetupDismissed)
.padding(.top, 4)
} else {
Text("No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled.")
.foregroundStyle(.secondary)
}
Spacer()
}
.padding()
.navigationTitle("Quick Setup")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
self.quickSetupDismissed = true
self.dismiss()
} label: {
Text("Close")
}
}
}
}
}
private var bestCandidate: GatewayDiscoveryModel.DiscoveredGateway? {
// Prefer whatever discovery says is first; the list is already name-sorted.
self.gatewayController.gateways.first
}
}

View File

@@ -4,6 +4,7 @@ import os
enum GatewaySettingsStore {
private static let gatewayService = "ai.openclaw.gateway"
private static let nodeService = "ai.openclaw.node"
private static let talkService = "ai.openclaw.talk"
private static let instanceIdDefaultsKey = "node.instanceId"
private static let preferredGatewayStableIDDefaultsKey = "gateway.preferredStableID"
@@ -24,6 +25,7 @@ enum GatewaySettingsStore {
private static let instanceIdAccount = "instanceId"
private static let preferredGatewayStableIDAccount = "preferredStableID"
private static let lastDiscoveredGatewayStableIDAccount = "lastDiscoveredStableID"
private static let talkElevenLabsApiKeyAccount = "elevenlabs.apiKey"
static func bootstrapPersistence() {
self.ensureStableInstanceID()
@@ -143,6 +145,27 @@ enum GatewaySettingsStore {
case discovered
}
static func loadTalkElevenLabsApiKey() -> String? {
let value = KeychainStore.loadString(
service: self.talkService,
account: self.talkElevenLabsApiKeyAccount)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if value?.isEmpty == false { return value }
return nil
}
static func saveTalkElevenLabsApiKey(_ apiKey: String?) {
let trimmed = apiKey?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if trimmed.isEmpty {
_ = KeychainStore.delete(service: self.talkService, account: self.talkElevenLabsApiKeyAccount)
return
}
_ = KeychainStore.saveString(
trimmed,
service: self.talkService,
account: self.talkElevenLabsApiKeyAccount)
}
static func saveLastGatewayConnectionManual(host: String, port: Int, useTLS: Bool, stableID: String) {
let defaults = UserDefaults.standard
defaults.set(LastGatewayKind.manual.rawValue, forKey: self.lastGatewayKindDefaultsKey)
@@ -184,6 +207,25 @@ enum GatewaySettingsStore {
return .manual(host: host, port: port, useTLS: useTLS, stableID: stableID)
}
static func clearLastGatewayConnection(defaults: UserDefaults = .standard) {
defaults.removeObject(forKey: self.lastGatewayKindDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayHostDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayPortDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayTlsDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayStableIDDefaultsKey)
}
static func deleteGatewayCredentials(instanceId: String) {
let trimmed = instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
_ = KeychainStore.delete(
service: self.gatewayService,
account: self.gatewayTokenAccount(instanceId: trimmed))
_ = KeychainStore.delete(
service: self.gatewayService,
account: self.gatewayPasswordAccount(instanceId: trimmed))
}
static func loadGatewayClientIdOverride(stableID: String) -> String? {
let trimmedID = stableID.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedID.isEmpty else { return nil }

View File

@@ -0,0 +1,42 @@
import Foundation
struct GatewaySetupPayload: Codable {
var url: String?
var host: String?
var port: Int?
var tls: Bool?
var token: String?
var password: String?
}
enum GatewaySetupCode {
static func decode(raw: String) -> GatewaySetupPayload? {
if let payload = decodeFromJSON(raw) {
return payload
}
if let decoded = decodeBase64Payload(raw),
let payload = decodeFromJSON(decoded)
{
return payload
}
return nil
}
private static func decodeFromJSON(_ json: String) -> GatewaySetupPayload? {
guard let data = json.data(using: .utf8) else { return nil }
return try? JSONDecoder().decode(GatewaySetupPayload.self, from: data)
}
private static func decodeBase64Payload(_ raw: String) -> String? {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let normalized = trimmed
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let padding = normalized.count % 4
let padded = padding == 0 ? normalized : normalized + String(repeating: "=", count: 4 - padding)
guard let data = Data(base64Encoded: padded) else { return nil }
return String(data: data, encoding: .utf8)
}
}

View File

@@ -6,10 +6,10 @@ struct GatewayTrustPromptAlert: ViewModifier {
private var promptBinding: Binding<GatewayConnectionController.TrustPrompt?> {
Binding(
get: { self.gatewayController.pendingTrustPrompt },
set: { newValue in
if newValue == nil {
self.gatewayController.clearPendingTrustPrompt()
}
set: { _ in
// Keep pending trust state until explicit user action.
// `alert(item:)` may set the binding to nil during dismissal, which can race with
// the button handler and cause accept to no-op.
})
}
@@ -39,4 +39,3 @@ extension View {
self.modifier(GatewayTrustPromptAlert())
}
}

View File

@@ -0,0 +1,43 @@
import Foundation
import Network
import os
enum TCPProbe {
static func probe(host: String, port: Int, timeoutSeconds: Double, queueLabel: String) async -> Bool {
guard port >= 1, port <= 65535 else { return false }
guard let nwPort = NWEndpoint.Port(rawValue: UInt16(port)) else { return false }
let endpointHost = NWEndpoint.Host(host)
let connection = NWConnection(host: endpointHost, port: nwPort, using: .tcp)
return await withCheckedContinuation { cont in
let queue = DispatchQueue(label: queueLabel)
let finished = OSAllocatedUnfairLock(initialState: false)
let finish: @Sendable (Bool) -> Void = { ok in
let shouldResume = finished.withLock { flag -> Bool in
if flag { return false }
flag = true
return true
}
guard shouldResume else { return }
connection.cancel()
cont.resume(returning: ok)
}
connection.stateUpdateHandler = { state in
switch state {
case .ready:
finish(true)
case .failed, .cancelled:
finish(false)
default:
break
}
}
connection.start(queue: queue)
queue.asyncAfter(deadline: .now() + timeoutSeconds) { finish(false) }
}
}
}

View File

@@ -17,15 +17,26 @@
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.15</string>
<key>CFBundleVersion</key>
<string>20260215</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.18</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>ai.openclaw.ios</string>
<key>CFBundleURLSchemes</key>
<array>
<string>openclaw</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>20260218</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
@@ -51,6 +62,7 @@
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>remote-notification</string>
</array>
<key>UILaunchScreen</key>
<dict/>

View File

@@ -12,6 +12,10 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private var authContinuation: CheckedContinuation<CLAuthorizationStatus, Never>?
private var locationContinuation: CheckedContinuation<CLLocation, Swift.Error>?
private var updatesContinuation: AsyncStream<CLLocation>.Continuation?
private var isStreaming = false
private var significantLocationCallback: (@Sendable (CLLocation) -> Void)?
private var isMonitoringSignificantChanges = false
override init() {
super.init()
@@ -104,6 +108,56 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
}
}
func startLocationUpdates(
desiredAccuracy: OpenClawLocationAccuracy,
significantChangesOnly: Bool) -> AsyncStream<CLLocation>
{
self.stopLocationUpdates()
self.manager.desiredAccuracy = Self.accuracyValue(desiredAccuracy)
self.manager.pausesLocationUpdatesAutomatically = true
self.manager.allowsBackgroundLocationUpdates = true
self.isStreaming = true
if significantChangesOnly {
self.manager.startMonitoringSignificantLocationChanges()
} else {
self.manager.startUpdatingLocation()
}
return AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in
self.updatesContinuation = continuation
continuation.onTermination = { @Sendable _ in
Task { @MainActor in
self.stopLocationUpdates()
}
}
}
}
func stopLocationUpdates() {
guard self.isStreaming else { return }
self.isStreaming = false
self.manager.stopUpdatingLocation()
self.manager.stopMonitoringSignificantLocationChanges()
self.updatesContinuation?.finish()
self.updatesContinuation = nil
}
func startMonitoringSignificantLocationChanges(onUpdate: @escaping @Sendable (CLLocation) -> Void) {
self.significantLocationCallback = onUpdate
guard !self.isMonitoringSignificantChanges else { return }
self.isMonitoringSignificantChanges = true
self.manager.startMonitoringSignificantLocationChanges()
}
func stopMonitoringSignificantLocationChanges() {
guard self.isMonitoringSignificantChanges else { return }
self.isMonitoringSignificantChanges = false
self.significantLocationCallback = nil
self.manager.stopMonitoringSignificantLocationChanges()
}
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus
Task { @MainActor in
@@ -117,12 +171,22 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let locs = locations
Task { @MainActor in
guard let cont = self.locationContinuation else { return }
self.locationContinuation = nil
if let latest = locs.last {
cont.resume(returning: latest)
} else {
cont.resume(throwing: Error.unavailable)
// Resolve the one-shot continuation first (if any).
if let cont = self.locationContinuation {
self.locationContinuation = nil
if let latest = locs.last {
cont.resume(returning: latest)
} else {
cont.resume(throwing: Error.unavailable)
}
// Don't return also forward to significant-change callback below
// so both consumers receive updates when both are active.
}
if let callback = self.significantLocationCallback, let latest = locs.last {
callback(latest)
}
if let latest = locs.last, let updates = self.updatesContinuation {
updates.yield(latest)
}
}
}

View File

@@ -0,0 +1,38 @@
import CoreLocation
import Foundation
import OpenClawKit
/// Monitors significant location changes and pushes `location.update`
/// events to the gateway so the severance hook can determine whether
/// the user is at their configured work location.
@MainActor
enum SignificantLocationMonitor {
static func startIfNeeded(
locationService: any LocationServicing,
locationMode: OpenClawLocationMode,
gateway: GatewayNodeSession
) {
guard locationMode == .always else { return }
let status = locationService.authorizationStatus()
guard status == .authorizedAlways else { return }
locationService.startMonitoringSignificantLocationChanges { location in
struct Payload: Codable {
var lat: Double
var lon: Double
var accuracyMeters: Double
var source: String?
}
let payload = Payload(
lat: location.coordinate.latitude,
lon: location.coordinate.longitude,
accuracyMeters: location.horizontalAccuracy,
source: "ios-significant-location")
guard let data = try? JSONEncoder().encode(payload),
let json = String(data: data, encoding: .utf8)
else { return }
Task { @MainActor in
await gateway.sendEvent(event: "location.update", payloadJSON: json)
}
}
}
}

View File

@@ -61,37 +61,10 @@ extension NodeAppModel {
private static func probeTCP(url: URL, timeoutSeconds: Double) async -> Bool {
guard let host = url.host, !host.isEmpty else { return false }
let portInt = url.port ?? ((url.scheme ?? "").lowercased() == "wss" ? 443 : 80)
guard portInt >= 1, portInt <= 65535 else { return false }
guard let nwPort = NWEndpoint.Port(rawValue: UInt16(portInt)) else { return false }
let endpointHost = NWEndpoint.Host(host)
let connection = NWConnection(host: endpointHost, port: nwPort, using: .tcp)
return await withCheckedContinuation { cont in
let queue = DispatchQueue(label: "a2ui.preflight")
let finished = OSAllocatedUnfairLock(initialState: false)
let finish: @Sendable (Bool) -> Void = { ok in
let shouldResume = finished.withLock { flag -> Bool in
if flag { return false }
flag = true
return true
}
guard shouldResume else { return }
connection.cancel()
cont.resume(returning: ok)
}
connection.stateUpdateHandler = { state in
switch state {
case .ready:
finish(true)
case .failed, .cancelled:
finish(false)
default:
break
}
}
connection.start(queue: queue)
queue.asyncAfter(deadline: .now() + timeoutSeconds) { finish(false) }
}
return await TCPProbe.probe(
host: host,
port: portInt,
timeoutSeconds: timeoutSeconds,
queueLabel: "a2ui.preflight")
}
}

View File

@@ -2,6 +2,7 @@ import OpenClawChatUI
import OpenClawKit
import OpenClawProtocol
import Observation
import os
import SwiftUI
import UIKit
import UserNotifications
@@ -10,7 +11,6 @@ import UserNotifications
private struct NotificationCallError: Error, Sendable {
let message: String
}
// Ensures notification requests return promptly even if the system prompt blocks.
private final class NotificationInvokeLatch<T: Sendable>: @unchecked Sendable {
private let lock = NSLock()
@@ -37,10 +37,11 @@ private final class NotificationInvokeLatch<T: Sendable>: @unchecked Sendable {
cont?.resume(returning: response)
}
}
@MainActor
@Observable
final class NodeAppModel {
private let deepLinkLogger = Logger(subsystem: "ai.openclaw.ios", category: "DeepLink")
private let pushWakeLogger = Logger(subsystem: "ai.openclaw.ios", category: "PushWake")
enum CameraHUDKind {
case photo
case recording
@@ -53,35 +54,24 @@ final class NodeAppModel {
private let camera: any CameraServicing
private let screenRecorder: any ScreenRecordingServicing
var gatewayStatusText: String = "Offline"
var nodeStatusText: String = "Offline"
var operatorStatusText: String = "Offline"
var gatewayServerName: String?
var gatewayRemoteAddress: String?
var connectedGatewayID: String?
var gatewayAutoReconnectEnabled: Bool = true
// When the gateway requires pairing approval, we pause reconnect churn and show a stable UX.
// Reconnect loops (both our own and the underlying WebSocket watchdog) can otherwise generate
// multiple pending requests and cause the onboarding UI to "flip-flop".
var gatewayPairingPaused: Bool = false
var gatewayPairingRequestId: String?
var seamColorHex: String?
private var mainSessionBaseKey: String = "main"
var selectedAgentId: String?
var gatewayDefaultAgentId: String?
var gatewayAgents: [AgentSummary] = []
var mainSessionKey: String {
let base = SessionKey.normalizeMainKey(self.mainSessionBaseKey)
let agentId = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if agentId.isEmpty || (!defaultId.isEmpty && agentId == defaultId) { return base }
return SessionKey.makeAgentSessionKey(agentId: agentId, baseKey: base)
}
var activeAgentName: String {
let agentId = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let resolvedId = agentId.isEmpty ? defaultId : agentId
if resolvedId.isEmpty { return "Main" }
if let match = self.gatewayAgents.first(where: { $0.id == resolvedId }) {
let name = (match.name ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return name.isEmpty ? match.id : name
}
return resolvedId
}
var lastShareEventText: String = "No share events yet."
var openChatRequestID: Int = 0
// Primary "node" connection: used for device capabilities and node.invoke requests.
private let nodeGateway = GatewayNodeSession()
@@ -104,16 +94,22 @@ final class NodeAppModel {
private let calendarService: any CalendarServicing
private let remindersService: any RemindersServicing
private let motionService: any MotionServicing
private let watchMessagingService: any WatchMessagingServicing
var lastAutoA2uiURL: String?
private var pttVoiceWakeSuspended = false
private var talkVoiceWakeSuspended = false
private var backgroundVoiceWakeSuspended = false
private var backgroundTalkSuspended = false
private var backgroundTalkKeptActive = false
private var backgroundedAt: Date?
private var reconnectAfterBackgroundArmed = false
private var gatewayConnected = false
private var operatorConnected = false
private var shareDeliveryChannel: String?
private var shareDeliveryTo: String?
private var apnsDeviceTokenHex: String?
private var apnsLastRegisteredTokenHex: String?
var gatewaySession: GatewayNodeSession { self.nodeGateway }
var operatorSession: GatewayNodeSession { self.operatorGateway }
private(set) var activeGatewayConnectConfig: GatewayConnectConfig?
@@ -135,6 +131,7 @@ final class NodeAppModel {
calendarService: any CalendarServicing = CalendarService(),
remindersService: any RemindersServicing = RemindersService(),
motionService: any MotionServicing = MotionService(),
watchMessagingService: any WatchMessagingServicing = WatchMessagingService(),
talkMode: TalkModeManager = TalkModeManager())
{
self.screen = screen
@@ -148,7 +145,9 @@ final class NodeAppModel {
self.calendarService = calendarService
self.remindersService = remindersService
self.motionService = motionService
self.watchMessagingService = watchMessagingService
self.talkMode = talkMode
self.apnsDeviceTokenHex = UserDefaults.standard.string(forKey: Self.apnsDeviceTokenUserDefaultsKey)
GatewayDiagnostics.bootstrap()
self.voiceWake.configure { [weak self] cmd in
@@ -164,6 +163,7 @@ final class NodeAppModel {
let enabled = UserDefaults.standard.bool(forKey: "voiceWake.enabled")
self.voiceWake.setEnabled(enabled)
self.talkMode.attachGateway(self.operatorGateway)
self.refreshLastShareEventFromRelay()
let talkEnabled = UserDefaults.standard.bool(forKey: "talk.enabled")
// Route through the coordinator so VoiceWake and Talk don't fight over the microphone.
self.setTalkEnabled(talkEnabled)
@@ -264,15 +264,18 @@ final class NodeAppModel {
func setScenePhase(_ phase: ScenePhase) {
let keepTalkActive = UserDefaults.standard.bool(forKey: "talk.background.enabled")
switch phase {
case .background:
self.isBackgrounded = true
self.stopGatewayHealthMonitor()
self.backgroundedAt = Date()
self.reconnectAfterBackgroundArmed = true
// Be conservative: release the mic when the app backgrounds.
// Release voice wake mic in background.
self.backgroundVoiceWakeSuspended = self.voiceWake.suspendForExternalAudioCapture()
self.backgroundTalkSuspended = self.talkMode.suspendForBackground()
let shouldKeepTalkActive = keepTalkActive && self.talkMode.isEnabled
self.backgroundTalkKeptActive = shouldKeepTalkActive
self.backgroundTalkSuspended = self.talkMode.suspendForBackground(keepActive: shouldKeepTalkActive)
case .active, .inactive:
self.isBackgrounded = false
if self.operatorConnected {
@@ -284,8 +287,12 @@ final class NodeAppModel {
Task { [weak self] in
guard let self else { return }
let suspended = await MainActor.run { self.backgroundTalkSuspended }
await MainActor.run { self.backgroundTalkSuspended = false }
await self.talkMode.resumeAfterBackground(wasSuspended: suspended)
let keptActive = await MainActor.run { self.backgroundTalkKeptActive }
await MainActor.run {
self.backgroundTalkSuspended = false
self.backgroundTalkKeptActive = false
}
await self.talkMode.resumeAfterBackground(wasSuspended: suspended, wasKeptActive: keptActive)
}
}
if phase == .active, self.reconnectAfterBackgroundArmed {
@@ -340,6 +347,7 @@ final class NodeAppModel {
}
func setTalkEnabled(_ enabled: Bool) {
UserDefaults.standard.set(enabled, forKey: "talk.enabled")
if enabled {
// Voice wake holds the microphone continuously; talk mode needs exclusive access for STT.
// When talk is enabled from the UI, prioritize talk and pause voice wake.
@@ -351,6 +359,11 @@ final class NodeAppModel {
self.talkVoiceWakeSuspended = false
}
self.talkMode.setEnabled(enabled)
Task { [weak self] in
await self?.pushTalkModeToGateway(
enabled: enabled,
phase: enabled ? "enabled" : "disabled")
}
}
func requestLocationPermissions(mode: OpenClawLocationMode) async -> Bool {
@@ -380,6 +393,14 @@ final class NodeAppModel {
}
private static let defaultSeamColor = Color(red: 79 / 255.0, green: 122 / 255.0, blue: 154 / 255.0)
private static let apnsDeviceTokenUserDefaultsKey = "push.apns.deviceTokenHex"
private static var apnsEnvironment: String {
#if DEBUG
"sandbox"
#else
"production"
#endif
}
private static func color(fromHex raw: String?) -> Color? {
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
@@ -447,6 +468,16 @@ final class NodeAppModel {
GatewaySettingsStore.saveGatewaySelectedAgentId(stableID: stableID, agentId: self.selectedAgentId)
}
self.talkMode.updateMainSessionKey(self.mainSessionKey)
if let relay = ShareGatewayRelaySettings.loadConfig() {
ShareGatewayRelaySettings.saveConfig(
ShareGatewayRelayConfig(
gatewayURLString: relay.gatewayURLString,
token: relay.token,
password: relay.password,
sessionKey: self.mainSessionKey,
deliveryChannel: self.shareDeliveryChannel,
deliveryTo: self.shareDeliveryTo))
}
}
func setGlobalWakeWords(_ words: [String]) async {
@@ -479,16 +510,49 @@ final class NodeAppModel {
let stream = await self.operatorGateway.subscribeServerEvents(bufferingNewest: 200)
for await evt in stream {
if Task.isCancelled { return }
guard evt.event == "voicewake.changed" else { continue }
guard let payload = evt.payload else { continue }
struct Payload: Decodable { var triggers: [String] }
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue }
let triggers = VoiceWakePreferences.sanitizeTriggerWords(decoded.triggers)
VoiceWakePreferences.saveTriggerWords(triggers)
switch evt.event {
case "voicewake.changed":
struct Payload: Decodable { var triggers: [String] }
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue }
let triggers = VoiceWakePreferences.sanitizeTriggerWords(decoded.triggers)
VoiceWakePreferences.saveTriggerWords(triggers)
case "talk.mode":
struct Payload: Decodable {
var enabled: Bool
var phase: String?
}
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue }
self.applyTalkModeSync(enabled: decoded.enabled, phase: decoded.phase)
default:
continue
}
}
}
}
private func applyTalkModeSync(enabled: Bool, phase: String?) {
_ = phase
guard self.talkMode.isEnabled != enabled else { return }
self.setTalkEnabled(enabled)
}
private func pushTalkModeToGateway(enabled: Bool, phase: String?) async {
guard await self.isOperatorConnected() else { return }
struct TalkModePayload: Encodable {
var enabled: Bool
var phase: String?
}
let payload = TalkModePayload(enabled: enabled, phase: phase)
guard let data = try? JSONEncoder().encode(payload),
let json = String(data: data, encoding: .utf8)
else { return }
_ = try? await self.operatorGateway.request(
method: "talk.mode",
paramsJSON: json,
timeoutSeconds: 8)
}
private func startGatewayHealthMonitor() {
self.gatewayHealthMonitorDisabled = false
self.gatewayHealthMonitor.start(
@@ -515,8 +579,11 @@ final class NodeAppModel {
onFailure: { [weak self] _ in
guard let self else { return }
await self.operatorGateway.disconnect()
await self.nodeGateway.disconnect()
await MainActor.run {
self.operatorConnected = false
self.gatewayConnected = false
self.gatewayStatusText = "Reconnecting…"
self.talkMode.updateGatewayConnected(false)
}
})
@@ -577,28 +644,41 @@ final class NodeAppModel {
switch route {
case let .agent(link):
await self.handleAgentDeepLink(link, originalURL: url)
case .gateway:
break
}
}
private func handleAgentDeepLink(_ link: AgentDeepLink, originalURL: URL) async {
let message = link.message.trimmingCharacters(in: .whitespacesAndNewlines)
guard !message.isEmpty else { return }
self.deepLinkLogger.info(
"agent deep link received messageChars=\(message.count) url=\(originalURL.absoluteString, privacy: .public)"
)
if message.count > 20000 {
self.screen.errorText = "Deep link too large (message exceeds 20,000 characters)."
self.recordShareEvent("Rejected: message too large (\(message.count) chars).")
return
}
guard await self.isGatewayConnected() else {
self.screen.errorText = "Gateway not connected (cannot forward deep link)."
self.recordShareEvent("Failed: gateway not connected.")
self.deepLinkLogger.error("agent deep link rejected: gateway not connected")
return
}
do {
try await self.sendAgentRequest(link: link)
self.screen.errorText = nil
self.recordShareEvent("Sent to gateway (\(message.count) chars).")
self.deepLinkLogger.info("agent deep link forwarded to gateway")
self.openChatRequestID &+= 1
} catch {
self.screen.errorText = "Agent request failed: \(error.localizedDescription)"
self.recordShareEvent("Failed: \(error.localizedDescription)")
self.deepLinkLogger.error("agent deep link send failed: \(error.localizedDescription, privacy: .public)")
}
}
@@ -1345,6 +1425,14 @@ private extension NodeAppModel {
return try await self.handleDeviceInvoke(req)
}
register([
OpenClawWatchCommand.status.rawValue,
OpenClawWatchCommand.notify.rawValue,
]) { [weak self] req in
guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable }
return try await self.handleWatchInvoke(req)
}
register([OpenClawPhotosCommand.latest.rawValue]) { [weak self] req in
guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable }
return try await self.handlePhotosInvoke(req)
@@ -1395,14 +1483,67 @@ private extension NodeAppModel {
return NodeCapabilityRouter(handlers: handlers)
}
func handleWatchInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
switch req.command {
case OpenClawWatchCommand.status.rawValue:
let status = await self.watchMessagingService.status()
let payload = OpenClawWatchStatusPayload(
supported: status.supported,
paired: status.paired,
appInstalled: status.appInstalled,
reachable: status.reachable,
activationState: status.activationState)
let json = try Self.encodePayload(payload)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
case OpenClawWatchCommand.notify.rawValue:
let params = try Self.decodeParams(OpenClawWatchNotifyParams.self, from: req.paramsJSON)
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
let body = params.body.trimmingCharacters(in: .whitespacesAndNewlines)
if title.isEmpty && body.isEmpty {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: OpenClawNodeError(
code: .invalidRequest,
message: "INVALID_REQUEST: empty watch notification"))
}
do {
let result = try await self.watchMessagingService.sendNotification(
id: req.id,
title: title,
body: body,
priority: params.priority)
let payload = OpenClawWatchNotifyPayload(
deliveredImmediately: result.deliveredImmediately,
queuedForDelivery: result.queuedForDelivery,
transport: result.transport)
let json = try Self.encodePayload(payload)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
} catch {
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: OpenClawNodeError(
code: .unavailable,
message: error.localizedDescription))
}
default:
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
}
}
func locationMode() -> OpenClawLocationMode {
let raw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off"
return OpenClawLocationMode(rawValue: raw) ?? .off
}
func isLocationPreciseEnabled() -> Bool {
if UserDefaults.standard.object(forKey: "location.preciseEnabled") == nil { return true }
return UserDefaults.standard.bool(forKey: "location.preciseEnabled")
// iOS settings now expose a single location mode control.
// Default location tool precision stays high unless a command explicitly requests balanced.
true
}
static func decodeParams<T: Decodable>(_ type: T.Type, from json: String?) throws -> T {
@@ -1454,6 +1595,26 @@ private extension NodeAppModel {
}
extension NodeAppModel {
var mainSessionKey: String {
let base = SessionKey.normalizeMainKey(self.mainSessionBaseKey)
let agentId = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if agentId.isEmpty || (!defaultId.isEmpty && agentId == defaultId) { return base }
return SessionKey.makeAgentSessionKey(agentId: agentId, baseKey: base)
}
var activeAgentName: String {
let agentId = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let resolvedId = agentId.isEmpty ? defaultId : agentId
if resolvedId.isEmpty { return "Main" }
if let match = self.gatewayAgents.first(where: { $0.id == resolvedId }) {
let name = (match.name ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return name.isEmpty ? match.id : name
}
return resolvedId
}
func connectToGateway(
url: URL,
gatewayStableID: String,
@@ -1506,6 +1667,8 @@ extension NodeAppModel {
func disconnectGateway() {
self.gatewayAutoReconnectEnabled = false
self.gatewayPairingPaused = false
self.gatewayPairingRequestId = nil
self.nodeGatewayTask?.cancel()
self.nodeGatewayTask = nil
self.operatorGatewayTask?.cancel()
@@ -1528,6 +1691,7 @@ extension NodeAppModel {
self.seamColorHex = nil
self.mainSessionBaseKey = "main"
self.talkMode.updateMainSessionKey(self.mainSessionKey)
ShareGatewayRelaySettings.clearConfig()
self.showLocalCanvasOnDisconnect()
}
}
@@ -1535,6 +1699,8 @@ extension NodeAppModel {
private extension NodeAppModel {
func prepareForGatewayConnect(url: URL, stableID: String) {
self.gatewayAutoReconnectEnabled = true
self.gatewayPairingPaused = false
self.gatewayPairingRequestId = nil
self.nodeGatewayTask?.cancel()
self.operatorGatewayTask?.cancel()
self.gatewayHealthMonitor.stop()
@@ -1548,6 +1714,7 @@ private extension NodeAppModel {
self.gatewayDefaultAgentId = nil
self.gatewayAgents = []
self.selectedAgentId = GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: stableID)
self.apnsLastRegisteredTokenHex = nil
}
func startOperatorGatewayLoop(
@@ -1564,6 +1731,14 @@ private extension NodeAppModel {
guard let self else { return }
var attempt = 0
while !Task.isCancelled {
if self.gatewayPairingPaused {
try? await Task.sleep(nanoseconds: 1_000_000_000)
continue
}
if !self.gatewayAutoReconnectEnabled {
try? await Task.sleep(nanoseconds: 1_000_000_000)
continue
}
if await self.isOperatorConnected() {
try? await Task.sleep(nanoseconds: 1_000_000_000)
continue
@@ -1592,6 +1767,7 @@ private extension NodeAppModel {
"operator gateway connected host=\(url.host ?? "?") scheme=\(url.scheme ?? "?")")
await self.refreshBrandingFromGateway()
await self.refreshAgentsFromGateway()
await self.refreshShareRouteFromGateway()
await self.startVoiceWakeSync()
await MainActor.run { self.startGatewayHealthMonitor() }
},
@@ -1639,8 +1815,17 @@ private extension NodeAppModel {
var attempt = 0
var currentOptions = nodeOptions
var didFallbackClientId = false
var pausedForPairingApproval = false
while !Task.isCancelled {
if self.gatewayPairingPaused {
try? await Task.sleep(nanoseconds: 1_000_000_000)
continue
}
if !self.gatewayAutoReconnectEnabled {
try? await Task.sleep(nanoseconds: 1_000_000_000)
continue
}
if await self.isGatewayConnected() {
try? await Task.sleep(nanoseconds: 1_000_000_000)
continue
@@ -1669,12 +1854,28 @@ private extension NodeAppModel {
self.screen.errorText = nil
UserDefaults.standard.set(true, forKey: "gateway.autoconnect")
}
GatewayDiagnostics.log(
"gateway connected host=\(url.host ?? "?") scheme=\(url.scheme ?? "?")")
let relayData = await MainActor.run {
(
sessionKey: self.mainSessionKey,
deliveryChannel: self.shareDeliveryChannel,
deliveryTo: self.shareDeliveryTo
)
}
ShareGatewayRelaySettings.saveConfig(
ShareGatewayRelayConfig(
gatewayURLString: url.absoluteString,
token: token,
password: password,
sessionKey: relayData.sessionKey,
deliveryChannel: relayData.deliveryChannel,
deliveryTo: relayData.deliveryTo))
GatewayDiagnostics.log("gateway connected host=\(url.host ?? "?") scheme=\(url.scheme ?? "?")")
if let addr = await self.nodeGateway.currentRemoteAddress() {
await MainActor.run { self.gatewayRemoteAddress = addr }
}
await self.showA2UIOnConnectIfNeeded()
await self.onNodeGatewayConnected()
await MainActor.run { SignificantLocationMonitor.startIfNeeded(locationService: self.locationService, locationMode: self.locationMode(), gateway: self.nodeGateway) }
},
onDisconnected: { [weak self] reason in
guard let self else { return }
@@ -1726,11 +1927,60 @@ private extension NodeAppModel {
self.showLocalCanvasOnDisconnect()
}
GatewayDiagnostics.log("gateway connect error: \(error.localizedDescription)")
// If auth is missing/rejected, pause reconnect churn until the user intervenes.
// Reconnect loops only spam the same failing handshake and make onboarding noisy.
let lower = error.localizedDescription.lowercased()
if lower.contains("unauthorized") || lower.contains("gateway token missing") {
await MainActor.run {
self.gatewayAutoReconnectEnabled = false
}
}
// If pairing is required, stop reconnect churn. The user must approve the request
// on the gateway before another connect attempt will succeed, and retry loops can
// generate multiple pending requests.
if lower.contains("not_paired") || lower.contains("pairing required") {
let requestId: String? = {
// GatewayResponseError for connect decorates the message with `(requestId: ...)`.
// Keep this resilient since other layers may wrap the text.
let text = error.localizedDescription
guard let start = text.range(of: "(requestId: ")?.upperBound else { return nil }
guard let end = text[start...].firstIndex(of: ")") else { return nil }
let raw = String(text[start..<end]).trimmingCharacters(in: .whitespacesAndNewlines)
return raw.isEmpty ? nil : raw
}()
await MainActor.run {
self.gatewayAutoReconnectEnabled = false
self.gatewayPairingPaused = true
self.gatewayPairingRequestId = requestId
if let requestId, !requestId.isEmpty {
self.gatewayStatusText =
"Pairing required (requestId: \(requestId)). Approve on gateway and return to OpenClaw."
} else {
self.gatewayStatusText = "Pairing required. Approve on gateway and return to OpenClaw."
}
}
// Hard stop the underlying WebSocket watchdog reconnects so the UI stays stable and
// we don't generate multiple pending requests while waiting for approval.
pausedForPairingApproval = true
self.operatorGatewayTask?.cancel()
self.operatorGatewayTask = nil
await self.operatorGateway.disconnect()
await self.nodeGateway.disconnect()
break
}
let sleepSeconds = min(8.0, 0.5 * pow(1.7, Double(attempt)))
try? await Task.sleep(nanoseconds: UInt64(sleepSeconds * 1_000_000_000))
}
}
if pausedForPairingApproval {
// Leave the status text + request id intact so onboarding can guide the user.
return
}
await MainActor.run {
self.gatewayStatusText = "Offline"
self.gatewayServerName = nil
@@ -1757,6 +2007,8 @@ private extension NodeAppModel {
clientId: clientId,
clientMode: "ui",
clientDisplayName: displayName,
// Operator traffic should authenticate via shared gateway auth only.
// Including device identity here can trigger duplicate pairing flows.
includeDeviceIdentity: false)
}
@@ -1775,6 +2027,206 @@ private extension NodeAppModel {
}
}
extension NodeAppModel {
private func refreshShareRouteFromGateway() async {
struct Params: Codable {
var includeGlobal: Bool
var includeUnknown: Bool
var limit: Int
}
struct SessionRow: Decodable {
var key: String
var updatedAt: Double?
var lastChannel: String?
var lastTo: String?
}
struct SessionsListResult: Decodable {
var sessions: [SessionRow]
}
let normalize: (String?) -> String? = { raw in
let value = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
do {
let data = try JSONEncoder().encode(
Params(includeGlobal: true, includeUnknown: false, limit: 80))
guard let json = String(data: data, encoding: .utf8) else { return }
let response = try await self.operatorGateway.request(
method: "sessions.list",
paramsJSON: json,
timeoutSeconds: 10)
let decoded = try JSONDecoder().decode(SessionsListResult.self, from: response)
let currentKey = self.mainSessionKey
let sorted = decoded.sessions.sorted { ($0.updatedAt ?? 0) > ($1.updatedAt ?? 0) }
let exactMatch = sorted.first { row in
row.key == currentKey && normalize(row.lastChannel) != nil && normalize(row.lastTo) != nil
}
let selected = exactMatch
let channel = normalize(selected?.lastChannel)
let to = normalize(selected?.lastTo)
await MainActor.run {
self.shareDeliveryChannel = channel
self.shareDeliveryTo = to
if let relay = ShareGatewayRelaySettings.loadConfig() {
ShareGatewayRelaySettings.saveConfig(
ShareGatewayRelayConfig(
gatewayURLString: relay.gatewayURLString,
token: relay.token,
password: relay.password,
sessionKey: self.mainSessionKey,
deliveryChannel: channel,
deliveryTo: to))
}
}
} catch {
// Best-effort only.
}
}
func runSharePipelineSelfTest() async {
self.recordShareEvent("Share self-test running…")
let payload = SharedContentPayload(
title: "OpenClaw Share Self-Test",
url: URL(string: "https://openclaw.ai/share-self-test"),
text: "Validate iOS share->deep-link->gateway forwarding.")
guard let deepLink = ShareToAgentDeepLink.buildURL(
from: payload,
instruction: "Reply with: SHARE SELF-TEST OK")
else {
self.recordShareEvent("Self-test failed: could not build deep link.")
return
}
await self.handleDeepLink(url: deepLink)
}
func refreshLastShareEventFromRelay() {
if let event = ShareGatewayRelaySettings.loadLastEvent() {
self.lastShareEventText = event
}
}
func recordShareEvent(_ text: String) {
ShareGatewayRelaySettings.saveLastEvent(text)
self.refreshLastShareEventFromRelay()
}
func reloadTalkConfig() {
Task { [weak self] in
await self?.talkMode.reloadConfig()
}
}
/// Back-compat hook retained for older gateway-connect flows.
func onNodeGatewayConnected() async {
await self.registerAPNsTokenIfNeeded()
}
func handleSilentPushWake(_ userInfo: [AnyHashable: Any]) async -> Bool {
guard Self.isSilentPushPayload(userInfo) else {
self.pushWakeLogger.info("Ignored APNs payload: not silent push")
return false
}
self.pushWakeLogger.info("Silent push received; attempting reconnect if needed")
return await self.reconnectGatewaySessionsForSilentPushIfNeeded()
}
func updateAPNsDeviceToken(_ tokenData: Data) {
let tokenHex = tokenData.map { String(format: "%02x", $0) }.joined()
let trimmed = tokenHex.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
self.apnsDeviceTokenHex = trimmed
UserDefaults.standard.set(trimmed, forKey: Self.apnsDeviceTokenUserDefaultsKey)
Task { [weak self] in
await self?.registerAPNsTokenIfNeeded()
}
}
private func registerAPNsTokenIfNeeded() async {
guard self.gatewayConnected else { return }
guard let token = self.apnsDeviceTokenHex?.trimmingCharacters(in: .whitespacesAndNewlines),
!token.isEmpty
else {
return
}
if token == self.apnsLastRegisteredTokenHex {
return
}
guard let topic = Bundle.main.bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines),
!topic.isEmpty
else {
return
}
struct PushRegistrationPayload: Codable {
var token: String
var topic: String
var environment: String
}
let payload = PushRegistrationPayload(
token: token,
topic: topic,
environment: Self.apnsEnvironment)
do {
let json = try Self.encodePayload(payload)
await self.nodeGateway.sendEvent(event: "push.apns.register", payloadJSON: json)
self.apnsLastRegisteredTokenHex = token
} catch {
// Best-effort only.
}
}
private static func isSilentPushPayload(_ userInfo: [AnyHashable: Any]) -> Bool {
guard let apsAny = userInfo["aps"] else { return false }
if let aps = apsAny as? [AnyHashable: Any] {
return Self.hasContentAvailable(aps["content-available"])
}
if let aps = apsAny as? [String: Any] {
return Self.hasContentAvailable(aps["content-available"])
}
return false
}
private static func hasContentAvailable(_ value: Any?) -> Bool {
if let number = value as? NSNumber {
return number.intValue == 1
}
if let text = value as? String {
return text.trimmingCharacters(in: .whitespacesAndNewlines) == "1"
}
return false
}
private func reconnectGatewaySessionsForSilentPushIfNeeded() async -> Bool {
guard self.isBackgrounded else {
self.pushWakeLogger.info("Wake no-op: app not backgrounded")
return false
}
guard self.gatewayAutoReconnectEnabled else {
self.pushWakeLogger.info("Wake no-op: auto reconnect disabled")
return false
}
guard self.activeGatewayConnectConfig != nil else {
self.pushWakeLogger.info("Wake no-op: no active gateway config")
return false
}
await self.operatorGateway.disconnect()
await self.nodeGateway.disconnect()
self.operatorConnected = false
self.gatewayConnected = false
self.gatewayStatusText = "Reconnecting…"
self.talkMode.updateGatewayConnected(false)
self.pushWakeLogger.info("Wake reconnect trigger applied")
return true
}
}
#if DEBUG
extension NodeAppModel {
func _test_handleInvoke(_ req: BridgeInvokeRequest) async -> BridgeInvokeResponse {
@@ -1808,5 +2260,9 @@ extension NodeAppModel {
func _test_showLocalCanvasOnDisconnect() {
self.showLocalCanvasOnDisconnect()
}
func _test_applyTalkModeSync(enabled: Bool, phase: String? = nil) {
self.applyTalkModeSync(enabled: enabled, phase: phase)
}
}
#endif

View File

@@ -257,15 +257,6 @@ private struct ManualEntryStep: View {
self.manualPassword = ""
}
private struct SetupPayload: Codable {
var url: String?
var host: String?
var port: Int?
var tls: Bool?
var token: String?
var password: String?
}
private func applySetupCode() {
let raw = self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines)
guard !raw.isEmpty else {
@@ -273,7 +264,7 @@ private struct ManualEntryStep: View {
return
}
guard let payload = self.decodeSetupPayload(raw: raw) else {
guard let payload = GatewaySetupCode.decode(raw: raw) else {
self.setupStatusText = "Setup code not recognized."
return
}
@@ -323,34 +314,7 @@ private struct ManualEntryStep: View {
}
}
private func decodeSetupPayload(raw: String) -> SetupPayload? {
if let payload = decodeSetupPayloadFromJSON(raw) {
return payload
}
if let decoded = decodeBase64Payload(raw),
let payload = decodeSetupPayloadFromJSON(decoded)
{
return payload
}
return nil
}
private func decodeSetupPayloadFromJSON(_ json: String) -> SetupPayload? {
guard let data = json.data(using: .utf8) else { return nil }
return try? JSONDecoder().decode(SetupPayload.self, from: data)
}
private func decodeBase64Payload(_ raw: String) -> String? {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let normalized = trimmed
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let padding = normalized.count % 4
let padded = padding == 0 ? normalized : normalized + String(repeating: "=", count: 4 - padding)
guard let data = Data(base64Encoded: padded) else { return nil }
return String(data: data, encoding: .utf8)
}
// (GatewaySetupCode) decode raw setup codes.
}
private struct ConnectionStatusBox: View {

View File

@@ -0,0 +1,52 @@
import Foundation
enum OnboardingConnectionMode: String, CaseIterable {
case homeNetwork = "home_network"
case remoteDomain = "remote_domain"
case developerLocal = "developer_local"
var title: String {
switch self {
case .homeNetwork:
"Home Network"
case .remoteDomain:
"Remote Domain"
case .developerLocal:
"Same Machine (Dev)"
}
}
}
enum OnboardingStateStore {
private static let completedDefaultsKey = "onboarding.completed"
private static let lastModeDefaultsKey = "onboarding.last_mode"
private static let lastSuccessTimeDefaultsKey = "onboarding.last_success_time"
@MainActor
static func shouldPresentOnLaunch(appModel: NodeAppModel, defaults: UserDefaults = .standard) -> Bool {
if defaults.bool(forKey: Self.completedDefaultsKey) { return false }
// If we have a last-known connection config, don't force onboarding on launch. Auto-connect
// should handle reconnecting, and users can always open onboarding manually if needed.
if GatewaySettingsStore.loadLastGatewayConnection() != nil { return false }
return appModel.gatewayServerName == nil
}
static func markCompleted(mode: OnboardingConnectionMode? = nil, defaults: UserDefaults = .standard) {
defaults.set(true, forKey: Self.completedDefaultsKey)
if let mode {
defaults.set(mode.rawValue, forKey: Self.lastModeDefaultsKey)
}
defaults.set(Int(Date().timeIntervalSince1970), forKey: Self.lastSuccessTimeDefaultsKey)
}
static func markIncomplete(defaults: UserDefaults = .standard) {
defaults.set(false, forKey: Self.completedDefaultsKey)
}
static func lastMode(defaults: UserDefaults = .standard) -> OnboardingConnectionMode? {
let raw = defaults.string(forKey: Self.lastModeDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !raw.isEmpty else { return nil }
return OnboardingConnectionMode(rawValue: raw)
}
}

View File

@@ -0,0 +1,890 @@
import CoreImage
import Combine
import OpenClawKit
import PhotosUI
import SwiftUI
import UIKit
private enum OnboardingStep: Int, CaseIterable {
case welcome
case mode
case connect
case auth
case success
var previous: Self? {
Self(rawValue: self.rawValue - 1)
}
var next: Self? {
Self(rawValue: self.rawValue + 1)
}
/// Progress label for the manual setup flow (mode connect auth success).
var manualProgressTitle: String {
let manualSteps: [OnboardingStep] = [.mode, .connect, .auth, .success]
guard let idx = manualSteps.firstIndex(of: self) else { return "" }
return "Step \(idx + 1) of \(manualSteps.count)"
}
var title: String {
switch self {
case .welcome: "Welcome"
case .mode: "Connection Mode"
case .connect: "Connect"
case .auth: "Authentication"
case .success: "Connected"
}
}
var canGoBack: Bool {
self != .welcome && self != .success
}
}
struct OnboardingWizardView: View {
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
@Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController
@Environment(\.scenePhase) private var scenePhase
@AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString
@AppStorage("gateway.discovery.domain") private var discoveryDomain: String = ""
@AppStorage("onboarding.developerMode") private var developerModeEnabled: Bool = false
@State private var step: OnboardingStep = .welcome
@State private var selectedMode: OnboardingConnectionMode?
@State private var manualHost: String = ""
@State private var manualPort: Int = 18789
@State private var manualPortText: String = "18789"
@State private var manualTLS: Bool = true
@State private var gatewayToken: String = ""
@State private var gatewayPassword: String = ""
@State private var connectMessage: String?
@State private var statusLine: String = "Scan the QR code from your gateway to connect."
@State private var connectingGatewayID: String?
@State private var issue: GatewayConnectionIssue = .none
@State private var didMarkCompleted = false
@State private var didAutoPresentQR = false
@State private var pairingRequestId: String?
@State private var discoveryRestartTask: Task<Void, Never>?
@State private var showQRScanner: Bool = false
@State private var scannerError: String?
@State private var selectedPhoto: PhotosPickerItem?
@State private var lastPairingAutoResumeAttemptAt: Date?
private static let pairingAutoResumeTicker = Timer.publish(every: 2.0, on: .main, in: .common).autoconnect()
let allowSkip: Bool
let onClose: () -> Void
private var isFullScreenStep: Bool {
self.step == .welcome || self.step == .success
}
var body: some View {
NavigationStack {
Group {
switch self.step {
case .welcome:
self.welcomeStep
case .success:
self.successStep
default:
Form {
switch self.step {
case .mode:
self.modeStep
case .connect:
self.connectStep
case .auth:
self.authStep
default:
EmptyView()
}
}
.scrollDismissesKeyboard(.interactively)
}
}
.navigationTitle(self.isFullScreenStep ? "" : self.step.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if !self.isFullScreenStep {
ToolbarItem(placement: .principal) {
VStack(spacing: 2) {
Text(self.step.title)
.font(.headline)
Text(self.step.manualProgressTitle)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
ToolbarItem(placement: .topBarLeading) {
if self.step.canGoBack {
Button {
self.navigateBack()
} label: {
Label("Back", systemImage: "chevron.left")
}
} else if self.allowSkip {
Button("Close") {
self.onClose()
}
}
}
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
}
}
.gatewayTrustPromptAlert()
.alert("QR Scanner Unavailable", isPresented: Binding(
get: { self.scannerError != nil },
set: { if !$0 { self.scannerError = nil } }
)) {
Button("OK", role: .cancel) {}
} message: {
Text(self.scannerError ?? "")
}
.sheet(isPresented: self.$showQRScanner) {
NavigationStack {
QRScannerView(
onGatewayLink: { link in
self.handleScannedLink(link)
},
onError: { error in
self.showQRScanner = false
self.statusLine = "Scanner error: \(error)"
self.scannerError = error
},
onDismiss: {
self.showQRScanner = false
})
.ignoresSafeArea()
.navigationTitle("Scan QR Code")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Cancel") { self.showQRScanner = false }
}
ToolbarItem(placement: .topBarTrailing) {
PhotosPicker(selection: self.$selectedPhoto, matching: .images) {
Label("Photos", systemImage: "photo")
}
}
}
}
.onChange(of: self.selectedPhoto) { _, newValue in
guard let item = newValue else { return }
self.selectedPhoto = nil
Task {
guard let data = try? await item.loadTransferable(type: Data.self) else {
self.showQRScanner = false
self.scannerError = "Could not load the selected image."
return
}
if let message = self.detectQRCode(from: data) {
if let link = GatewayConnectDeepLink.fromSetupCode(message) {
self.handleScannedLink(link)
return
}
if let url = URL(string: message),
let route = DeepLinkParser.parse(url),
case let .gateway(link) = route
{
self.handleScannedLink(link)
return
}
}
self.showQRScanner = false
self.scannerError = "No valid QR code found in the selected image."
}
}
}
.onAppear {
self.initializeState()
}
.onDisappear {
self.discoveryRestartTask?.cancel()
self.discoveryRestartTask = nil
}
.onChange(of: self.discoveryDomain) { _, _ in
self.scheduleDiscoveryRestart()
}
.onChange(of: self.manualPortText) { _, newValue in
let digits = newValue.filter(\.isNumber)
if digits != newValue {
self.manualPortText = digits
return
}
guard let parsed = Int(digits), parsed > 0 else {
self.manualPort = 0
return
}
self.manualPort = min(parsed, 65535)
}
.onChange(of: self.manualPort) { _, newValue in
let normalized = newValue > 0 ? String(newValue) : ""
if self.manualPortText != normalized {
self.manualPortText = normalized
}
}
.onChange(of: self.gatewayToken) { _, newValue in
self.saveGatewayCredentials(token: newValue, password: self.gatewayPassword)
}
.onChange(of: self.gatewayPassword) { _, newValue in
self.saveGatewayCredentials(token: self.gatewayToken, password: newValue)
}
.onChange(of: self.appModel.gatewayStatusText) { _, newValue in
let next = GatewayConnectionIssue.detect(from: newValue)
// Avoid "flip-flopping" the UI by clearing actionable issues when the underlying connection
// transitions through intermediate statuses (e.g. Offline/Connecting while reconnect churns).
if self.issue.needsPairing, next.needsPairing {
// Keep the requestId sticky even if the status line omits it after we pause.
let mergedRequestId = next.requestId ?? self.issue.requestId ?? self.pairingRequestId
self.issue = .pairingRequired(requestId: mergedRequestId)
} else if self.issue.needsPairing, !next.needsPairing {
// Ignore non-pairing statuses until the user explicitly retries/scans again, or we connect.
} else if self.issue.needsAuthToken, !next.needsAuthToken, !next.needsPairing {
// Same idea for auth: once we learn credentials are missing/rejected, keep that sticky until
// the user retries/scans again or we successfully connect.
} else {
self.issue = next
}
if let requestId = next.requestId, !requestId.isEmpty {
self.pairingRequestId = requestId
}
// If the gateway tells us auth is missing/rejected, stop reconnect churn until the user intervenes.
if next.needsAuthToken {
self.appModel.gatewayAutoReconnectEnabled = false
}
if self.issue.needsAuthToken || self.issue.needsPairing {
self.step = .auth
}
if !newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
self.connectMessage = newValue
self.statusLine = newValue
}
}
.onChange(of: self.appModel.gatewayServerName) { _, newValue in
guard newValue != nil else { return }
self.showQRScanner = false
self.statusLine = "Connected."
if !self.didMarkCompleted, let selectedMode {
OnboardingStateStore.markCompleted(mode: selectedMode)
self.didMarkCompleted = true
}
self.onClose()
}
.onChange(of: self.scenePhase) { _, newValue in
guard newValue == .active else { return }
self.attemptAutomaticPairingResumeIfNeeded()
}
.onReceive(Self.pairingAutoResumeTicker) { _ in
self.attemptAutomaticPairingResumeIfNeeded()
}
}
@ViewBuilder
private var welcomeStep: some View {
VStack(spacing: 0) {
Spacer()
Image(systemName: "qrcode.viewfinder")
.font(.system(size: 64))
.foregroundStyle(.tint)
.padding(.bottom, 20)
Text("Welcome")
.font(.largeTitle.weight(.bold))
.padding(.bottom, 8)
Text("Connect to your OpenClaw gateway")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
Spacer()
VStack(spacing: 12) {
Button {
self.statusLine = "Opening QR scanner…"
self.showQRScanner = true
} label: {
Label("Scan QR Code", systemImage: "qrcode")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
Button {
self.step = .mode
} label: {
Text("Set Up Manually")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.controlSize(.large)
}
.padding(.bottom, 12)
Text(self.statusLine)
.font(.footnote)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
.padding(.horizontal, 24)
.padding(.bottom, 48)
}
}
@ViewBuilder
private var modeStep: some View {
Section("Connection Mode") {
OnboardingModeRow(
title: OnboardingConnectionMode.homeNetwork.title,
subtitle: "LAN or Tailscale host",
selected: self.selectedMode == .homeNetwork)
{
self.selectMode(.homeNetwork)
}
OnboardingModeRow(
title: OnboardingConnectionMode.remoteDomain.title,
subtitle: "VPS with domain",
selected: self.selectedMode == .remoteDomain)
{
self.selectMode(.remoteDomain)
}
Toggle(
"Developer mode",
isOn: Binding(
get: { self.developerModeEnabled },
set: { newValue in
self.developerModeEnabled = newValue
if !newValue, self.selectedMode == .developerLocal {
self.selectedMode = nil
}
}))
if self.developerModeEnabled {
OnboardingModeRow(
title: OnboardingConnectionMode.developerLocal.title,
subtitle: "For local iOS app development",
selected: self.selectedMode == .developerLocal)
{
self.selectMode(.developerLocal)
}
}
}
Section {
Button("Continue") {
self.step = .connect
}
.disabled(self.selectedMode == nil)
}
}
@ViewBuilder
private var connectStep: some View {
if let selectedMode {
Section {
LabeledContent("Mode", value: selectedMode.title)
LabeledContent("Discovery", value: self.gatewayController.discoveryStatusText)
LabeledContent("Status", value: self.appModel.gatewayStatusText)
LabeledContent("Progress", value: self.statusLine)
} header: {
Text("Status")
} footer: {
if let connectMessage {
Text(connectMessage)
}
}
switch selectedMode {
case .homeNetwork:
self.homeNetworkConnectSection
case .remoteDomain:
self.remoteDomainConnectSection
case .developerLocal:
self.developerConnectSection
}
} else {
Section {
Text("Choose a mode first.")
Button("Back to Mode Selection") {
self.step = .mode
}
}
}
}
private var homeNetworkConnectSection: some View {
Group {
Section("Discovered Gateways") {
if self.gatewayController.gateways.isEmpty {
Text("No gateways found yet.")
.foregroundStyle(.secondary)
} else {
ForEach(self.gatewayController.gateways) { gateway in
let hasHost = self.gatewayHasResolvableHost(gateway)
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(gateway.name)
if let host = gateway.lanHost ?? gateway.tailnetDns {
Text(host)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
Spacer()
Button {
Task { await self.connectDiscoveredGateway(gateway) }
} label: {
if self.connectingGatewayID == gateway.id {
ProgressView()
.progressViewStyle(.circular)
} else if !hasHost {
Text("Resolving…")
} else {
Text("Connect")
}
}
.disabled(self.connectingGatewayID != nil || !hasHost)
}
}
}
Button("Restart Discovery") {
self.gatewayController.restartDiscovery()
}
.disabled(self.connectingGatewayID != nil)
}
self.manualConnectionFieldsSection(title: "Manual Fallback")
}
}
private var remoteDomainConnectSection: some View {
self.manualConnectionFieldsSection(title: "Domain Settings")
}
private var developerConnectSection: some View {
Section {
TextField("Host", text: self.$manualHost)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
TextField("Port", text: self.$manualPortText)
.keyboardType(.numberPad)
Toggle("Use TLS", isOn: self.$manualTLS)
Button {
Task { await self.connectManual() }
} label: {
if self.connectingGatewayID == "manual" {
HStack(spacing: 8) {
ProgressView()
.progressViewStyle(.circular)
Text("Connecting…")
}
} else {
Text("Connect")
}
}
.disabled(!self.canConnectManual || self.connectingGatewayID != nil)
} header: {
Text("Developer Local")
} footer: {
Text("Default host is localhost. Use your Mac LAN IP if simulator networking requires it.")
}
}
private var authStep: some View {
Group {
Section("Authentication") {
TextField("Gateway Auth Token", text: self.$gatewayToken)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
SecureField("Gateway Password", text: self.$gatewayPassword)
if self.issue.needsAuthToken {
Text("Gateway rejected credentials. Scan a fresh QR code or update token/password.")
.font(.footnote)
.foregroundStyle(.secondary)
} else {
Text("Auth token looks valid.")
.font(.footnote)
.foregroundStyle(.secondary)
}
}
if self.issue.needsPairing {
Section {
Button {
self.resumeAfterPairingApproval()
} label: {
Label("Resume After Approval", systemImage: "arrow.clockwise")
}
.disabled(self.connectingGatewayID != nil)
} header: {
Text("Pairing Approval")
} footer: {
let requestLine: String = {
if let id = self.issue.requestId, !id.isEmpty {
return "Request ID: \(id)"
}
return "Request ID: check `openclaw devices list`."
}()
Text(
"Approve this device on the gateway.\n"
+ "1) `openclaw devices approve` (or `openclaw devices approve <requestId>`)\n"
+ "2) `/pair approve` in Telegram\n"
+ "\(requestLine)\n"
+ "OpenClaw will also retry automatically when you return to this app.")
}
}
Section {
Button {
self.openQRScannerFromOnboarding()
} label: {
Label("Scan QR Code Again", systemImage: "qrcode.viewfinder")
}
.disabled(self.connectingGatewayID != nil)
Button {
Task { await self.retryLastAttempt() }
} label: {
if self.connectingGatewayID == "retry" {
ProgressView()
.progressViewStyle(.circular)
} else {
Text("Retry Connection")
}
}
.disabled(self.connectingGatewayID != nil)
}
}
}
private var successStep: some View {
VStack(spacing: 0) {
Spacer()
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 64))
.foregroundStyle(.green)
.padding(.bottom, 20)
Text("Connected")
.font(.largeTitle.weight(.bold))
.padding(.bottom, 8)
let server = self.appModel.gatewayServerName ?? "gateway"
Text(server)
.font(.subheadline)
.foregroundStyle(.secondary)
.padding(.bottom, 4)
if let addr = self.appModel.gatewayRemoteAddress {
Text(addr)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
Button {
self.onClose()
} label: {
Text("Open OpenClaw")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.padding(.horizontal, 24)
.padding(.bottom, 48)
}
}
@ViewBuilder
private func manualConnectionFieldsSection(title: String) -> some View {
Section(title) {
TextField("Host", text: self.$manualHost)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
TextField("Port", text: self.$manualPortText)
.keyboardType(.numberPad)
Toggle("Use TLS", isOn: self.$manualTLS)
TextField("Discovery Domain (optional)", text: self.$discoveryDomain)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
Button {
Task { await self.connectManual() }
} label: {
if self.connectingGatewayID == "manual" {
HStack(spacing: 8) {
ProgressView()
.progressViewStyle(.circular)
Text("Connecting…")
}
} else {
Text("Connect")
}
}
.disabled(!self.canConnectManual || self.connectingGatewayID != nil)
}
}
private func handleScannedLink(_ link: GatewayConnectDeepLink) {
self.manualHost = link.host
self.manualPort = link.port
self.manualTLS = link.tls
if let token = link.token {
self.gatewayToken = token
}
if let password = link.password {
self.gatewayPassword = password
}
self.saveGatewayCredentials(token: self.gatewayToken, password: self.gatewayPassword)
self.showQRScanner = false
self.connectMessage = "Connecting via QR code…"
self.statusLine = "QR loaded. Connecting to \(link.host):\(link.port)"
if self.selectedMode == nil {
self.selectedMode = link.tls ? .remoteDomain : .homeNetwork
}
Task { await self.connectManual() }
}
private func openQRScannerFromOnboarding() {
// Stop active reconnect loops before scanning new credentials.
self.appModel.disconnectGateway()
self.connectingGatewayID = nil
self.connectMessage = nil
self.issue = .none
self.pairingRequestId = nil
self.statusLine = "Opening QR scanner…"
self.showQRScanner = true
}
private func resumeAfterPairingApproval() {
// We intentionally stop reconnect churn while unpaired to avoid generating multiple pending requests.
self.appModel.gatewayAutoReconnectEnabled = true
self.appModel.gatewayPairingPaused = false
self.appModel.gatewayPairingRequestId = nil
// Pairing state is sticky to prevent UI flip-flop during reconnect churn.
// Once the user explicitly resumes after approving, clear the sticky issue
// so new status/auth errors can surface instead of being masked as pairing.
self.issue = .none
self.connectMessage = "Retrying after approval…"
self.statusLine = "Retrying after approval…"
Task { await self.retryLastAttempt() }
}
private func resumeAfterPairingApprovalInBackground() {
// Keep the pairing issue sticky to avoid visual flicker while we probe for approval.
self.appModel.gatewayAutoReconnectEnabled = true
self.appModel.gatewayPairingPaused = false
self.appModel.gatewayPairingRequestId = nil
Task { await self.retryLastAttempt(silent: true) }
}
private func attemptAutomaticPairingResumeIfNeeded() {
guard self.scenePhase == .active else { return }
guard self.step == .auth else { return }
guard self.issue.needsPairing else { return }
guard self.connectingGatewayID == nil else { return }
let now = Date()
if let last = self.lastPairingAutoResumeAttemptAt, now.timeIntervalSince(last) < 6 {
return
}
self.lastPairingAutoResumeAttemptAt = now
self.resumeAfterPairingApprovalInBackground()
}
private func detectQRCode(from data: Data) -> String? {
guard let ciImage = CIImage(data: data) else { return nil }
let detector = CIDetector(
ofType: CIDetectorTypeQRCode, context: nil,
options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
let features = detector?.features(in: ciImage) ?? []
for feature in features {
if let qr = feature as? CIQRCodeFeature, let message = qr.messageString {
return message
}
}
return nil
}
private func navigateBack() {
guard let target = self.step.previous else { return }
self.connectingGatewayID = nil
self.connectMessage = nil
self.step = target
}
private var canConnectManual: Bool {
let host = self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines)
return !host.isEmpty && self.manualPort > 0 && self.manualPort <= 65535
}
private func initializeState() {
if self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
if let last = GatewaySettingsStore.loadLastGatewayConnection() {
switch last {
case let .manual(host, port, useTLS, _):
self.manualHost = host
self.manualPort = port
self.manualTLS = useTLS
case .discovered:
self.manualHost = "openclaw.local"
self.manualPort = 18789
self.manualTLS = true
}
} else {
self.manualHost = "openclaw.local"
self.manualPort = 18789
self.manualTLS = true
}
}
self.manualPortText = self.manualPort > 0 ? String(self.manualPort) : ""
if self.selectedMode == nil {
self.selectedMode = OnboardingStateStore.lastMode()
}
if self.selectedMode == .developerLocal && self.manualHost == "openclaw.local" {
self.manualHost = "localhost"
self.manualTLS = false
}
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedInstanceId.isEmpty {
self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? ""
self.gatewayPassword = GatewaySettingsStore.loadGatewayPassword(instanceId: trimmedInstanceId) ?? ""
}
let hasSavedGateway = GatewaySettingsStore.loadLastGatewayConnection() != nil
let hasToken = !self.gatewayToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
let hasPassword = !self.gatewayPassword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
if !self.didAutoPresentQR, !hasSavedGateway, !hasToken, !hasPassword {
self.didAutoPresentQR = true
self.statusLine = "No saved pairing found. Scan QR code to connect."
self.showQRScanner = true
}
}
private func scheduleDiscoveryRestart() {
self.discoveryRestartTask?.cancel()
self.discoveryRestartTask = Task { @MainActor in
try? await Task.sleep(nanoseconds: 350_000_000)
guard !Task.isCancelled else { return }
self.gatewayController.restartDiscovery()
}
}
private func saveGatewayCredentials(token: String, password: String) {
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedInstanceId.isEmpty else { return }
let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
GatewaySettingsStore.saveGatewayToken(trimmedToken, instanceId: trimmedInstanceId)
let trimmedPassword = password.trimmingCharacters(in: .whitespacesAndNewlines)
GatewaySettingsStore.saveGatewayPassword(trimmedPassword, instanceId: trimmedInstanceId)
}
private func connectDiscoveredGateway(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async {
self.connectingGatewayID = gateway.id
self.issue = .none
self.connectMessage = "Connecting to \(gateway.name)"
self.statusLine = "Connecting to \(gateway.name)"
defer { self.connectingGatewayID = nil }
await self.gatewayController.connect(gateway)
}
private func selectMode(_ mode: OnboardingConnectionMode) {
self.selectedMode = mode
self.applyModeDefaults(mode)
}
private func applyModeDefaults(_ mode: OnboardingConnectionMode) {
let host = self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let hostIsDefaultLike = host.isEmpty || host == "openclaw.local" || host == "localhost"
switch mode {
case .homeNetwork:
if hostIsDefaultLike { self.manualHost = "openclaw.local" }
self.manualTLS = true
if self.manualPort <= 0 || self.manualPort > 65535 { self.manualPort = 18789 }
case .remoteDomain:
if host == "openclaw.local" || host == "localhost" { self.manualHost = "" }
self.manualTLS = true
if self.manualPort <= 0 || self.manualPort > 65535 { self.manualPort = 18789 }
case .developerLocal:
if hostIsDefaultLike { self.manualHost = "localhost" }
self.manualTLS = false
if self.manualPort <= 0 || self.manualPort > 65535 { self.manualPort = 18789 }
}
}
private func gatewayHasResolvableHost(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> Bool {
let lanHost = gateway.lanHost?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !lanHost.isEmpty { return true }
let tailnetDns = gateway.tailnetDns?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return !tailnetDns.isEmpty
}
private func connectManual() async {
let host = self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines)
guard !host.isEmpty, self.manualPort > 0, self.manualPort <= 65535 else { return }
self.connectingGatewayID = "manual"
self.issue = .none
self.connectMessage = "Connecting to \(host)"
self.statusLine = "Connecting to \(host):\(self.manualPort)"
defer { self.connectingGatewayID = nil }
await self.gatewayController.connectManual(host: host, port: self.manualPort, useTLS: self.manualTLS)
}
private func retryLastAttempt(silent: Bool = false) async {
self.connectingGatewayID = silent ? "retry-auto" : "retry"
// Keep current auth/pairing issue sticky while retrying to avoid Step 3 UI flip-flop.
if !silent {
self.connectMessage = "Retrying…"
self.statusLine = "Retrying last connection…"
}
defer { self.connectingGatewayID = nil }
await self.gatewayController.connectLastKnown()
}
}
private struct OnboardingModeRow: View {
let title: String
let subtitle: String
let selected: Bool
let action: () -> Void
var body: some View {
Button(action: self.action) {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(self.title)
.font(.body.weight(.semibold))
Text(self.subtitle)
.font(.footnote)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: self.selected ? "checkmark.circle.fill" : "circle")
.foregroundStyle(self.selected ? Color.accentColor : Color.secondary)
}
}
.buttonStyle(.plain)
}
}

View File

@@ -0,0 +1,96 @@
import OpenClawKit
import SwiftUI
import VisionKit
struct QRScannerView: UIViewControllerRepresentable {
let onGatewayLink: (GatewayConnectDeepLink) -> Void
let onError: (String) -> Void
let onDismiss: () -> Void
func makeUIViewController(context: Context) -> UIViewController {
guard DataScannerViewController.isSupported else {
context.coordinator.reportError("QR scanning is not supported on this device.")
return UIViewController()
}
guard DataScannerViewController.isAvailable else {
context.coordinator.reportError("Camera scanning is currently unavailable.")
return UIViewController()
}
let scanner = DataScannerViewController(
recognizedDataTypes: [.barcode(symbologies: [.qr])],
isHighlightingEnabled: true)
scanner.delegate = context.coordinator
do {
try scanner.startScanning()
} catch {
context.coordinator.reportError("Could not start QR scanner.")
}
return scanner
}
func updateUIViewController(_: UIViewController, context _: Context) {}
static func dismantleUIViewController(_ uiViewController: UIViewController, coordinator: Coordinator) {
if let scanner = uiViewController as? DataScannerViewController {
scanner.stopScanning()
}
coordinator.parent.onDismiss()
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
final class Coordinator: NSObject, DataScannerViewControllerDelegate {
let parent: QRScannerView
private var handled = false
private var reportedError = false
init(parent: QRScannerView) {
self.parent = parent
}
func reportError(_ message: String) {
guard !self.reportedError else { return }
self.reportedError = true
Task { @MainActor in
self.parent.onError(message)
}
}
func dataScanner(_: DataScannerViewController, didAdd items: [RecognizedItem], allItems _: [RecognizedItem]) {
guard !self.handled else { return }
for item in items {
guard case let .barcode(barcode) = item,
let payload = barcode.payloadStringValue
else { continue }
// Try setup code format first (base64url JSON from /pair qr).
if let link = GatewayConnectDeepLink.fromSetupCode(payload) {
self.handled = true
self.parent.onGatewayLink(link)
return
}
// Fall back to deep link URL format (openclaw://gateway?...).
if let url = URL(string: payload),
let route = DeepLinkParser.parse(url),
case let .gateway(link) = route
{
self.handled = true
self.parent.onGatewayLink(link)
return
}
}
}
func dataScanner(_: DataScannerViewController, didRemove _: [RecognizedItem], allItems _: [RecognizedItem]) {}
func dataScanner(
_: DataScannerViewController,
becameUnavailableWithError _: DataScannerViewController.ScanningUnavailable)
{
self.reportError("Camera is not available on this device.")
}
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>

View File

@@ -1,12 +1,73 @@
import SwiftUI
import Foundation
import os
import UIKit
final class OpenClawAppDelegate: NSObject, UIApplicationDelegate {
private let logger = Logger(subsystem: "ai.openclaw.ios", category: "Push")
private var pendingAPNsDeviceToken: Data?
weak var appModel: NodeAppModel? {
didSet {
guard let model = self.appModel, let token = self.pendingAPNsDeviceToken else { return }
self.pendingAPNsDeviceToken = nil
Task { @MainActor in
model.updateAPNsDeviceToken(token)
}
}
}
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool
{
application.registerForRemoteNotifications()
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
if let appModel = self.appModel {
Task { @MainActor in
appModel.updateAPNsDeviceToken(deviceToken)
}
return
}
self.pendingAPNsDeviceToken = deviceToken
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
self.logger.error("APNs registration failed: \(error.localizedDescription, privacy: .public)")
}
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
self.logger.info("APNs remote notification received keys=\(userInfo.keys.count, privacy: .public)")
Task { @MainActor in
guard let appModel = self.appModel else {
self.logger.info("APNs wake skipped: appModel unavailable")
completionHandler(.noData)
return
}
let handled = await appModel.handleSilentPushWake(userInfo)
self.logger.info("APNs wake handled=\(handled, privacy: .public)")
completionHandler(handled ? .newData : .noData)
}
}
}
@main
struct OpenClawApp: App {
@State private var appModel: NodeAppModel
@State private var gatewayController: GatewayConnectionController
@UIApplicationDelegateAdaptor(OpenClawAppDelegate.self) private var appDelegate
@Environment(\.scenePhase) private var scenePhase
init() {
Self.installUncaughtExceptionLogger()
GatewaySettingsStore.bootstrapPersistence()
let appModel = NodeAppModel()
_appModel = State(initialValue: appModel)
@@ -19,6 +80,9 @@ struct OpenClawApp: App {
.environment(self.appModel)
.environment(self.appModel.voiceWake)
.environment(self.gatewayController)
.task {
self.appDelegate.appModel = self.appModel
}
.onOpenURL { url in
Task { await self.appModel.handleDeepLink(url: url) }
}
@@ -29,3 +93,18 @@ struct OpenClawApp: App {
}
}
}
extension OpenClawApp {
private static func installUncaughtExceptionLogger() {
NSLog("OpenClaw: installing uncaught exception handler")
NSSetUncaughtExceptionHandler { exception in
// Useful when the app hits NSExceptions from SwiftUI/WebKit internals; these do not
// produce a normal Swift error backtrace.
let reason = exception.reason ?? "(no reason)"
NSLog("UNCAUGHT EXCEPTION: %@ %@", exception.name.rawValue, reason)
for line in exception.callStackSymbols {
NSLog(" %@", line)
}
}
}
}

View File

@@ -6,7 +6,7 @@ final class RemindersService: RemindersServicing {
func list(params: OpenClawRemindersListParams) async throws -> OpenClawRemindersListPayload {
let store = EKEventStore()
let status = EKEventStore.authorizationStatus(for: .reminder)
let authorized = await Self.ensureAuthorization(store: store, status: status)
let authorized = EventKitAuthorization.allowsRead(status: status)
guard authorized else {
throw NSError(domain: "Reminders", code: 1, userInfo: [
NSLocalizedDescriptionKey: "REMINDERS_PERMISSION_REQUIRED: grant Reminders permission",
@@ -50,7 +50,7 @@ final class RemindersService: RemindersServicing {
func add(params: OpenClawRemindersAddParams) async throws -> OpenClawRemindersAddPayload {
let store = EKEventStore()
let status = EKEventStore.authorizationStatus(for: .reminder)
let authorized = await Self.ensureWriteAuthorization(store: store, status: status)
let authorized = EventKitAuthorization.allowsWrite(status: status)
guard authorized else {
throw NSError(domain: "Reminders", code: 2, userInfo: [
NSLocalizedDescriptionKey: "REMINDERS_PERMISSION_REQUIRED: grant Reminders permission",
@@ -100,38 +100,6 @@ final class RemindersService: RemindersServicing {
return OpenClawRemindersAddPayload(reminder: payload)
}
private static func ensureAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool {
switch status {
case .authorized:
return true
case .notDetermined:
// Dont prompt during node.invoke; prompts block the invoke and lead to timeouts.
return false
case .restricted, .denied:
return false
case .fullAccess:
return true
case .writeOnly:
return false
@unknown default:
return false
}
}
private static func ensureWriteAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool {
switch status {
case .authorized, .fullAccess, .writeOnly:
return true
case .notDetermined:
// Dont prompt during node.invoke; prompts block the invoke and lead to timeouts.
return false
case .restricted, .denied:
return false
@unknown default:
return false
}
}
private static func resolveList(
store: EKEventStore,
listId: String?,

View File

@@ -3,34 +3,69 @@ import UIKit
struct RootCanvas: View {
@Environment(NodeAppModel.self) private var appModel
@Environment(GatewayConnectionController.self) private var gatewayController
@Environment(VoiceWakeManager.self) private var voiceWake
@Environment(\.colorScheme) private var systemColorScheme
@Environment(\.scenePhase) private var scenePhase
@AppStorage(VoiceWakePreferences.enabledKey) private var voiceWakeEnabled: Bool = false
@AppStorage("screen.preventSleep") private var preventSleep: Bool = true
@AppStorage("canvas.debugStatusEnabled") private var canvasDebugStatusEnabled: Bool = false
@AppStorage("onboarding.requestID") private var onboardingRequestID: Int = 0
@AppStorage("gateway.onboardingComplete") private var onboardingComplete: Bool = false
@AppStorage("gateway.hasConnectedOnce") private var hasConnectedOnce: Bool = false
@AppStorage("gateway.preferredStableID") private var preferredGatewayStableID: String = ""
@AppStorage("gateway.manual.enabled") private var manualGatewayEnabled: Bool = false
@AppStorage("gateway.manual.host") private var manualGatewayHost: String = ""
@AppStorage("onboarding.quickSetupDismissed") private var quickSetupDismissed: Bool = false
@State private var presentedSheet: PresentedSheet?
@State private var voiceWakeToastText: String?
@State private var toastDismissTask: Task<Void, Never>?
@State private var showOnboarding: Bool = false
@State private var onboardingAllowSkip: Bool = true
@State private var didEvaluateOnboarding: Bool = false
@State private var didAutoOpenSettings: Bool = false
private enum PresentedSheet: Identifiable {
case settings
case chat
case quickSetup
var id: Int {
switch self {
case .settings: 0
case .chat: 1
case .quickSetup: 2
}
}
}
enum StartupPresentationRoute: Equatable {
case none
case onboarding
case settings
}
static func startupPresentationRoute(
gatewayConnected: Bool,
hasConnectedOnce: Bool,
onboardingComplete: Bool,
hasExistingGatewayConfig: Bool,
shouldPresentOnLaunch: Bool) -> StartupPresentationRoute
{
if gatewayConnected {
return .none
}
// On first run or explicit launch onboarding state, onboarding always wins.
if shouldPresentOnLaunch || !hasConnectedOnce || !onboardingComplete {
return .onboarding
}
// Settings auto-open is a recovery path for previously-connected installs only.
if !hasExistingGatewayConfig {
return .settings
}
return .none
}
var body: some View {
ZStack {
CanvasContent(
@@ -57,30 +92,63 @@ struct RootCanvas: View {
switch sheet {
case .settings:
SettingsTab()
.environment(self.appModel)
.environment(self.appModel.voiceWake)
.environment(self.gatewayController)
case .chat:
ChatSheet(
// Chat RPCs run on the operator session (read/write scopes).
gateway: self.appModel.operatorSession,
sessionKey: self.appModel.mainSessionKey,
agentName: self.appModel.activeAgentName,
userAccent: self.appModel.seamColor)
case .quickSetup:
GatewayQuickSetupSheet()
.environment(self.appModel)
.environment(self.gatewayController)
}
}
.fullScreenCover(isPresented: self.$showOnboarding) {
OnboardingWizardView(
allowSkip: self.onboardingAllowSkip,
onClose: {
self.showOnboarding = false
})
.environment(self.appModel)
.environment(self.appModel.voiceWake)
.environment(self.gatewayController)
}
.onAppear { self.updateIdleTimer() }
.onAppear { self.evaluateOnboardingPresentation(force: false) }
.onAppear { self.maybeAutoOpenSettings() }
.onChange(of: self.preventSleep) { _, _ in self.updateIdleTimer() }
.onChange(of: self.scenePhase) { _, _ in self.updateIdleTimer() }
.onAppear { self.maybeShowQuickSetup() }
.onChange(of: self.gatewayController.gateways.count) { _, _ in self.maybeShowQuickSetup() }
.onAppear { self.updateCanvasDebugStatus() }
.onChange(of: self.canvasDebugStatusEnabled) { _, _ in self.updateCanvasDebugStatus() }
.onChange(of: self.appModel.gatewayStatusText) { _, _ in self.updateCanvasDebugStatus() }
.onChange(of: self.appModel.gatewayServerName) { _, _ in self.updateCanvasDebugStatus() }
.onChange(of: self.appModel.gatewayServerName) { _, newValue in
if newValue != nil {
self.showOnboarding = false
}
}
.onChange(of: self.onboardingRequestID) { _, _ in
self.evaluateOnboardingPresentation(force: true)
}
.onChange(of: self.appModel.gatewayRemoteAddress) { _, _ in self.updateCanvasDebugStatus() }
.onChange(of: self.appModel.gatewayServerName) { _, newValue in
if newValue != nil {
self.onboardingComplete = true
self.hasConnectedOnce = true
OnboardingStateStore.markCompleted(mode: nil)
}
self.maybeAutoOpenSettings()
}
.onChange(of: self.appModel.openChatRequestID) { _, _ in
self.presentedSheet = .chat
}
.onChange(of: self.voiceWake.lastTriggeredCommand) { _, newValue in
guard let newValue else { return }
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -136,11 +204,31 @@ struct RootCanvas: View {
self.appModel.screen.updateDebugStatus(title: title, subtitle: subtitle)
}
private func shouldAutoOpenSettings() -> Bool {
if self.appModel.gatewayServerName != nil { return false }
if !self.hasConnectedOnce { return true }
if !self.onboardingComplete { return true }
return !self.hasExistingGatewayConfig()
private func evaluateOnboardingPresentation(force: Bool) {
if force {
self.onboardingAllowSkip = true
self.showOnboarding = true
return
}
guard !self.didEvaluateOnboarding else { return }
self.didEvaluateOnboarding = true
let route = Self.startupPresentationRoute(
gatewayConnected: self.appModel.gatewayServerName != nil,
hasConnectedOnce: self.hasConnectedOnce,
onboardingComplete: self.onboardingComplete,
hasExistingGatewayConfig: self.hasExistingGatewayConfig(),
shouldPresentOnLaunch: OnboardingStateStore.shouldPresentOnLaunch(appModel: self.appModel))
switch route {
case .none:
break
case .onboarding:
self.onboardingAllowSkip = true
self.showOnboarding = true
case .settings:
self.didAutoOpenSettings = true
self.presentedSheet = .settings
}
}
private func hasExistingGatewayConfig() -> Bool {
@@ -151,10 +239,26 @@ struct RootCanvas: View {
private func maybeAutoOpenSettings() {
guard !self.didAutoOpenSettings else { return }
guard self.shouldAutoOpenSettings() else { return }
guard !self.showOnboarding else { return }
let route = Self.startupPresentationRoute(
gatewayConnected: self.appModel.gatewayServerName != nil,
hasConnectedOnce: self.hasConnectedOnce,
onboardingComplete: self.onboardingComplete,
hasExistingGatewayConfig: self.hasExistingGatewayConfig(),
shouldPresentOnLaunch: false)
guard route == .settings else { return }
self.didAutoOpenSettings = true
self.presentedSheet = .settings
}
private func maybeShowQuickSetup() {
guard !self.quickSetupDismissed else { return }
guard !self.showOnboarding else { return }
guard self.presentedSheet == nil else { return }
guard self.appModel.gatewayServerName == nil else { return }
guard !self.gatewayController.gateways.isEmpty else { return }
self.presentedSheet = .quickSetup
}
}
private struct CanvasContent: View {

View File

@@ -3,6 +3,7 @@ import SwiftUI
struct RootTabs: View {
@Environment(NodeAppModel.self) private var appModel
@Environment(VoiceWakeManager.self) private var voiceWake
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@AppStorage(VoiceWakePreferences.enabledKey) private var voiceWakeEnabled: Bool = false
@State private var selectedTab: Int = 0
@State private var voiceWakeToastText: String?
@@ -52,14 +53,14 @@ struct RootTabs: View {
guard !trimmed.isEmpty else { return }
self.toastDismissTask?.cancel()
withAnimation(.spring(response: 0.25, dampingFraction: 0.85)) {
withAnimation(self.reduceMotion ? .none : .spring(response: 0.25, dampingFraction: 0.85)) {
self.voiceWakeToastText = trimmed
}
self.toastDismissTask = Task {
try? await Task.sleep(nanoseconds: 2_300_000_000)
await MainActor.run {
withAnimation(.easeOut(duration: 0.25)) {
withAnimation(self.reduceMotion ? .none : .easeOut(duration: 0.25)) {
self.voiceWakeToastText = nil
}
}
@@ -104,66 +105,10 @@ struct RootTabs: View {
}
private var statusActivity: StatusPill.Activity? {
// Keep the top pill consistent across tabs (camera + voice wake + pairing states).
if self.appModel.isBackgrounded {
return StatusPill.Activity(
title: "Foreground required",
systemImage: "exclamationmark.triangle.fill",
tint: .orange)
}
let gatewayStatus = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
let gatewayLower = gatewayStatus.lowercased()
if gatewayLower.contains("repair") {
return StatusPill.Activity(title: "Repairing…", systemImage: "wrench.and.screwdriver", tint: .orange)
}
if gatewayLower.contains("approval") || gatewayLower.contains("pairing") {
return StatusPill.Activity(title: "Approval pending", systemImage: "person.crop.circle.badge.clock")
}
// Avoid duplicating the primary gateway status ("Connecting") in the activity slot.
if self.appModel.screenRecordActive {
return StatusPill.Activity(title: "Recording screen…", systemImage: "record.circle.fill", tint: .red)
}
if let cameraHUDText = self.appModel.cameraHUDText,
let cameraHUDKind = self.appModel.cameraHUDKind,
!cameraHUDText.isEmpty
{
let systemImage: String
let tint: Color?
switch cameraHUDKind {
case .photo:
systemImage = "camera.fill"
tint = nil
case .recording:
systemImage = "video.fill"
tint = .red
case .success:
systemImage = "checkmark.circle.fill"
tint = .green
case .error:
systemImage = "exclamationmark.triangle.fill"
tint = .red
}
return StatusPill.Activity(title: cameraHUDText, systemImage: systemImage, tint: tint)
}
if self.voiceWakeEnabled {
let voiceStatus = self.appModel.voiceWake.statusText
if voiceStatus.localizedCaseInsensitiveContains("microphone permission") {
return StatusPill.Activity(title: "Mic permission", systemImage: "mic.slash", tint: .orange)
}
if voiceStatus == "Paused" {
// Talk mode intentionally pauses voice wake to release the mic. Don't spam the HUD for that case.
if self.appModel.talkMode.isEnabled {
return nil
}
let suffix = self.appModel.isBackgrounded ? " (background)" : ""
return StatusPill.Activity(title: "Voice Wake paused\(suffix)", systemImage: "pause.circle.fill")
}
}
return nil
StatusActivityBuilder.build(
appModel: self.appModel,
voiceWakeEnabled: self.voiceWakeEnabled,
cameraHUDText: self.appModel.cameraHUDText,
cameraHUDKind: self.appModel.cameraHUDKind)
}
}

View File

@@ -1,14 +1,12 @@
import OpenClawKit
import Observation
import SwiftUI
import UIKit
import WebKit
@MainActor
@Observable
final class ScreenController {
let webView: WKWebView
private let navigationDelegate: ScreenNavigationDelegate
private let a2uiActionHandler: CanvasA2UIActionMessageHandler
private weak var activeWebView: WKWebView?
var urlString: String = ""
var errorText: String?
@@ -24,29 +22,6 @@ final class ScreenController {
private var debugStatusSubtitle: String?
init() {
let config = WKWebViewConfiguration()
config.websiteDataStore = .nonPersistent()
let a2uiActionHandler = CanvasA2UIActionMessageHandler()
let userContentController = WKUserContentController()
for name in CanvasA2UIActionMessageHandler.handlerNames {
userContentController.add(a2uiActionHandler, name: name)
}
config.userContentController = userContentController
self.navigationDelegate = ScreenNavigationDelegate()
self.a2uiActionHandler = a2uiActionHandler
self.webView = WKWebView(frame: .zero, configuration: config)
// Canvas scaffold is a fully self-contained HTML page; avoid relying on transparency underlays.
self.webView.isOpaque = true
self.webView.backgroundColor = .black
self.webView.scrollView.backgroundColor = .black
self.webView.scrollView.contentInsetAdjustmentBehavior = .never
self.webView.scrollView.contentInset = .zero
self.webView.scrollView.scrollIndicatorInsets = .zero
self.webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
self.applyScrollBehavior()
self.webView.navigationDelegate = self.navigationDelegate
self.navigationDelegate.controller = self
a2uiActionHandler.controller = self
self.reload()
}
@@ -71,24 +46,26 @@ final class ScreenController {
}
func reload() {
let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines)
self.applyScrollBehavior()
guard let webView = self.activeWebView else { return }
let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
guard let url = Self.canvasScaffoldURL else { return }
self.errorText = nil
self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
return
}
guard let url = URL(string: trimmed) else {
self.errorText = "Invalid URL: \(trimmed)"
return
}
self.errorText = nil
if url.isFileURL {
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
} else {
guard let url = URL(string: trimmed) else {
self.errorText = "Invalid URL: \(trimmed)"
return
}
self.errorText = nil
if url.isFileURL {
self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
} else {
self.webView.load(URLRequest(url: url))
}
webView.load(URLRequest(url: url))
}
}
@@ -108,7 +85,8 @@ final class ScreenController {
self.applyDebugStatusIfNeeded()
}
fileprivate func applyDebugStatusIfNeeded() {
func applyDebugStatusIfNeeded() {
guard let webView = self.activeWebView else { return }
let enabled = self.debugStatusEnabled
let title = self.debugStatusTitle
let subtitle = self.debugStatusSubtitle
@@ -127,7 +105,7 @@ final class ScreenController {
} catch (_) {}
})()
"""
self.webView.evaluateJavaScript(js) { _, _ in }
webView.evaluateJavaScript(js) { _, _ in }
}
func waitForA2UIReady(timeoutMs: Int) async -> Bool {
@@ -154,8 +132,13 @@ final class ScreenController {
}
func eval(javaScript: String) async throws -> String {
try await withCheckedThrowingContinuation { cont in
self.webView.evaluateJavaScript(javaScript) { result, error in
guard let webView = self.activeWebView else {
throw NSError(domain: "Screen", code: 3, userInfo: [
NSLocalizedDescriptionKey: "web view unavailable",
])
}
return try await withCheckedThrowingContinuation { cont in
webView.evaluateJavaScript(javaScript) { result, error in
if let error {
cont.resume(throwing: error)
return
@@ -174,8 +157,13 @@ final class ScreenController {
if let maxWidth {
config.snapshotWidth = NSNumber(value: Double(maxWidth))
}
guard let webView = self.activeWebView else {
throw NSError(domain: "Screen", code: 3, userInfo: [
NSLocalizedDescriptionKey: "web view unavailable",
])
}
let image: UIImage = try await withCheckedThrowingContinuation { cont in
self.webView.takeSnapshot(with: config) { image, error in
webView.takeSnapshot(with: config) { image, error in
if let error {
cont.resume(throwing: error)
return
@@ -206,8 +194,13 @@ final class ScreenController {
if let maxWidth {
config.snapshotWidth = NSNumber(value: Double(maxWidth))
}
guard let webView = self.activeWebView else {
throw NSError(domain: "Screen", code: 3, userInfo: [
NSLocalizedDescriptionKey: "web view unavailable",
])
}
let image: UIImage = try await withCheckedThrowingContinuation { cont in
self.webView.takeSnapshot(with: config) { image, error in
webView.takeSnapshot(with: config) { image, error in
if let error {
cont.resume(throwing: error)
return
@@ -238,6 +231,17 @@ final class ScreenController {
return data.base64EncodedString()
}
func attachWebView(_ webView: WKWebView) {
self.activeWebView = webView
self.reload()
self.applyDebugStatusIfNeeded()
}
func detachWebView(_ webView: WKWebView) {
guard self.activeWebView === webView else { return }
self.activeWebView = nil
}
private static func bundledResourceURL(
name: String,
ext: String,
@@ -277,9 +281,10 @@ final class ScreenController {
}
private func applyScrollBehavior() {
guard let webView = self.activeWebView else { return }
let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines)
let allowScroll = !trimmed.isEmpty
let scrollView = self.webView.scrollView
let scrollView = webView.scrollView
// Default canvas needs raw touch events; external pages should scroll.
scrollView.isScrollEnabled = allowScroll
scrollView.bounces = allowScroll
@@ -366,72 +371,3 @@ extension Double {
return self
}
}
// MARK: - Navigation Delegate
/// Handles navigation policy to intercept openclaw:// deep links from canvas
@MainActor
private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
weak var controller: ScreenController?
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void)
{
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
// Intercept openclaw:// deep links.
if url.scheme?.lowercased() == "openclaw" {
decisionHandler(.cancel)
self.controller?.onDeepLink?(url)
return
}
decisionHandler(.allow)
}
func webView(
_: WKWebView,
didFailProvisionalNavigation _: WKNavigation?,
withError error: any Error)
{
self.controller?.errorText = error.localizedDescription
}
func webView(_: WKWebView, didFinish _: WKNavigation?) {
self.controller?.errorText = nil
self.controller?.applyDebugStatusIfNeeded()
}
func webView(_: WKWebView, didFail _: WKNavigation?, withError error: any Error) {
self.controller?.errorText = error.localizedDescription
}
}
private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
static let messageName = "openclawCanvasA2UIAction"
static let handlerNames = [messageName]
weak var controller: ScreenController?
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
guard Self.handlerNames.contains(message.name) else { return }
guard let controller else { return }
guard let url = message.webView?.url else { return }
if url.isFileURL {
guard controller.isTrustedCanvasUIURL(url) else { return }
} else {
// For security, only accept actions from local-network pages (e.g. the canvas host).
guard controller.isLocalNetworkCanvasURL(url) else { return }
}
guard let body = ScreenController.parseA2UIActionBody(message.body) else { return }
controller.onA2UIAction?(body)
}
}

View File

@@ -5,11 +5,189 @@ import WebKit
struct ScreenWebView: UIViewRepresentable {
var controller: ScreenController
func makeUIView(context: Context) -> WKWebView {
self.controller.webView
func makeCoordinator() -> ScreenWebViewCoordinator {
ScreenWebViewCoordinator(controller: self.controller)
}
func updateUIView(_ webView: WKWebView, context: Context) {
// State changes are driven by ScreenController.
func makeUIView(context: Context) -> UIView {
context.coordinator.makeContainerView()
}
func updateUIView(_: UIView, context: Context) {
context.coordinator.updateController(self.controller)
}
static func dismantleUIView(_: UIView, coordinator: ScreenWebViewCoordinator) {
coordinator.teardown()
}
}
@MainActor
final class ScreenWebViewCoordinator: NSObject {
private weak var controller: ScreenController?
private let navigationDelegate = ScreenNavigationDelegate()
private let a2uiActionHandler = CanvasA2UIActionMessageHandler()
private let userContentController = WKUserContentController()
private(set) var managedWebView: WKWebView?
private weak var containerView: UIView?
init(controller: ScreenController) {
self.controller = controller
super.init()
self.navigationDelegate.controller = controller
self.a2uiActionHandler.controller = controller
}
func makeContainerView() -> UIView {
if let containerView {
return containerView
}
let container = UIView(frame: .zero)
container.backgroundColor = .black
let webView = Self.makeWebView(userContentController: self.userContentController)
webView.navigationDelegate = self.navigationDelegate
self.installA2UIHandlers()
webView.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(webView)
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
webView.topAnchor.constraint(equalTo: container.topAnchor),
webView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
self.managedWebView = webView
self.containerView = container
self.controller?.attachWebView(webView)
return container
}
func updateController(_ controller: ScreenController) {
let previousController = self.controller
let controllerChanged = self.controller !== controller
self.controller = controller
self.navigationDelegate.controller = controller
self.a2uiActionHandler.controller = controller
if controllerChanged, let managedWebView {
previousController?.detachWebView(managedWebView)
controller.attachWebView(managedWebView)
}
}
func teardown() {
if let managedWebView {
self.controller?.detachWebView(managedWebView)
managedWebView.navigationDelegate = nil
}
self.removeA2UIHandlers()
self.navigationDelegate.controller = nil
self.a2uiActionHandler.controller = nil
self.managedWebView = nil
self.containerView = nil
}
private static func makeWebView(userContentController: WKUserContentController) -> WKWebView {
let config = WKWebViewConfiguration()
config.websiteDataStore = .nonPersistent()
config.userContentController = userContentController
let webView = WKWebView(frame: .zero, configuration: config)
// Canvas scaffold is a fully self-contained HTML page; avoid relying on transparency underlays.
webView.isOpaque = true
webView.backgroundColor = .black
let scrollView = webView.scrollView
scrollView.backgroundColor = .black
scrollView.contentInsetAdjustmentBehavior = .never
scrollView.contentInset = .zero
scrollView.scrollIndicatorInsets = .zero
scrollView.automaticallyAdjustsScrollIndicatorInsets = false
return webView
}
private func installA2UIHandlers() {
for name in CanvasA2UIActionMessageHandler.handlerNames {
self.userContentController.add(self.a2uiActionHandler, name: name)
}
}
private func removeA2UIHandlers() {
for name in CanvasA2UIActionMessageHandler.handlerNames {
self.userContentController.removeScriptMessageHandler(forName: name)
}
}
}
// MARK: - Navigation Delegate
/// Handles navigation policy to intercept openclaw:// deep links from canvas
@MainActor
private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
weak var controller: ScreenController?
func webView(
_: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void)
{
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
// Intercept openclaw:// deep links.
if url.scheme?.lowercased() == "openclaw" {
decisionHandler(.cancel)
self.controller?.onDeepLink?(url)
return
}
decisionHandler(.allow)
}
func webView(
_: WKWebView,
didFailProvisionalNavigation _: WKNavigation?,
withError error: any Error)
{
self.controller?.errorText = error.localizedDescription
}
func webView(_: WKWebView, didFinish _: WKNavigation?) {
self.controller?.errorText = nil
self.controller?.applyDebugStatusIfNeeded()
}
func webView(_: WKWebView, didFail _: WKNavigation?, withError error: any Error) {
self.controller?.errorText = error.localizedDescription
}
}
private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
static let messageName = "openclawCanvasA2UIAction"
static let handlerNames = [messageName]
weak var controller: ScreenController?
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
guard Self.handlerNames.contains(message.name) else { return }
guard let controller else { return }
guard let url = message.webView?.url else { return }
if url.isFileURL {
guard controller.isTrustedCanvasUIURL(url) else { return }
} else {
// For security, only accept actions from local-network pages (e.g. the canvas host).
guard controller.isLocalNetworkCanvasURL(url) else { return }
}
guard let body = ScreenController.parseA2UIActionBody(message.body) else { return }
controller.onA2UIAction?(body)
}
}

View File

@@ -28,6 +28,12 @@ protocol LocationServicing: Sendable {
desiredAccuracy: OpenClawLocationAccuracy,
maxAgeMs: Int?,
timeoutMs: Int?) async throws -> CLLocation
func startLocationUpdates(
desiredAccuracy: OpenClawLocationAccuracy,
significantChangesOnly: Bool) -> AsyncStream<CLLocation>
func stopLocationUpdates()
func startMonitoringSignificantLocationChanges(onUpdate: @escaping @Sendable (CLLocation) -> Void)
func stopMonitoringSignificantLocationChanges()
}
protocol DeviceStatusServicing: Sendable {
@@ -59,6 +65,29 @@ protocol MotionServicing: Sendable {
func pedometer(params: OpenClawPedometerParams) async throws -> OpenClawPedometerPayload
}
struct WatchMessagingStatus: Sendable, Equatable {
var supported: Bool
var paired: Bool
var appInstalled: Bool
var reachable: Bool
var activationState: String
}
struct WatchNotificationSendResult: Sendable, Equatable {
var deliveredImmediately: Bool
var queuedForDelivery: Bool
var transport: String
}
protocol WatchMessagingServicing: AnyObject, Sendable {
func status() async -> WatchMessagingStatus
func sendNotification(
id: String,
title: String,
body: String,
priority: OpenClawNotificationPriority?) async throws -> WatchNotificationSendResult
}
extension CameraController: CameraServicing {}
extension ScreenRecordService: ScreenRecordingServicing {}
extension LocationService: LocationServicing {}

View File

@@ -0,0 +1,176 @@
import Foundation
import OpenClawKit
import OSLog
@preconcurrency import WatchConnectivity
enum WatchMessagingError: LocalizedError {
case unsupported
case notPaired
case watchAppNotInstalled
var errorDescription: String? {
switch self {
case .unsupported:
"WATCH_UNAVAILABLE: WatchConnectivity is not supported on this device"
case .notPaired:
"WATCH_UNAVAILABLE: no paired Apple Watch"
case .watchAppNotInstalled:
"WATCH_UNAVAILABLE: OpenClaw watch companion app is not installed"
}
}
}
final class WatchMessagingService: NSObject, WatchMessagingServicing, @unchecked Sendable {
private static let logger = Logger(subsystem: "ai.openclaw", category: "watch.messaging")
private let session: WCSession?
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()
}
}
static func isSupportedOnDevice() -> Bool {
WCSession.isSupported()
}
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)
}
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")
}
return Self.status(for: session)
}
func sendNotification(
id: String,
title: String,
body: String,
priority: OpenClawNotificationPriority?) 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 }
let payload: [String: Any] = [
"type": "watch.notify",
"id": id,
"title": title,
"body": body,
"priority": priority?.rawValue ?? OpenClawNotificationPriority.active.rawValue,
"sentAtMs": Int(Date().timeIntervalSince1970 * 1000),
]
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")
}
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)
})
}
}
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 static func status(for session: WCSession) -> WatchMessagingStatus {
WatchMessagingStatus(
supported: true,
paired: session.isPaired,
appInstalled: session.isWatchAppInstalled,
reachable: session.isReachable,
activationState: activationStateLabel(session.activationState))
}
private static func activationStateLabel(_ state: WCSessionActivationState) -> String {
switch state {
case .notActivated:
"notActivated"
case .inactive:
"inactive"
case .activated:
"activated"
@unknown default:
"unknown"
}
}
}
extension WatchMessagingService: WCSessionDelegate {
func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: (any Error)?)
{
if let error {
Self.logger.error("watch activation failed: \(error.localizedDescription, privacy: .public)")
return
}
Self.logger.debug("watch activation state=\(Self.activationStateLabel(activationState), privacy: .public)")
}
func sessionDidBecomeInactive(_ session: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) {
session.activate()
}
func sessionReachabilityDidChange(_ session: WCSession) {}
}

View File

@@ -6,6 +6,12 @@ import SwiftUI
import UIKit
struct SettingsTab: View {
private struct FeatureHelp: Identifiable {
let id = UUID()
let title: String
let message: String
}
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
@Environment(VoiceWakeManager.self) private var voiceWake: VoiceWakeManager
@Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController
@@ -15,9 +21,10 @@ struct SettingsTab: View {
@AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
@AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
@AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false
@AppStorage("talk.voiceDirectiveHint.enabled") private var talkVoiceDirectiveHintEnabled: Bool = true
@AppStorage("camera.enabled") private var cameraEnabled: Bool = true
@AppStorage("location.enabledMode") private var locationEnabledModeRaw: String = OpenClawLocationMode.off.rawValue
@AppStorage("location.preciseEnabled") private var locationPreciseEnabled: Bool = true
@AppStorage("screen.preventSleep") private var preventSleep: Bool = true
@AppStorage("gateway.preferredStableID") private var preferredGatewayStableID: String = ""
@AppStorage("gateway.lastDiscoveredStableID") private var lastDiscoveredGatewayStableID: String = ""
@@ -28,17 +35,27 @@ struct SettingsTab: View {
@AppStorage("gateway.manual.tls") private var manualGatewayTLS: Bool = true
@AppStorage("gateway.discovery.debugLogs") private var discoveryDebugLogsEnabled: Bool = false
@AppStorage("canvas.debugStatusEnabled") private var canvasDebugStatusEnabled: Bool = false
// Onboarding control (RootCanvas listens to onboarding.requestID and force-opens the wizard).
@AppStorage("onboarding.requestID") private var onboardingRequestID: Int = 0
@AppStorage("gateway.onboardingComplete") private var onboardingComplete: Bool = false
@AppStorage("gateway.hasConnectedOnce") private var hasConnectedOnce: Bool = false
@State private var connectingGatewayID: String?
@State private var localIPAddress: String?
@State private var lastLocationModeRaw: String = OpenClawLocationMode.off.rawValue
@State private var gatewayToken: String = ""
@State private var gatewayPassword: String = ""
@State private var defaultShareInstruction: String = ""
@AppStorage("gateway.setupCode") private var setupCode: String = ""
@State private var setupStatusText: String?
@State private var manualGatewayPortText: String = ""
@State private var gatewayExpanded: Bool = true
@State private var selectedAgentPickerId: String = ""
@State private var showResetOnboardingAlert: Bool = false
@State private var activeFeatureHelp: FeatureHelp?
@State private var suppressCredentialPersist: Bool = false
private let gatewayLogger = Logger(subsystem: "ai.openclaw.ios", category: "GatewaySettings")
var body: some View {
@@ -103,7 +120,6 @@ struct SettingsTab: View {
.foregroundStyle(.secondary)
}
DisclosureGroup("Advanced") {
if self.appModel.gatewayServerName == nil {
LabeledContent("Discovery", value: self.gatewayController.discoveryStatusText)
}
@@ -148,69 +164,74 @@ struct SettingsTab: View {
self.gatewayList(showing: .all)
}
Toggle("Use Manual Gateway", isOn: self.$manualGatewayEnabled)
DisclosureGroup("Advanced") {
Toggle("Use Manual Gateway", isOn: self.$manualGatewayEnabled)
TextField("Host", text: self.$manualGatewayHost)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
TextField("Host", text: self.$manualGatewayHost)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
TextField("Port (optional)", text: self.manualPortBinding)
.keyboardType(.numberPad)
TextField("Port (optional)", text: self.manualPortBinding)
.keyboardType(.numberPad)
Toggle("Use TLS", isOn: self.$manualGatewayTLS)
Toggle("Use TLS", isOn: self.$manualGatewayTLS)
Button {
Task { await self.connectManual() }
} label: {
if self.connectingGatewayID == "manual" {
HStack(spacing: 8) {
ProgressView()
.progressViewStyle(.circular)
Text("Connecting…")
Button {
Task { await self.connectManual() }
} label: {
if self.connectingGatewayID == "manual" {
HStack(spacing: 8) {
ProgressView()
.progressViewStyle(.circular)
Text("Connecting…")
}
} else {
Text("Connect (Manual)")
}
} else {
Text("Connect (Manual)")
}
}
.disabled(self.connectingGatewayID != nil || self.manualGatewayHost
.trimmingCharacters(in: .whitespacesAndNewlines)
.isEmpty || !self.manualPortIsValid)
.disabled(self.connectingGatewayID != nil || self.manualGatewayHost
.trimmingCharacters(in: .whitespacesAndNewlines)
.isEmpty || !self.manualPortIsValid)
Text(
"Use this when mDNS/Bonjour discovery is blocked. "
+ "Leave port empty for 443 on tailnet DNS (TLS) or 18789 otherwise.")
.font(.footnote)
.foregroundStyle(.secondary)
Text(
"Use this when mDNS/Bonjour discovery is blocked. "
+ "Leave port empty for 443 on tailnet DNS (TLS) or 18789 otherwise.")
.font(.footnote)
.foregroundStyle(.secondary)
Toggle("Discovery Debug Logs", isOn: self.$discoveryDebugLogsEnabled)
.onChange(of: self.discoveryDebugLogsEnabled) { _, newValue in
self.gatewayController.setDiscoveryDebugLoggingEnabled(newValue)
Toggle("Discovery Debug Logs", isOn: self.$discoveryDebugLogsEnabled)
.onChange(of: self.discoveryDebugLogsEnabled) { _, newValue in
self.gatewayController.setDiscoveryDebugLoggingEnabled(newValue)
}
NavigationLink("Discovery Logs") {
GatewayDiscoveryDebugLogView()
}
NavigationLink("Discovery Logs") {
GatewayDiscoveryDebugLogView()
Toggle("Debug Canvas Status", isOn: self.$canvasDebugStatusEnabled)
TextField("Gateway Auth Token", text: self.$gatewayToken)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
SecureField("Gateway Password", text: self.$gatewayPassword)
Button("Reset Onboarding", role: .destructive) {
self.showResetOnboardingAlert = true
}
VStack(alignment: .leading, spacing: 6) {
Text("Debug")
.font(.footnote.weight(.semibold))
.foregroundStyle(.secondary)
Text(self.gatewayDebugText())
.font(.system(size: 12, weight: .regular, design: .monospaced))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10, style: .continuous))
}
}
Toggle("Debug Canvas Status", isOn: self.$canvasDebugStatusEnabled)
TextField("Gateway Token", text: self.$gatewayToken)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
SecureField("Gateway Password", text: self.$gatewayPassword)
VStack(alignment: .leading, spacing: 6) {
Text("Debug")
.font(.footnote.weight(.semibold))
.foregroundStyle(.secondary)
Text(self.gatewayDebugText())
.font(.system(size: 12, weight: .regular, design: .monospaced))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10, style: .continuous))
}
}
} label: {
HStack(spacing: 10) {
Circle()
@@ -227,16 +248,22 @@ struct SettingsTab: View {
Section("Device") {
DisclosureGroup("Features") {
Toggle("Voice Wake", isOn: self.$voiceWakeEnabled)
.onChange(of: self.voiceWakeEnabled) { _, newValue in
self.featureToggle(
"Voice Wake",
isOn: self.$voiceWakeEnabled,
help: "Enables wake-word activation to start a hands-free session.") { newValue in
self.appModel.setVoiceWakeEnabled(newValue)
}
Toggle("Talk Mode", isOn: self.$talkEnabled)
.onChange(of: self.talkEnabled) { _, newValue in
self.featureToggle(
"Talk Mode",
isOn: self.$talkEnabled,
help: "Enables voice conversation mode with your connected OpenClaw agent.") { newValue in
self.appModel.setTalkEnabled(newValue)
}
// Keep this separate so users can hide the side bubble without disabling Talk Mode.
Toggle("Show Talk Button", isOn: self.$talkButtonEnabled)
self.featureToggle(
"Background Listening",
isOn: self.$talkBackgroundEnabled,
help: "Keeps listening while the app is backgrounded. Uses more battery.")
NavigationLink {
VoiceWakeWordsSettingsView()
@@ -246,29 +273,78 @@ struct SettingsTab: View {
value: VoiceWakePreferences.displayString(for: self.voiceWake.triggerWords))
}
Toggle("Allow Camera", isOn: self.$cameraEnabled)
Text("Allows the gateway to request photos or short video clips (foreground only).")
.font(.footnote)
.foregroundStyle(.secondary)
self.featureToggle(
"Allow Camera",
isOn: self.$cameraEnabled,
help: "Allows the gateway to request photos or short video clips while OpenClaw is foregrounded.")
HStack(spacing: 8) {
Text("Location Access")
Spacer()
Button {
self.activeFeatureHelp = FeatureHelp(
title: "Location Access",
message: "Controls location permissions for OpenClaw. Off disables location tools, While Using enables foreground location, and Always enables background location.")
} label: {
Image(systemName: "info.circle")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.accessibilityLabel("Location Access info")
}
Picker("Location Access", selection: self.$locationEnabledModeRaw) {
Text("Off").tag(OpenClawLocationMode.off.rawValue)
Text("While Using").tag(OpenClawLocationMode.whileUsing.rawValue)
Text("Always").tag(OpenClawLocationMode.always.rawValue)
}
.labelsHidden()
.pickerStyle(.segmented)
Toggle("Precise Location", isOn: self.$locationPreciseEnabled)
.disabled(self.locationMode == .off)
self.featureToggle(
"Prevent Sleep",
isOn: self.$preventSleep,
help: "Keeps the screen awake while OpenClaw is open.")
Text("Always requires system permission and may prompt to open Settings.")
.font(.footnote)
.foregroundStyle(.secondary)
DisclosureGroup("Advanced") {
self.featureToggle(
"Voice Directive Hint",
isOn: self.$talkVoiceDirectiveHintEnabled,
help: "Adds voice-switching instructions to Talk prompts. Disable to reduce prompt size.")
self.featureToggle(
"Show Talk Button",
isOn: self.$talkButtonEnabled,
help: "Shows the floating Talk button in the main interface.")
TextField("Default Share Instruction", text: self.$defaultShareInstruction, axis: .vertical)
.lineLimit(2 ... 6)
.textInputAutocapitalization(.sentences)
HStack(spacing: 8) {
Text("Default Share Instruction")
.font(.footnote)
.foregroundStyle(.secondary)
Spacer()
Button {
self.activeFeatureHelp = FeatureHelp(
title: "Default Share Instruction",
message: "Appends this instruction when sharing content into OpenClaw from iOS.")
} label: {
Image(systemName: "info.circle")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.accessibilityLabel("Default Share Instruction info")
}
Toggle("Prevent Sleep", isOn: self.$preventSleep)
Text("Keeps the screen awake while OpenClaw is open.")
.font(.footnote)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 8) {
Button {
Task { await self.appModel.runSharePipelineSelfTest() }
} label: {
Label("Run Share Self-Test", systemImage: "checkmark.seal")
}
Text(self.appModel.lastShareEventText)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
DisclosureGroup("Device Info") {
@@ -276,19 +352,11 @@ struct SettingsTab: View {
Text(self.instanceId)
.font(.footnote)
.foregroundStyle(.secondary)
LabeledContent("IP", value: self.localIPAddress ?? "")
.contextMenu {
if let ip = self.localIPAddress {
Button {
UIPasteboard.general.string = ip
} label: {
Label("Copy", systemImage: "doc.on.doc")
}
}
}
.lineLimit(1)
.truncationMode(.middle)
LabeledContent("Device", value: self.deviceFamily())
LabeledContent("Platform", value: self.platformString())
LabeledContent("Version", value: self.appVersion())
LabeledContent("Model", value: self.modelIdentifier())
LabeledContent("OpenClaw", value: self.openClawVersionString())
}
}
}
@@ -303,8 +371,22 @@ struct SettingsTab: View {
.accessibilityLabel("Close")
}
}
.alert("Reset Onboarding?", isPresented: self.$showResetOnboardingAlert) {
Button("Reset", role: .destructive) {
self.resetOnboarding()
}
Button("Cancel", role: .cancel) {}
} message: {
Text(
"This will disconnect, clear saved gateway connection + credentials, and reopen the onboarding wizard.")
}
.alert(item: self.$activeFeatureHelp) { help in
Alert(
title: Text(help.title),
message: Text(help.message),
dismissButton: .default(Text("OK")))
}
.onAppear {
self.localIPAddress = Self.primaryIPv4Address()
self.lastLocationModeRaw = self.locationEnabledModeRaw
self.syncManualPortText()
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -312,6 +394,8 @@ struct SettingsTab: View {
self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? ""
self.gatewayPassword = GatewaySettingsStore.loadGatewayPassword(instanceId: trimmedInstanceId) ?? ""
}
self.defaultShareInstruction = ShareToAgentSettings.loadDefaultInstruction()
self.appModel.refreshLastShareEventFromRelay()
// Keep setup front-and-center when disconnected; keep things compact once connected.
self.gatewayExpanded = !self.isGatewayConnected
self.selectedAgentPickerId = self.appModel.selectedAgentId ?? ""
@@ -331,17 +415,22 @@ struct SettingsTab: View {
GatewaySettingsStore.savePreferredGatewayStableID(trimmed)
}
.onChange(of: self.gatewayToken) { _, newValue in
guard !self.suppressCredentialPersist else { return }
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !instanceId.isEmpty else { return }
GatewaySettingsStore.saveGatewayToken(trimmed, instanceId: instanceId)
}
.onChange(of: self.gatewayPassword) { _, newValue in
guard !self.suppressCredentialPersist else { return }
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !instanceId.isEmpty else { return }
GatewaySettingsStore.saveGatewayPassword(trimmed, instanceId: instanceId)
}
.onChange(of: self.defaultShareInstruction) { _, newValue in
ShareToAgentSettings.saveDefaultInstruction(newValue)
}
.onChange(of: self.manualGatewayPort) { _, _ in
self.syncManualPortText()
}
@@ -421,10 +510,11 @@ struct SettingsTab: View {
ForEach(rows) { gateway in
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(gateway.name)
// Avoid localized-string formatting edge cases from Bonjour-advertised names.
Text(verbatim: gateway.name)
let detailLines = self.gatewayDetailLines(gateway)
ForEach(detailLines, id: \.self) { line in
Text(line)
Text(verbatim: line)
.font(.footnote)
.foregroundStyle(.secondary)
}
@@ -472,14 +562,6 @@ struct SettingsTab: View {
return "iOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
}
private var locationMode: OpenClawLocationMode {
OpenClawLocationMode(rawValue: self.locationEnabledModeRaw) ?? .off
}
private func appVersion() -> String {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
}
private func deviceFamily() -> String {
switch UIDevice.current.userInterfaceIdiom {
case .pad:
@@ -491,14 +573,36 @@ struct SettingsTab: View {
}
}
private func modelIdentifier() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let machine = withUnsafeBytes(of: &systemInfo.machine) { ptr in
String(bytes: ptr.prefix { $0 != 0 }, encoding: .utf8)
private func openClawVersionString() -> String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
let trimmedBuild = build.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedBuild.isEmpty || trimmedBuild == version {
return version
}
return "\(version) (\(trimmedBuild))"
}
private func featureToggle(
_ title: String,
isOn: Binding<Bool>,
help: String,
onChange: ((Bool) -> Void)? = nil
) -> some View {
HStack(spacing: 8) {
Toggle(title, isOn: isOn)
Button {
self.activeFeatureHelp = FeatureHelp(title: title, message: help)
} label: {
Image(systemName: "info.circle")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.accessibilityLabel("\(title) info")
}
.onChange(of: isOn.wrappedValue) { _, newValue in
onChange?(newValue)
}
let trimmed = machine?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? "unknown" : trimmed
}
private func connect(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async {
@@ -510,7 +614,10 @@ struct SettingsTab: View {
GatewaySettingsStore.saveLastDiscoveredGatewayStableID(gateway.stableID)
defer { self.connectingGatewayID = nil }
await self.gatewayController.connect(gateway)
let err = await self.gatewayController.connectWithDiagnostics(gateway)
if let err {
self.setupStatusText = err
}
}
private func connectLastKnown() async {
@@ -590,15 +697,6 @@ struct SettingsTab: View {
}
}
private struct SetupPayload: Codable {
var url: String?
var host: String?
var port: Int?
var tls: Bool?
var token: String?
var password: String?
}
private func applySetupCodeAndConnect() async {
self.setupStatusText = nil
guard self.applySetupCode() else { return }
@@ -626,7 +724,7 @@ struct SettingsTab: View {
return false
}
guard let payload = self.decodeSetupPayload(raw: raw) else {
guard let payload = GatewaySetupCode.decode(raw: raw) else {
self.setupStatusText = "Setup code not recognized."
return false
}
@@ -727,67 +825,14 @@ struct SettingsTab: View {
}
private static func probeTCP(host: String, port: Int, timeoutSeconds: Double) async -> Bool {
guard let nwPort = NWEndpoint.Port(rawValue: UInt16(port)) else { return false }
let endpointHost = NWEndpoint.Host(host)
let connection = NWConnection(host: endpointHost, port: nwPort, using: .tcp)
return await withCheckedContinuation { cont in
let queue = DispatchQueue(label: "gateway.preflight")
let finished = OSAllocatedUnfairLock(initialState: false)
let finish: @Sendable (Bool) -> Void = { ok in
let shouldResume = finished.withLock { flag -> Bool in
if flag { return false }
flag = true
return true
}
guard shouldResume else { return }
connection.cancel()
cont.resume(returning: ok)
}
connection.stateUpdateHandler = { state in
switch state {
case .ready:
finish(true)
case .failed, .cancelled:
finish(false)
default:
break
}
}
connection.start(queue: queue)
queue.asyncAfter(deadline: .now() + timeoutSeconds) {
finish(false)
}
}
await TCPProbe.probe(
host: host,
port: port,
timeoutSeconds: timeoutSeconds,
queueLabel: "gateway.preflight")
}
private func decodeSetupPayload(raw: String) -> SetupPayload? {
if let payload = decodeSetupPayloadFromJSON(raw) {
return payload
}
if let decoded = decodeBase64Payload(raw),
let payload = decodeSetupPayloadFromJSON(decoded)
{
return payload
}
return nil
}
private func decodeSetupPayloadFromJSON(_ json: String) -> SetupPayload? {
guard let data = json.data(using: .utf8) else { return nil }
return try? JSONDecoder().decode(SetupPayload.self, from: data)
}
private func decodeBase64Payload(_ raw: String) -> String? {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let normalized = trimmed
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let padding = normalized.count % 4
let padded = padding == 0 ? normalized : normalized + String(repeating: "=", count: 4 - padding)
guard let data = Data(base64Encoded: padded) else { return nil }
return String(data: data, encoding: .utf8)
}
// (GatewaySetupCode) decode raw setup codes.
private func connectManual() async {
let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -852,44 +897,6 @@ struct SettingsTab: View {
return nil
}
private static func primaryIPv4Address() -> String? {
var addrList: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&addrList) == 0, let first = addrList else { return nil }
defer { freeifaddrs(addrList) }
var fallback: String?
var en0: String?
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
let flags = Int32(ptr.pointee.ifa_flags)
let isUp = (flags & IFF_UP) != 0
let isLoopback = (flags & IFF_LOOPBACK) != 0
let name = String(cString: ptr.pointee.ifa_name)
let family = ptr.pointee.ifa_addr.pointee.sa_family
if !isUp || isLoopback || family != UInt8(AF_INET) { continue }
var addr = ptr.pointee.ifa_addr.pointee
var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let result = getnameinfo(
&addr,
socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
&buffer,
socklen_t(buffer.count),
nil,
0,
NI_NUMERICHOST)
guard result == 0 else { continue }
let len = buffer.prefix { $0 != 0 }
let bytes = len.map { UInt8(bitPattern: $0) }
guard let ip = String(bytes: bytes, encoding: .utf8) else { continue }
if name == "en0" { en0 = ip; break }
if fallback == nil { fallback = ip }
}
return en0 ?? fallback
}
private static func hasTailnetIPv4() -> Bool {
var addrList: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&addrList) == 0, let first = addrList else { return false }
@@ -949,6 +956,43 @@ struct SettingsTab: View {
SettingsNetworkingHelpers.httpURLString(host: host, port: port, fallback: fallback)
}
private func resetOnboarding() {
// Disconnect first so RootCanvas doesn't instantly mark onboarding complete again.
self.appModel.disconnectGateway()
self.connectingGatewayID = nil
self.setupStatusText = nil
self.setupCode = ""
self.gatewayAutoConnect = false
self.suppressCredentialPersist = true
defer { self.suppressCredentialPersist = false }
self.gatewayToken = ""
self.gatewayPassword = ""
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedInstanceId.isEmpty {
GatewaySettingsStore.deleteGatewayCredentials(instanceId: trimmedInstanceId)
}
// Reset onboarding state + clear saved gateway connection (the two things RootCanvas checks).
GatewaySettingsStore.clearLastGatewayConnection()
// RootCanvas also short-circuits onboarding when these are true.
self.onboardingComplete = false
self.hasConnectedOnce = false
// Clear manual override so it doesn't count as an existing gateway config.
self.manualGatewayEnabled = false
self.manualGatewayHost = ""
// Force re-present even without app restart.
self.onboardingRequestID += 1
// The onboarding wizard is presented from RootCanvas; dismiss Settings so it can show.
self.dismiss()
}
private func gatewayDetailLines(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> [String] {
var lines: [String] = []
if let lanHost = gateway.lanHost { lines.append("LAN: \(lanHost)") }

View File

@@ -0,0 +1,71 @@
import SwiftUI
enum StatusActivityBuilder {
@MainActor
static func build(
appModel: NodeAppModel,
voiceWakeEnabled: Bool,
cameraHUDText: String?,
cameraHUDKind: NodeAppModel.CameraHUDKind?
) -> StatusPill.Activity? {
// Keep the top pill consistent across tabs (camera + voice wake + pairing states).
if appModel.isBackgrounded {
return StatusPill.Activity(
title: "Foreground required",
systemImage: "exclamationmark.triangle.fill",
tint: .orange)
}
let gatewayStatus = appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
let gatewayLower = gatewayStatus.lowercased()
if gatewayLower.contains("repair") {
return StatusPill.Activity(title: "Repairing…", systemImage: "wrench.and.screwdriver", tint: .orange)
}
if gatewayLower.contains("approval") || gatewayLower.contains("pairing") {
return StatusPill.Activity(title: "Approval pending", systemImage: "person.crop.circle.badge.clock")
}
// Avoid duplicating the primary gateway status ("Connecting") in the activity slot.
if appModel.screenRecordActive {
return StatusPill.Activity(title: "Recording screen…", systemImage: "record.circle.fill", tint: .red)
}
if let cameraHUDText, !cameraHUDText.isEmpty, let cameraHUDKind {
let systemImage: String
let tint: Color?
switch cameraHUDKind {
case .photo:
systemImage = "camera.fill"
tint = nil
case .recording:
systemImage = "video.fill"
tint = .red
case .success:
systemImage = "checkmark.circle.fill"
tint = .green
case .error:
systemImage = "exclamationmark.triangle.fill"
tint = .red
}
return StatusPill.Activity(title: cameraHUDText, systemImage: systemImage, tint: tint)
}
if voiceWakeEnabled {
let voiceStatus = appModel.voiceWake.statusText
if voiceStatus.localizedCaseInsensitiveContains("microphone permission") {
return StatusPill.Activity(title: "Mic permission", systemImage: "mic.slash", tint: .orange)
}
if voiceStatus == "Paused" {
// Talk mode intentionally pauses voice wake to release the mic. Don't spam the HUD for that case.
if appModel.talkMode.isEnabled {
return nil
}
let suffix = appModel.isBackgrounded ? " (background)" : ""
return StatusPill.Activity(title: "Voice Wake paused\(suffix)", systemImage: "pause.circle.fill")
}
}
return nil
}
}

View File

@@ -2,6 +2,8 @@ import SwiftUI
struct StatusPill: View {
@Environment(\.scenePhase) private var scenePhase
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.colorSchemeContrast) private var contrast
enum GatewayState: Equatable {
case connected
@@ -49,11 +51,11 @@ struct StatusPill: View {
Circle()
.fill(self.gateway.color)
.frame(width: 9, height: 9)
.scaleEffect(self.gateway == .connecting ? (self.pulse ? 1.15 : 0.85) : 1.0)
.opacity(self.gateway == .connecting ? (self.pulse ? 1.0 : 0.6) : 1.0)
.scaleEffect(self.gateway == .connecting && !self.reduceMotion ? (self.pulse ? 1.15 : 0.85) : 1.0)
.opacity(self.gateway == .connecting && !self.reduceMotion ? (self.pulse ? 1.0 : 0.6) : 1.0)
Text(self.gateway.title)
.font(.system(size: 13, weight: .semibold))
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
}
@@ -64,17 +66,17 @@ struct StatusPill: View {
if let activity {
HStack(spacing: 6) {
Image(systemName: activity.systemImage)
.font(.system(size: 13, weight: .semibold))
.font(.subheadline.weight(.semibold))
.foregroundStyle(activity.tint ?? .primary)
Text(activity.title)
.font(.system(size: 13, weight: .semibold))
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
.lineLimit(1)
}
.transition(.opacity.combined(with: .move(edge: .top)))
} else {
Image(systemName: self.voiceWakeEnabled ? "mic.fill" : "mic.slash")
.font(.system(size: 13, weight: .semibold))
.font(.subheadline.weight(.semibold))
.foregroundStyle(self.voiceWakeEnabled ? .primary : .secondary)
.accessibilityLabel(self.voiceWakeEnabled ? "Voice Wake enabled" : "Voice Wake disabled")
.transition(.opacity.combined(with: .move(edge: .top)))
@@ -87,21 +89,28 @@ struct StatusPill: View {
.fill(.ultraThinMaterial)
.overlay {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(.white.opacity(self.brighten ? 0.24 : 0.18), lineWidth: 0.5)
.strokeBorder(
.white.opacity(self.contrast == .increased ? 0.5 : (self.brighten ? 0.24 : 0.18)),
lineWidth: self.contrast == .increased ? 1.0 : 0.5
)
}
.shadow(color: .black.opacity(0.25), radius: 12, y: 6)
}
}
.buttonStyle(.plain)
.accessibilityLabel("Status")
.accessibilityLabel("Connection Status")
.accessibilityValue(self.accessibilityValue)
.onAppear { self.updatePulse(for: self.gateway, scenePhase: self.scenePhase) }
.accessibilityHint("Double tap to open settings")
.onAppear { self.updatePulse(for: self.gateway, scenePhase: self.scenePhase, reduceMotion: self.reduceMotion) }
.onDisappear { self.pulse = false }
.onChange(of: self.gateway) { _, newValue in
self.updatePulse(for: newValue, scenePhase: self.scenePhase)
self.updatePulse(for: newValue, scenePhase: self.scenePhase, reduceMotion: self.reduceMotion)
}
.onChange(of: self.scenePhase) { _, newValue in
self.updatePulse(for: self.gateway, scenePhase: newValue)
self.updatePulse(for: self.gateway, scenePhase: newValue, reduceMotion: self.reduceMotion)
}
.onChange(of: self.reduceMotion) { _, newValue in
self.updatePulse(for: self.gateway, scenePhase: self.scenePhase, reduceMotion: newValue)
}
.animation(.easeInOut(duration: 0.18), value: self.activity?.title)
}
@@ -113,9 +122,9 @@ struct StatusPill: View {
return "\(self.gateway.title), Voice Wake \(self.voiceWakeEnabled ? "enabled" : "disabled")"
}
private func updatePulse(for gateway: GatewayState, scenePhase: ScenePhase) {
guard gateway == .connecting, scenePhase == .active else {
withAnimation(.easeOut(duration: 0.2)) { self.pulse = false }
private func updatePulse(for gateway: GatewayState, scenePhase: ScenePhase, reduceMotion: Bool) {
guard gateway == .connecting, scenePhase == .active, !reduceMotion else {
withAnimation(reduceMotion ? .none : .easeOut(duration: 0.2)) { self.pulse = false }
return
}

View File

@@ -1,17 +1,19 @@
import SwiftUI
struct VoiceWakeToast: View {
@Environment(\.colorSchemeContrast) private var contrast
var command: String
var brighten: Bool = false
var body: some View {
HStack(spacing: 10) {
Image(systemName: "mic.fill")
.font(.system(size: 14, weight: .semibold))
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
Text(self.command)
.font(.system(size: 14, weight: .semibold))
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
.lineLimit(1)
.truncationMode(.tail)
@@ -23,11 +25,14 @@ struct VoiceWakeToast: View {
.fill(.ultraThinMaterial)
.overlay {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(.white.opacity(self.brighten ? 0.24 : 0.18), lineWidth: 0.5)
.strokeBorder(
.white.opacity(self.contrast == .increased ? 0.5 : (self.brighten ? 0.24 : 0.18)),
lineWidth: self.contrast == .increased ? 1.0 : 0.5
)
}
.shadow(color: .black.opacity(0.25), radius: 12, y: 6)
}
.accessibilityLabel("Voice Wake")
.accessibilityValue(self.command)
.accessibilityLabel("Voice Wake triggered")
.accessibilityValue("Command: \(self.command)")
}
}

View File

@@ -16,6 +16,7 @@ import Speech
final class TalkModeManager: NSObject {
private typealias SpeechRequest = SFSpeechAudioBufferRecognitionRequest
private static let defaultModelIdFallback = "eleven_v3"
private static let redactedConfigSentinel = "__OPENCLAW_REDACTED__"
var isEnabled: Bool = false
var isListening: Bool = false
var isSpeaking: Bool = false
@@ -218,8 +219,12 @@ final class TalkModeManager: NSObject {
/// Suspends microphone usage without disabling Talk Mode.
/// Used when the app backgrounds (or when we need to temporarily release the mic).
func suspendForBackground() -> Bool {
func suspendForBackground(keepActive: Bool = false) -> Bool {
guard self.isEnabled else { return false }
if keepActive {
self.statusText = self.isListening ? "Listening" : self.statusText
return false
}
let wasActive = self.isListening || self.isSpeaking || self.isPushToTalkActive
self.isListening = false
@@ -246,7 +251,8 @@ final class TalkModeManager: NSObject {
return wasActive
}
func resumeAfterBackground(wasSuspended: Bool) async {
func resumeAfterBackground(wasSuspended: Bool, wasKeptActive: Bool = false) async {
if wasKeptActive { return }
guard wasSuspended else { return }
guard self.isEnabled else { return }
await self.start()
@@ -814,29 +820,24 @@ final class TalkModeManager: NSObject {
private func subscribeChatIfNeeded(sessionKey: String) async {
let key = sessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
guard !key.isEmpty else { return }
guard let gateway else { return }
guard !self.chatSubscribedSessionKeys.contains(key) else { return }
let payload = "{\"sessionKey\":\"\(key)\"}"
await gateway.sendEvent(event: "chat.subscribe", payloadJSON: payload)
// Operator clients receive chat events without node-style subscriptions.
self.chatSubscribedSessionKeys.insert(key)
self.logger.info("chat.subscribe ok sessionKey=\(key, privacy: .public)")
}
private func unsubscribeAllChats() async {
guard let gateway else { return }
let keys = self.chatSubscribedSessionKeys
self.chatSubscribedSessionKeys.removeAll()
for key in keys {
let payload = "{\"sessionKey\":\"\(key)\"}"
await gateway.sendEvent(event: "chat.unsubscribe", payloadJSON: payload)
}
}
private func buildPrompt(transcript: String) -> String {
let interrupted = self.lastInterruptedAtSeconds
self.lastInterruptedAtSeconds = nil
return TalkPromptBuilder.build(transcript: transcript, interruptedAtSeconds: interrupted)
let includeVoiceDirectiveHint = (UserDefaults.standard.object(forKey: "talk.voiceDirectiveHint.enabled") as? Bool) ?? true
return TalkPromptBuilder.build(
transcript: transcript,
interruptedAtSeconds: interrupted,
includeVoiceDirectiveHint: includeVoiceDirectiveHint)
}
private enum ChatCompletionState: CustomStringConvertible {
@@ -1114,6 +1115,7 @@ final class TalkModeManager: NSObject {
}
private func shouldInterrupt(with transcript: String) -> Bool {
guard self.shouldAllowSpeechInterruptForCurrentRoute() else { return false }
let trimmed = transcript.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.count >= 3 else { return false }
if let spoken = self.lastSpokenText?.lowercased(), spoken.contains(trimmed.lowercased()) {
@@ -1122,6 +1124,20 @@ final class TalkModeManager: NSObject {
return true
}
private func shouldAllowSpeechInterruptForCurrentRoute() -> Bool {
let route = AVAudioSession.sharedInstance().currentRoute
// Built-in speaker/receiver often feeds TTS back into STT, causing false interrupts.
// Allow barge-in for isolated outputs (headphones/Bluetooth/USB/CarPlay/AirPlay).
return !route.outputs.contains { output in
switch output.portType {
case .builtInSpeaker, .builtInReceiver:
return true
default:
return false
}
}
}
private func shouldUseIncrementalTTS() -> Bool {
true
}
@@ -1668,6 +1684,15 @@ extension TalkModeManager {
return value.allSatisfy { $0.isLetter || $0.isNumber || $0 == "-" || $0 == "_" }
}
private static func normalizedTalkApiKey(_ raw: String?) -> String? {
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
guard trimmed != Self.redactedConfigSentinel else { return nil }
// Config values may be env placeholders (for example `${ELEVENLABS_API_KEY}`).
if trimmed.hasPrefix("${"), trimmed.hasSuffix("}") { return nil }
return trimmed
}
func reloadConfig() async {
guard let gateway else { return }
do {
@@ -1699,7 +1724,15 @@ extension TalkModeManager {
}
self.defaultOutputFormat = (talk?["outputFormat"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines)
self.apiKey = (talk?["apiKey"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
let rawConfigApiKey = (talk?["apiKey"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
let configApiKey = Self.normalizedTalkApiKey(rawConfigApiKey)
let localApiKey = Self.normalizedTalkApiKey(GatewaySettingsStore.loadTalkElevenLabsApiKey())
if rawConfigApiKey == Self.redactedConfigSentinel {
self.apiKey = (localApiKey?.isEmpty == false) ? localApiKey : nil
GatewayDiagnostics.log("talk config apiKey redacted; using local override if present")
} else {
self.apiKey = (localApiKey?.isEmpty == false) ? localApiKey : configApiKey
}
if let interrupt = talk?["interruptOnSpeech"] as? Bool {
self.interruptOnSpeech = interrupt
}

View File

@@ -76,4 +76,52 @@ import Testing
timeoutSeconds: nil,
key: nil)))
}
@Test func parseGatewayLinkParsesCommonFields() {
let url = URL(
string: "openclaw://gateway?host=openclaw.local&port=18789&tls=1&token=abc&password=def")!
#expect(
DeepLinkParser.parse(url) == .gateway(
.init(host: "openclaw.local", port: 18789, tls: true, token: "abc", password: "def")))
}
@Test func parseGatewaySetupCodeParsesBase64UrlPayload() {
let payload = #"{"url":"wss://gateway.example.com:443","token":"tok","password":"pw"}"#
let encoded = Data(payload.utf8)
.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
let link = GatewayConnectDeepLink.fromSetupCode(encoded)
#expect(link == .init(
host: "gateway.example.com",
port: 443,
tls: true,
token: "tok",
password: "pw"))
}
@Test func parseGatewaySetupCodeRejectsInvalidInput() {
#expect(GatewayConnectDeepLink.fromSetupCode("not-a-valid-setup-code") == nil)
}
@Test func parseGatewaySetupCodeDefaultsTo443ForWssWithoutPort() {
let payload = #"{"url":"wss://gateway.example.com","token":"tok"}"#
let encoded = Data(payload.utf8)
.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
let link = GatewayConnectDeepLink.fromSetupCode(encoded)
#expect(link == .init(
host: "gateway.example.com",
port: 443,
tls: true,
token: "tok",
password: nil))
}
}

View File

@@ -76,4 +76,47 @@ private func withUserDefaults<T>(_ updates: [String: Any?], _ body: () throws ->
#expect(commands.contains(OpenClawLocationCommand.get.rawValue))
}
}
@Test @MainActor func currentCommandsExcludeDangerousSystemExecCommands() {
withUserDefaults([
"node.instanceId": "ios-test",
"camera.enabled": true,
"location.enabledMode": OpenClawLocationMode.whileUsing.rawValue,
]) {
let appModel = NodeAppModel()
let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false)
let commands = Set(controller._test_currentCommands())
// iOS should expose notify, but not host shell/exec-approval commands.
#expect(commands.contains(OpenClawSystemCommand.notify.rawValue))
#expect(!commands.contains(OpenClawSystemCommand.run.rawValue))
#expect(!commands.contains(OpenClawSystemCommand.which.rawValue))
#expect(!commands.contains(OpenClawSystemCommand.execApprovalsGet.rawValue))
#expect(!commands.contains(OpenClawSystemCommand.execApprovalsSet.rawValue))
}
}
@Test @MainActor func loadLastConnectionReadsSavedValues() {
withUserDefaults([:]) {
GatewaySettingsStore.saveLastGatewayConnectionManual(
host: "gateway.example.com",
port: 443,
useTLS: true,
stableID: "manual|gateway.example.com|443")
let loaded = GatewaySettingsStore.loadLastGatewayConnection()
#expect(loaded == .manual(host: "gateway.example.com", port: 443, useTLS: true, stableID: "manual|gateway.example.com|443"))
}
}
@Test @MainActor func loadLastConnectionReturnsNilForInvalidData() {
withUserDefaults([
"gateway.last.kind": "manual",
"gateway.last.host": "",
"gateway.last.port": 0,
"gateway.last.tls": false,
"gateway.last.stableID": "manual|invalid|0",
]) {
let loaded = GatewaySettingsStore.loadLastGatewayConnection()
#expect(loaded == nil)
}
}
}

View File

@@ -0,0 +1,33 @@
import Testing
@testable import OpenClaw
@Suite(.serialized) struct GatewayConnectionIssueTests {
@Test func detectsTokenMissing() {
let issue = GatewayConnectionIssue.detect(from: "unauthorized: gateway token missing")
#expect(issue == .tokenMissing)
#expect(issue.needsAuthToken)
}
@Test func detectsUnauthorized() {
let issue = GatewayConnectionIssue.detect(from: "Gateway error: unauthorized role")
#expect(issue == .unauthorized)
#expect(issue.needsAuthToken)
}
@Test func detectsPairingWithRequestId() {
let issue = GatewayConnectionIssue.detect(from: "pairing required (requestId: abc123)")
#expect(issue == .pairingRequired(requestId: "abc123"))
#expect(issue.needsPairing)
#expect(issue.requestId == "abc123")
}
@Test func detectsNetworkError() {
let issue = GatewayConnectionIssue.detect(from: "Gateway error: Connection refused")
#expect(issue == .network)
}
@Test func returnsNoneForBenignStatus() {
let issue = GatewayConnectionIssue.detect(from: "Connected")
#expect(issue == .none)
}
}

View File

@@ -17,8 +17,8 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.15</string>
<string>2026.2.18</string>
<key>CFBundleVersion</key>
<string>20260215</string>
<string>20260218</string>
</dict>
</plist>

View File

@@ -29,6 +29,39 @@ private func withUserDefaults<T>(_ updates: [String: Any?], _ body: () throws ->
return try body()
}
@MainActor
private final class MockWatchMessagingService: WatchMessagingServicing, @unchecked Sendable {
var currentStatus = WatchMessagingStatus(
supported: true,
paired: true,
appInstalled: true,
reachable: true,
activationState: "activated")
var nextSendResult = WatchNotificationSendResult(
deliveredImmediately: true,
queuedForDelivery: false,
transport: "sendMessage")
var sendError: Error?
var lastSent: (id: String, title: String, body: String, priority: OpenClawNotificationPriority?)?
func status() async -> WatchMessagingStatus {
self.currentStatus
}
func sendNotification(
id: String,
title: String,
body: String,
priority: OpenClawNotificationPriority?) async throws -> WatchNotificationSendResult
{
self.lastSent = (id: id, title: title, body: body, priority: priority)
if let sendError = self.sendError {
throw sendError
}
return self.nextSendResult
}
}
@Suite(.serialized) struct NodeAppModelInvokeTests {
@Test @MainActor func decodeParamsFailsWithoutJSON() {
#expect(throws: Error.self) {
@@ -156,6 +189,96 @@ private func withUserDefaults<T>(_ updates: [String: Any?], _ body: () throws ->
#expect(res.error?.code == .invalidRequest)
}
@Test @MainActor func handleInvokeWatchStatusReturnsServiceSnapshot() async throws {
let watchService = MockWatchMessagingService()
watchService.currentStatus = WatchMessagingStatus(
supported: true,
paired: true,
appInstalled: true,
reachable: false,
activationState: "inactive")
let appModel = NodeAppModel(watchMessagingService: watchService)
let req = BridgeInvokeRequest(id: "watch-status", command: OpenClawWatchCommand.status.rawValue)
let res = await appModel._test_handleInvoke(req)
#expect(res.ok == true)
let payloadData = try #require(res.payloadJSON?.data(using: .utf8))
let payload = try JSONDecoder().decode(OpenClawWatchStatusPayload.self, from: payloadData)
#expect(payload.supported == true)
#expect(payload.reachable == false)
#expect(payload.activationState == "inactive")
}
@Test @MainActor func handleInvokeWatchNotifyRoutesToWatchService() async throws {
let watchService = MockWatchMessagingService()
watchService.nextSendResult = WatchNotificationSendResult(
deliveredImmediately: false,
queuedForDelivery: true,
transport: "transferUserInfo")
let appModel = NodeAppModel(watchMessagingService: watchService)
let params = OpenClawWatchNotifyParams(
title: "OpenClaw",
body: "Meeting with Peter is at 4pm",
priority: .timeSensitive)
let paramsData = try JSONEncoder().encode(params)
let paramsJSON = String(decoding: paramsData, as: UTF8.self)
let req = BridgeInvokeRequest(
id: "watch-notify",
command: OpenClawWatchCommand.notify.rawValue,
paramsJSON: paramsJSON)
let res = await appModel._test_handleInvoke(req)
#expect(res.ok == true)
#expect(watchService.lastSent?.title == "OpenClaw")
#expect(watchService.lastSent?.body == "Meeting with Peter is at 4pm")
#expect(watchService.lastSent?.priority == .timeSensitive)
let payloadData = try #require(res.payloadJSON?.data(using: .utf8))
let payload = try JSONDecoder().decode(OpenClawWatchNotifyPayload.self, from: payloadData)
#expect(payload.deliveredImmediately == false)
#expect(payload.queuedForDelivery == true)
#expect(payload.transport == "transferUserInfo")
}
@Test @MainActor func handleInvokeWatchNotifyRejectsEmptyMessage() async throws {
let watchService = MockWatchMessagingService()
let appModel = NodeAppModel(watchMessagingService: watchService)
let params = OpenClawWatchNotifyParams(title: " ", body: "\n")
let paramsData = try JSONEncoder().encode(params)
let paramsJSON = String(decoding: paramsData, as: UTF8.self)
let req = BridgeInvokeRequest(
id: "watch-notify-empty",
command: OpenClawWatchCommand.notify.rawValue,
paramsJSON: paramsJSON)
let res = await appModel._test_handleInvoke(req)
#expect(res.ok == false)
#expect(res.error?.code == .invalidRequest)
#expect(watchService.lastSent == nil)
}
@Test @MainActor func handleInvokeWatchNotifyReturnsUnavailableOnDeliveryFailure() async throws {
let watchService = MockWatchMessagingService()
watchService.sendError = NSError(
domain: "watch",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "WATCH_UNAVAILABLE: no paired Apple Watch"])
let appModel = NodeAppModel(watchMessagingService: watchService)
let params = OpenClawWatchNotifyParams(title: "OpenClaw", body: "Delivery check")
let paramsData = try JSONEncoder().encode(params)
let paramsJSON = String(decoding: paramsData, as: UTF8.self)
let req = BridgeInvokeRequest(
id: "watch-notify-fail",
command: OpenClawWatchCommand.notify.rawValue,
paramsJSON: paramsJSON)
let res = await appModel._test_handleInvoke(req)
#expect(res.ok == false)
#expect(res.error?.code == .unavailable)
#expect(res.error?.message.contains("WATCH_UNAVAILABLE") == true)
}
@Test @MainActor func handleDeepLinkSetsErrorWhenNotConnected() async {
let appModel = NodeAppModel()
let url = URL(string: "openclaw://agent?message=hello")!

View File

@@ -0,0 +1,57 @@
import Foundation
import Testing
@testable import OpenClaw
@Suite(.serialized) struct OnboardingStateStoreTests {
@Test @MainActor func shouldPresentWhenFreshAndDisconnected() {
let testDefaults = self.makeDefaults()
let defaults = testDefaults.defaults
defer { self.reset(testDefaults) }
let appModel = NodeAppModel()
appModel.gatewayServerName = nil
#expect(OnboardingStateStore.shouldPresentOnLaunch(appModel: appModel, defaults: defaults))
}
@Test @MainActor func doesNotPresentWhenConnected() {
let testDefaults = self.makeDefaults()
let defaults = testDefaults.defaults
defer { self.reset(testDefaults) }
let appModel = NodeAppModel()
appModel.gatewayServerName = "gateway"
#expect(!OnboardingStateStore.shouldPresentOnLaunch(appModel: appModel, defaults: defaults))
}
@Test @MainActor func markCompletedPersistsMode() {
let testDefaults = self.makeDefaults()
let defaults = testDefaults.defaults
defer { self.reset(testDefaults) }
let appModel = NodeAppModel()
appModel.gatewayServerName = nil
OnboardingStateStore.markCompleted(mode: .remoteDomain, defaults: defaults)
#expect(OnboardingStateStore.lastMode(defaults: defaults) == .remoteDomain)
#expect(!OnboardingStateStore.shouldPresentOnLaunch(appModel: appModel, defaults: defaults))
OnboardingStateStore.markIncomplete(defaults: defaults)
#expect(OnboardingStateStore.shouldPresentOnLaunch(appModel: appModel, defaults: defaults))
}
private struct TestDefaults {
var suiteName: String
var defaults: UserDefaults
}
private func makeDefaults() -> TestDefaults {
let suiteName = "OnboardingStateStoreTests.\(UUID().uuidString)"
return TestDefaults(
suiteName: suiteName,
defaults: UserDefaults(suiteName: suiteName) ?? .standard)
}
private func reset(_ defaults: TestDefaults) {
defaults.defaults.removePersistentDomain(forName: defaults.suiteName)
}
}

View File

@@ -2,25 +2,38 @@ import Testing
import WebKit
@testable import OpenClaw
@MainActor
private func mountScreen(_ screen: ScreenController) throws -> (ScreenWebViewCoordinator, WKWebView) {
let coordinator = ScreenWebViewCoordinator(controller: screen)
_ = coordinator.makeContainerView()
let webView = try #require(coordinator.managedWebView)
return (coordinator, webView)
}
@Suite struct ScreenControllerTests {
@Test @MainActor func canvasModeConfiguresWebViewForTouch() {
@Test @MainActor func canvasModeConfiguresWebViewForTouch() throws {
let screen = ScreenController()
let (coordinator, webView) = try mountScreen(screen)
defer { coordinator.teardown() }
#expect(screen.webView.isOpaque == true)
#expect(screen.webView.backgroundColor == .black)
#expect(webView.isOpaque == true)
#expect(webView.backgroundColor == .black)
let scrollView = screen.webView.scrollView
let scrollView = webView.scrollView
#expect(scrollView.backgroundColor == .black)
#expect(scrollView.contentInsetAdjustmentBehavior == .never)
#expect(scrollView.isScrollEnabled == false)
#expect(scrollView.bounces == false)
}
@Test @MainActor func navigateEnablesScrollForWebPages() {
@Test @MainActor func navigateEnablesScrollForWebPages() throws {
let screen = ScreenController()
let (coordinator, webView) = try mountScreen(screen)
defer { coordinator.teardown() }
screen.navigate(to: "https://example.com")
let scrollView = screen.webView.scrollView
let scrollView = webView.scrollView
#expect(scrollView.isScrollEnabled == true)
#expect(scrollView.bounces == true)
}
@@ -34,6 +47,9 @@ import WebKit
@Test @MainActor func evalExecutesJavaScript() async throws {
let screen = ScreenController()
let (coordinator, _) = try mountScreen(screen)
defer { coordinator.teardown() }
let deadline = ContinuousClock().now.advanced(by: .seconds(3))
while true {

View File

@@ -0,0 +1,51 @@
import OpenClawKit
import Foundation
import Testing
@Suite struct ShareToAgentDeepLinkTests {
@Test func buildMessageIncludesSharedFields() {
let payload = SharedContentPayload(
title: "Article",
url: URL(string: "https://example.com/post")!,
text: "Read this")
let message = ShareToAgentDeepLink.buildMessage(
from: payload,
instruction: "Summarize and give next steps.")
#expect(message.contains("Shared from iOS."))
#expect(message.contains("Title: Article"))
#expect(message.contains("URL: https://example.com/post"))
#expect(message.contains("Text:\nRead this"))
#expect(message.contains("Summarize and give next steps."))
}
@Test func buildURLEncodesAgentRoute() {
let payload = SharedContentPayload(
title: "",
url: URL(string: "https://example.com")!,
text: nil)
let url = ShareToAgentDeepLink.buildURL(from: payload)
let parsed = url.flatMap { DeepLinkParser.parse($0) }
guard case let .agent(agent)? = parsed else {
Issue.record("Expected openclaw://agent deep link")
return
}
#expect(agent.thinking == "low")
#expect(agent.message.contains("https://example.com"))
}
@Test func buildURLReturnsNilWhenPayloadEmpty() {
let payload = SharedContentPayload(title: nil, url: nil, text: nil)
#expect(ShareToAgentDeepLink.buildURL(from: payload) == nil)
}
@Test func shareInstructionSettingsRoundTrip() {
let value = "Focus on booking constraints and alternatives."
ShareToAgentSettings.saveDefaultInstruction(value)
defer { ShareToAgentSettings.saveDefaultInstruction(nil) }
#expect(ShareToAgentSettings.loadDefaultInstruction() == value)
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>OpenClaw</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.18</string>
<key>CFBundleVersion</key>
<string>20260218</string>
<key>WKCompanionAppBundleIdentifier</key>
<string>$(OPENCLAW_APP_BUNDLE_ID)</string>
<key>WKWatchKitApp</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>OpenClaw</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.18</string>
<key>CFBundleVersion</key>
<string>20260218</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>WKAppBundleIdentifier</key>
<string>$(OPENCLAW_WATCH_APP_BUNDLE_ID)</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.watchkit</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,20 @@
import SwiftUI
@main
struct OpenClawWatchApp: App {
@State private var inboxStore = WatchInboxStore()
@State private var receiver: WatchConnectivityReceiver?
var body: some Scene {
WindowGroup {
WatchInboxView(store: self.inboxStore)
.task {
if self.receiver == nil {
let receiver = WatchConnectivityReceiver(store: self.inboxStore)
receiver.activate()
self.receiver = receiver
}
}
}
}
}

View File

@@ -0,0 +1,92 @@
import Foundation
import WatchConnectivity
final class WatchConnectivityReceiver: NSObject, @unchecked Sendable {
private let store: WatchInboxStore
private let session: WCSession?
init(store: WatchInboxStore) {
self.store = store
if WCSession.isSupported() {
self.session = WCSession.default
} else {
self.session = nil
}
super.init()
}
func activate() {
guard let session = self.session else { return }
session.delegate = self
session.activate()
}
private static func parseNotificationPayload(_ payload: [String: Any]) -> WatchNotifyMessage? {
guard let type = payload["type"] as? String, type == "watch.notify" else {
return nil
}
let title = (payload["title"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let body = (payload["body"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard title.isEmpty == false || body.isEmpty == false else {
return nil
}
let id = (payload["id"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchNotifyMessage(
id: id,
title: title,
body: body,
sentAtMs: sentAtMs)
}
}
extension WatchConnectivityReceiver: WCSessionDelegate {
func session(
_: WCSession,
activationDidCompleteWith _: WCSessionActivationState,
error _: (any Error)?)
{}
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")
}
}
func session(
_: WCSession,
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")
}
}
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")
}
}
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")
}
}
}

View File

@@ -0,0 +1,124 @@
import Foundation
import Observation
import UserNotifications
import WatchKit
struct WatchNotifyMessage: Sendable {
var id: String?
var title: String
var body: String
var sentAtMs: Int?
}
@MainActor @Observable final class WatchInboxStore {
private struct PersistedState: Codable {
var title: String
var body: String
var transport: String
var updatedAt: Date
var lastDeliveryKey: String?
}
private static let persistedStateKey = "watch.inbox.state.v1"
private let defaults: UserDefaults
var title = "OpenClaw"
var body = "Waiting for messages from your iPhone."
var transport = "none"
var updatedAt: Date?
private var lastDeliveryKey: String?
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
self.restorePersistedState()
Task {
await self.ensureNotificationAuthorization()
}
}
func consume(message: WatchNotifyMessage, transport: String) {
let messageID = message.id?
.trimmingCharacters(in: .whitespacesAndNewlines)
let deliveryKey = self.deliveryKey(
messageID: messageID,
title: message.title,
body: message.body,
sentAtMs: message.sentAtMs)
guard deliveryKey != self.lastDeliveryKey else { return }
let normalizedTitle = message.title.isEmpty ? "OpenClaw" : message.title
self.title = normalizedTitle
self.body = message.body
self.transport = transport
self.updatedAt = Date()
self.lastDeliveryKey = deliveryKey
self.persistState()
Task {
await self.postLocalNotification(
identifier: deliveryKey,
title: normalizedTitle,
body: message.body)
}
}
private func restorePersistedState() {
guard let data = self.defaults.data(forKey: Self.persistedStateKey),
let state = try? JSONDecoder().decode(PersistedState.self, from: data)
else {
return
}
self.title = state.title
self.body = state.body
self.transport = state.transport
self.updatedAt = state.updatedAt
self.lastDeliveryKey = state.lastDeliveryKey
}
private func persistState() {
guard let updatedAt = self.updatedAt else { return }
let state = PersistedState(
title: self.title,
body: self.body,
transport: self.transport,
updatedAt: updatedAt,
lastDeliveryKey: self.lastDeliveryKey)
guard let data = try? JSONEncoder().encode(state) else { return }
self.defaults.set(data, forKey: Self.persistedStateKey)
}
private func deliveryKey(messageID: String?, title: String, body: String, sentAtMs: Int?) -> String {
if let messageID, messageID.isEmpty == false {
return "id:\(messageID)"
}
return "content:\(title)|\(body)|\(sentAtMs ?? 0)"
}
private func ensureNotificationAuthorization() async {
let center = UNUserNotificationCenter.current()
let settings = await center.notificationSettings()
switch settings.authorizationStatus {
case .notDetermined:
_ = try? await center.requestAuthorization(options: [.alert, .sound])
default:
break
}
}
private func postLocalNotification(identifier: String, title: String, body: String) async {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
content.threadIdentifier = "openclaw-watch"
let request = UNNotificationRequest(
identifier: identifier,
content: content,
trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.2, repeats: false))
_ = try? await UNUserNotificationCenter.current().add(request)
WKInterfaceDevice.current().play(.notification)
}
}

View File

@@ -0,0 +1,27 @@
import SwiftUI
struct WatchInboxView: View {
@Bindable var store: WatchInboxStore
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
Text(store.title)
.font(.headline)
.lineLimit(2)
Text(store.body)
.font(.body)
.fixedSize(horizontal: false, vertical: true)
if let updatedAt = store.updatedAt {
Text("Updated \(updatedAt.formatted(date: .omitted, time: .shortened))")
.font(.footnote)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}
}

View File

@@ -66,7 +66,8 @@ platform :ios do
if team_id.nil? || team_id.strip.empty?
helper_path = File.expand_path("../../scripts/ios-team-id.sh", __dir__)
if File.exist?(helper_path)
team_id = sh("bash #{helper_path.shellescape}").strip
# Keep CI/local compatibility where teams are present in keychain but not Xcode account metadata.
team_id = sh("IOS_ALLOW_KEYCHAIN_TEAM_FALLBACK=1 bash #{helper_path.shellescape}").strip
end
end
UI.user_error!("Missing IOS_DEVELOPMENT_TEAM (Apple Team ID). Add it to fastlane/.env or export it in your shell.") if team_id.nil? || team_id.strip.empty?

View File

@@ -22,7 +22,7 @@ ASC_KEY_PATH=/absolute/path/to/AuthKey_XXXXXXXXXX.p8
IOS_DEVELOPMENT_TEAM=YOUR_TEAM_ID
```
Tip: run `scripts/ios-team-id.sh` from the repo root to print a Team ID to paste into `.env`. Fastlane falls back to this helper if `IOS_DEVELOPMENT_TEAM` is missing.
Tip: run `scripts/ios-team-id.sh` from the repo root to print a Team ID to paste into `.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.
Run:

View File

@@ -29,9 +29,15 @@ targets:
OpenClaw:
type: application
platform: iOS
configFiles:
Debug: Signing.xcconfig
Release: Signing.xcconfig
sources:
- path: Sources
dependencies:
- target: OpenClawShareExtension
embed: true
- target: OpenClawWatchApp
- package: OpenClawKit
- package: OpenClawKit
product: OpenClawChatUI
@@ -69,10 +75,11 @@ targets:
settings:
base:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_STYLE: Manual
DEVELOPMENT_TEAM: Y5PE65HELJ
PRODUCT_BUNDLE_IDENTIFIER: ai.openclaw.ios
PROVISIONING_PROFILE_SPECIFIER: "ai.openclaw.ios Development"
CODE_SIGN_ENTITLEMENTS: Sources/OpenClaw.entitlements
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_APP_BUNDLE_ID)"
PROVISIONING_PROFILE_SPECIFIER: "$(OPENCLAW_APP_PROFILE)"
SWIFT_VERSION: "6.0"
SWIFT_STRICT_CONCURRENCY: complete
ENABLE_APPINTENTS_METADATA: NO
@@ -81,13 +88,18 @@ targets:
properties:
CFBundleDisplayName: OpenClaw
CFBundleIconName: AppIcon
CFBundleShortVersionString: "2026.2.15"
CFBundleVersion: "20260215"
CFBundleURLTypes:
- CFBundleURLName: ai.openclaw.ios
CFBundleURLSchemes:
- openclaw
CFBundleShortVersionString: "2026.2.18"
CFBundleVersion: "20260218"
UILaunchScreen: {}
UIApplicationSceneManifest:
UIApplicationSupportsMultipleScenes: false
UIBackgroundModes:
- audio
- remote-notification
NSLocalNetworkUsageDescription: OpenClaw discovers and connects to your OpenClaw gateway on the local network.
NSAppTransportSecurity:
NSAllowsArbitraryLoadsInWebContent: true
@@ -109,6 +121,90 @@ targets:
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
OpenClawShareExtension:
type: app-extension
platform: iOS
configFiles:
Debug: Signing.xcconfig
Release: Signing.xcconfig
sources:
- path: ShareExtension
dependencies:
- package: OpenClawKit
settings:
base:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_SHARE_BUNDLE_ID)"
PROVISIONING_PROFILE_SPECIFIER: "$(OPENCLAW_SHARE_PROFILE)"
SWIFT_VERSION: "6.0"
SWIFT_STRICT_CONCURRENCY: complete
info:
path: ShareExtension/Info.plist
properties:
CFBundleDisplayName: OpenClaw Share
CFBundleShortVersionString: "2026.2.18"
CFBundleVersion: "20260218"
NSExtension:
NSExtensionPointIdentifier: com.apple.share-services
NSExtensionPrincipalClass: "$(PRODUCT_MODULE_NAME).ShareViewController"
NSExtensionAttributes:
NSExtensionActivationRule:
NSExtensionActivationSupportsText: true
NSExtensionActivationSupportsWebURLWithMaxCount: 1
NSExtensionActivationSupportsImageWithMaxCount: 10
NSExtensionActivationSupportsMovieWithMaxCount: 1
OpenClawWatchApp:
type: application.watchapp2
platform: watchOS
deploymentTarget: "11.0"
sources:
- path: WatchApp
dependencies:
- target: OpenClawWatchExtension
configFiles:
Debug: Config/Signing.xcconfig
Release: Config/Signing.xcconfig
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_WATCH_APP_BUNDLE_ID)"
info:
path: WatchApp/Info.plist
properties:
CFBundleDisplayName: OpenClaw
CFBundleShortVersionString: "2026.2.18"
CFBundleVersion: "20260218"
WKCompanionAppBundleIdentifier: "$(OPENCLAW_APP_BUNDLE_ID)"
WKWatchKitApp: true
OpenClawWatchExtension:
type: watchkit2-extension
platform: watchOS
deploymentTarget: "11.0"
sources:
- path: WatchExtension/Sources
dependencies:
- sdk: WatchConnectivity.framework
- sdk: UserNotifications.framework
configFiles:
Debug: Config/Signing.xcconfig
Release: Config/Signing.xcconfig
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_WATCH_EXTENSION_BUNDLE_ID)"
info:
path: WatchExtension/Info.plist
properties:
CFBundleDisplayName: OpenClaw
CFBundleShortVersionString: "2026.2.18"
CFBundleVersion: "20260218"
NSExtension:
NSExtensionAttributes:
WKAppBundleIdentifier: "$(OPENCLAW_WATCH_APP_BUNDLE_ID)"
NSExtensionPointIdentifier: com.apple.watchkit
OpenClawTests:
type: bundle.unit-test
platform: iOS
@@ -130,5 +226,5 @@ targets:
path: Tests/Info.plist
properties:
CFBundleDisplayName: OpenClawTests
CFBundleShortVersionString: "2026.2.15"
CFBundleVersion: "20260215"
CFBundleShortVersionString: "2026.2.18"
CFBundleVersion: "20260218"

View File

@@ -1,6 +1,5 @@
import Foundation
import OpenClawKit
import OpenClawProtocol
// Prefer the OpenClawKit wrapper to keep gateway request payloads consistent.
typealias AnyCodable = OpenClawKit.AnyCodable
@@ -42,40 +41,3 @@ extension AnyCodable {
}
}
}
extension OpenClawProtocol.AnyCodable {
var stringValue: String? {
self.value as? String
}
var boolValue: Bool? {
self.value as? Bool
}
var intValue: Int? {
self.value as? Int
}
var doubleValue: Double? {
self.value as? Double
}
var dictionaryValue: [String: OpenClawProtocol.AnyCodable]? {
self.value as? [String: OpenClawProtocol.AnyCodable]
}
var arrayValue: [OpenClawProtocol.AnyCodable]? {
self.value as? [OpenClawProtocol.AnyCodable]
}
var foundationValue: Any {
switch self.value {
case let dict as [String: OpenClawProtocol.AnyCodable]:
dict.mapValues { $0.foundationValue }
case let array as [OpenClawProtocol.AnyCodable]:
array.map(\.foundationValue)
default:
self.value
}
}
}

View File

@@ -106,14 +106,16 @@ actor CameraCaptureService {
}
withExtendedLifetime(delegate) {}
let maxPayloadBytes = 5 * 1024 * 1024
// Base64 inflates payloads by ~4/3; cap encoded bytes so the payload stays under 5MB (API limit).
let maxEncodedBytes = (maxPayloadBytes / 4) * 3
let res = try JPEGTranscoder.transcodeToJPEG(
imageData: rawData,
maxWidthPx: maxWidth,
quality: quality,
maxBytes: maxEncodedBytes)
let res: (data: Data, widthPx: Int, heightPx: Int)
do {
res = try PhotoCapture.transcodeJPEGForGateway(
rawData: rawData,
maxWidthPx: maxWidth,
quality: quality)
} catch {
throw CameraError.captureFailed(error.localizedDescription)
}
return (data: res.data, size: CGSize(width: res.widthPx, height: res.heightPx))
}
@@ -355,8 +357,8 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat
func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?)
{
error: Error?
) {
guard !self.didResume, let cont else { return }
self.didResume = true
self.cont = nil
@@ -378,8 +380,8 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat
func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings,
error: Error?)
{
error: Error?
) {
guard let error else { return }
guard !self.didResume, let cont else { return }
self.didResume = true

View File

@@ -1,17 +1,13 @@
import CoreServices
import Foundation
final class CanvasFileWatcher: @unchecked Sendable {
private let url: URL
private let queue: DispatchQueue
private var stream: FSEventStreamRef?
private var pending = false
private let onChange: () -> Void
private let watcher: CoalescingFSEventsWatcher
init(url: URL, onChange: @escaping () -> Void) {
self.url = url
self.queue = DispatchQueue(label: "ai.openclaw.canvaswatcher")
self.onChange = onChange
self.watcher = CoalescingFSEventsWatcher(
paths: [url.path],
queueLabel: "ai.openclaw.canvaswatcher",
onChange: onChange)
}
deinit {
@@ -19,76 +15,10 @@ final class CanvasFileWatcher: @unchecked Sendable {
}
func start() {
guard self.stream == nil else { return }
let retainedSelf = Unmanaged.passRetained(self)
var context = FSEventStreamContext(
version: 0,
info: retainedSelf.toOpaque(),
retain: nil,
release: { pointer in
guard let pointer else { return }
Unmanaged<CanvasFileWatcher>.fromOpaque(pointer).release()
},
copyDescription: nil)
let paths = [self.url.path] as CFArray
let flags = FSEventStreamCreateFlags(
kFSEventStreamCreateFlagFileEvents |
kFSEventStreamCreateFlagUseCFTypes |
kFSEventStreamCreateFlagNoDefer)
guard let stream = FSEventStreamCreate(
kCFAllocatorDefault,
Self.callback,
&context,
paths,
FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
0.05,
flags)
else {
retainedSelf.release()
return
}
self.stream = stream
FSEventStreamSetDispatchQueue(stream, self.queue)
if FSEventStreamStart(stream) == false {
self.stream = nil
FSEventStreamSetDispatchQueue(stream, nil)
FSEventStreamInvalidate(stream)
FSEventStreamRelease(stream)
}
self.watcher.start()
}
func stop() {
guard let stream = self.stream else { return }
self.stream = nil
FSEventStreamStop(stream)
FSEventStreamSetDispatchQueue(stream, nil)
FSEventStreamInvalidate(stream)
FSEventStreamRelease(stream)
}
}
extension CanvasFileWatcher {
private static let callback: FSEventStreamCallback = { _, info, numEvents, _, eventFlags, _ in
guard let info else { return }
let watcher = Unmanaged<CanvasFileWatcher>.fromOpaque(info).takeUnretainedValue()
watcher.handleEvents(numEvents: numEvents, eventFlags: eventFlags)
}
private func handleEvents(numEvents: Int, eventFlags: UnsafePointer<FSEventStreamEventFlags>?) {
guard numEvents > 0 else { return }
guard eventFlags != nil else { return }
// Coalesce rapid changes (common during builds/atomic saves).
if self.pending { return }
self.pending = true
self.queue.asyncAfter(deadline: .now() + 0.12) { [weak self] in
guard let self else { return }
self.pending = false
self.onChange()
}
self.watcher.stop()
}
}

View File

@@ -0,0 +1,111 @@
import CoreServices
import Foundation
final class CoalescingFSEventsWatcher: @unchecked Sendable {
private let queue: DispatchQueue
private var stream: FSEventStreamRef?
private var pending = false
private let paths: [String]
private let shouldNotify: (Int, UnsafeMutableRawPointer?) -> Bool
private let onChange: () -> Void
private let coalesceDelay: TimeInterval
init(
paths: [String],
queueLabel: String,
coalesceDelay: TimeInterval = 0.12,
shouldNotify: @escaping (Int, UnsafeMutableRawPointer?) -> Bool = { _, _ in true },
onChange: @escaping () -> Void
) {
self.paths = paths
self.queue = DispatchQueue(label: queueLabel)
self.coalesceDelay = coalesceDelay
self.shouldNotify = shouldNotify
self.onChange = onChange
}
deinit {
self.stop()
}
func start() {
guard self.stream == nil else { return }
let retainedSelf = Unmanaged.passRetained(self)
var context = FSEventStreamContext(
version: 0,
info: retainedSelf.toOpaque(),
retain: nil,
release: { pointer in
guard let pointer else { return }
Unmanaged<CoalescingFSEventsWatcher>.fromOpaque(pointer).release()
},
copyDescription: nil)
let paths = self.paths as CFArray
let flags = FSEventStreamCreateFlags(
kFSEventStreamCreateFlagFileEvents |
kFSEventStreamCreateFlagUseCFTypes |
kFSEventStreamCreateFlagNoDefer)
guard let stream = FSEventStreamCreate(
kCFAllocatorDefault,
Self.callback,
&context,
paths,
FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
0.05,
flags)
else {
retainedSelf.release()
return
}
self.stream = stream
FSEventStreamSetDispatchQueue(stream, self.queue)
if FSEventStreamStart(stream) == false {
self.stream = nil
FSEventStreamSetDispatchQueue(stream, nil)
FSEventStreamInvalidate(stream)
FSEventStreamRelease(stream)
}
}
func stop() {
guard let stream = self.stream else { return }
self.stream = nil
FSEventStreamStop(stream)
FSEventStreamSetDispatchQueue(stream, nil)
FSEventStreamInvalidate(stream)
FSEventStreamRelease(stream)
}
}
extension CoalescingFSEventsWatcher {
private static let callback: FSEventStreamCallback = { _, info, numEvents, eventPaths, eventFlags, _ in
guard let info else { return }
let watcher = Unmanaged<CoalescingFSEventsWatcher>.fromOpaque(info).takeUnretainedValue()
watcher.handleEvents(numEvents: numEvents, eventPaths: eventPaths, eventFlags: eventFlags)
}
private func handleEvents(
numEvents: Int,
eventPaths: UnsafeMutableRawPointer?,
eventFlags: UnsafePointer<FSEventStreamEventFlags>?
) {
guard numEvents > 0 else { return }
guard eventFlags != nil else { return }
guard self.shouldNotify(numEvents, eventPaths) else { return }
// Coalesce rapid changes (common during builds/atomic saves).
if self.pending { return }
self.pending = true
self.queue.asyncAfter(deadline: .now() + self.coalesceDelay) { [weak self] in
guard let self else { return }
self.pending = false
self.onChange()
}
}
}

View File

@@ -1,23 +1,34 @@
import CoreServices
import Foundation
final class ConfigFileWatcher: @unchecked Sendable {
private let url: URL
private let queue: DispatchQueue
private var stream: FSEventStreamRef?
private var pending = false
private let onChange: () -> Void
private let watchedDir: URL
private let targetPath: String
private let targetName: String
private let watcher: CoalescingFSEventsWatcher
init(url: URL, onChange: @escaping () -> Void) {
self.url = url
self.queue = DispatchQueue(label: "ai.openclaw.configwatcher")
self.onChange = onChange
self.watchedDir = url.deletingLastPathComponent()
self.targetPath = url.path
self.targetName = url.lastPathComponent
let watchedDirPath = self.watchedDir.path
let targetPath = self.targetPath
let targetName = self.targetName
self.watcher = CoalescingFSEventsWatcher(
paths: [watchedDirPath],
queueLabel: "ai.openclaw.configwatcher",
shouldNotify: { _, eventPaths in
guard let eventPaths else { return true }
let paths = unsafeBitCast(eventPaths, to: NSArray.self)
for case let path as String in paths {
if path == targetPath { return true }
if path.hasSuffix("/\(targetName)") { return true }
if path == watchedDirPath { return true }
}
return false
},
onChange: onChange)
}
deinit {
@@ -25,94 +36,10 @@ final class ConfigFileWatcher: @unchecked Sendable {
}
func start() {
guard self.stream == nil else { return }
let retainedSelf = Unmanaged.passRetained(self)
var context = FSEventStreamContext(
version: 0,
info: retainedSelf.toOpaque(),
retain: nil,
release: { pointer in
guard let pointer else { return }
Unmanaged<ConfigFileWatcher>.fromOpaque(pointer).release()
},
copyDescription: nil)
let paths = [self.watchedDir.path] as CFArray
let flags = FSEventStreamCreateFlags(
kFSEventStreamCreateFlagFileEvents |
kFSEventStreamCreateFlagUseCFTypes |
kFSEventStreamCreateFlagNoDefer)
guard let stream = FSEventStreamCreate(
kCFAllocatorDefault,
Self.callback,
&context,
paths,
FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
0.05,
flags)
else {
retainedSelf.release()
return
}
self.stream = stream
FSEventStreamSetDispatchQueue(stream, self.queue)
if FSEventStreamStart(stream) == false {
self.stream = nil
FSEventStreamSetDispatchQueue(stream, nil)
FSEventStreamInvalidate(stream)
FSEventStreamRelease(stream)
}
self.watcher.start()
}
func stop() {
guard let stream = self.stream else { return }
self.stream = nil
FSEventStreamStop(stream)
FSEventStreamSetDispatchQueue(stream, nil)
FSEventStreamInvalidate(stream)
FSEventStreamRelease(stream)
}
}
extension ConfigFileWatcher {
private static let callback: FSEventStreamCallback = { _, info, numEvents, eventPaths, eventFlags, _ in
guard let info else { return }
let watcher = Unmanaged<ConfigFileWatcher>.fromOpaque(info).takeUnretainedValue()
watcher.handleEvents(
numEvents: numEvents,
eventPaths: eventPaths,
eventFlags: eventFlags)
}
private func handleEvents(
numEvents: Int,
eventPaths: UnsafeMutableRawPointer?,
eventFlags: UnsafePointer<FSEventStreamEventFlags>?)
{
guard numEvents > 0 else { return }
guard eventFlags != nil else { return }
guard self.matchesTarget(eventPaths: eventPaths) else { return }
if self.pending { return }
self.pending = true
self.queue.asyncAfter(deadline: .now() + 0.12) { [weak self] in
guard let self else { return }
self.pending = false
self.onChange()
}
}
private func matchesTarget(eventPaths: UnsafeMutableRawPointer?) -> Bool {
guard let eventPaths else { return true }
let paths = unsafeBitCast(eventPaths, to: NSArray.self)
for case let path as String in paths {
if path == self.targetPath { return true }
if path.hasSuffix("/\(self.targetName)") { return true }
if path == self.watchedDir.path { return true }
}
return false
self.watcher.stop()
}
}

View File

@@ -21,6 +21,7 @@ enum CronWakeMode: String, CaseIterable, Identifiable, Codable {
enum CronDeliveryMode: String, CaseIterable, Identifiable, Codable {
case none
case announce
case webhook
var id: String {
self.rawValue

View File

@@ -67,6 +67,8 @@ final class DeepLinkHandler {
switch route {
case let .agent(link):
await self.handleAgent(link: link, originalURL: url)
case .gateway:
break
}
}

View File

@@ -22,16 +22,6 @@ final class DevicePairingApprovalPrompter {
private var alertHostWindow: NSWindow?
private var resolvedByRequestId: Set<String> = []
private final class AlertHostWindow: NSWindow {
override var canBecomeKey: Bool {
true
}
override var canBecomeMain: Bool {
true
}
}
private struct PairingList: Codable {
let pending: [PendingRequest]
let paired: [PairedDevice]?
@@ -238,35 +228,11 @@ final class DevicePairingApprovalPrompter {
}
private func endActiveAlert() {
guard let alert = self.activeAlert else { return }
if let parent = alert.window.sheetParent {
parent.endSheet(alert.window, returnCode: .abort)
}
self.activeAlert = nil
self.activeRequestId = nil
PairingAlertSupport.endActiveAlert(activeAlert: &self.activeAlert, activeRequestId: &self.activeRequestId)
}
private func requireAlertHostWindow() -> NSWindow {
if let alertHostWindow {
return alertHostWindow
}
let window = AlertHostWindow(
contentRect: NSRect(x: 0, y: 0, width: 520, height: 1),
styleMask: [.borderless],
backing: .buffered,
defer: false)
window.title = ""
window.isReleasedWhenClosed = false
window.level = .floating
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
window.isOpaque = false
window.hasShadow = false
window.backgroundColor = .clear
window.ignoresMouseEvents = true
self.alertHostWindow = window
return window
PairingAlertSupport.requireAlertHostWindow(alertHostWindow: &self.alertHostWindow)
}
private func handle(push: GatewayPush) {

View File

@@ -158,7 +158,7 @@ final class InstancesStore {
private func localFallbackInstance(reason: String) -> InstanceInfo {
let host = Host.current().localizedName ?? "this-mac"
let ip = Self.primaryIPv4Address()
let ip = SystemPresenceInfo.primaryIPv4Address()
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
let platform = "macos \(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)"
@@ -172,58 +172,13 @@ final class InstancesStore {
platform: platform,
deviceFamily: "Mac",
modelIdentifier: InstanceIdentity.modelIdentifier,
lastInputSeconds: Self.lastInputSeconds(),
lastInputSeconds: SystemPresenceInfo.lastInputSeconds(),
mode: "local",
reason: reason,
text: text,
ts: ts)
}
private static func lastInputSeconds() -> Int? {
let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null
let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent)
if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil }
return Int(seconds.rounded())
}
private static func primaryIPv4Address() -> String? {
var addrList: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&addrList) == 0, let first = addrList else { return nil }
defer { freeifaddrs(addrList) }
var fallback: String?
var en0: String?
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
let flags = Int32(ptr.pointee.ifa_flags)
let isUp = (flags & IFF_UP) != 0
let isLoopback = (flags & IFF_LOOPBACK) != 0
let name = String(cString: ptr.pointee.ifa_name)
let family = ptr.pointee.ifa_addr.pointee.sa_family
if !isUp || isLoopback || family != UInt8(AF_INET) { continue }
var addr = ptr.pointee.ifa_addr.pointee
var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let result = getnameinfo(
&addr,
socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
&buffer,
socklen_t(buffer.count),
nil,
0,
NI_NUMERICHOST)
guard result == 0 else { continue }
let len = buffer.prefix { $0 != 0 }
let bytes = len.map { UInt8(bitPattern: $0) }
guard let ip = String(bytes: bytes, encoding: .utf8) else { continue }
if name == "en0" { en0 = ip; break }
if fallback == nil { fallback = ip }
}
return en0 ?? fallback
}
// MARK: - Helpers
/// Keep the last raw payload for logging.

View File

@@ -38,16 +38,6 @@ final class NodePairingApprovalPrompter {
private var remoteResolutionsByRequestId: [String: PairingResolution] = [:]
private var autoApproveAttempts: Set<String> = []
private final class AlertHostWindow: NSWindow {
override var canBecomeKey: Bool {
true
}
override var canBecomeMain: Bool {
true
}
}
private struct PairingList: Codable {
let pending: [PendingRequest]
let paired: [PairedNode]?
@@ -242,35 +232,11 @@ final class NodePairingApprovalPrompter {
}
private func endActiveAlert() {
guard let alert = self.activeAlert else { return }
if let parent = alert.window.sheetParent {
parent.endSheet(alert.window, returnCode: .abort)
}
self.activeAlert = nil
self.activeRequestId = nil
PairingAlertSupport.endActiveAlert(activeAlert: &self.activeAlert, activeRequestId: &self.activeRequestId)
}
private func requireAlertHostWindow() -> NSWindow {
if let alertHostWindow {
return alertHostWindow
}
let window = AlertHostWindow(
contentRect: NSRect(x: 0, y: 0, width: 520, height: 1),
styleMask: [.borderless],
backing: .buffered,
defer: false)
window.title = ""
window.isReleasedWhenClosed = false
window.level = .floating
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
window.isOpaque = false
window.hasShadow = false
window.backgroundColor = .clear
window.ignoresMouseEvents = true
self.alertHostWindow = window
return window
PairingAlertSupport.requireAlertHostWindow(alertHostWindow: &self.alertHostWindow)
}
private func handle(push: GatewayPush) {

View File

@@ -0,0 +1,46 @@
import AppKit
final class PairingAlertHostWindow: NSWindow {
override var canBecomeKey: Bool {
true
}
override var canBecomeMain: Bool {
true
}
}
@MainActor
enum PairingAlertSupport {
static func endActiveAlert(activeAlert: inout NSAlert?, activeRequestId: inout String?) {
guard let alert = activeAlert else { return }
if let parent = alert.window.sheetParent {
parent.endSheet(alert.window, returnCode: .abort)
}
activeAlert = nil
activeRequestId = nil
}
static func requireAlertHostWindow(alertHostWindow: inout NSWindow?) -> NSWindow {
if let alertHostWindow {
return alertHostWindow
}
let window = PairingAlertHostWindow(
contentRect: NSRect(x: 0, y: 0, width: 520, height: 1),
styleMask: [.borderless],
backing: .buffered,
defer: false)
window.title = ""
window.isReleasedWhenClosed = false
window.level = .floating
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
window.isOpaque = false
window.hasShadow = false
window.backgroundColor = .clear
window.ignoresMouseEvents = true
alertHostWindow = window
return window
}
}

View File

@@ -1,5 +1,4 @@
import Cocoa
import Darwin
import Foundation
import OSLog
@@ -33,10 +32,10 @@ final class PresenceReporter {
private func push(reason: String) async {
let mode = await MainActor.run { AppStateStore.shared.connectionMode.rawValue }
let host = InstanceIdentity.displayName
let ip = Self.primaryIPv4Address() ?? "ip-unknown"
let ip = SystemPresenceInfo.primaryIPv4Address() ?? "ip-unknown"
let version = Self.appVersionString()
let platform = Self.platformString()
let lastInput = Self.lastInputSeconds()
let lastInput = SystemPresenceInfo.lastInputSeconds()
let text = Self.composePresenceSummary(mode: mode, reason: reason)
var params: [String: AnyHashable] = [
"instanceId": AnyHashable(self.instanceId),
@@ -64,9 +63,9 @@ final class PresenceReporter {
private static func composePresenceSummary(mode: String, reason: String) -> String {
let host = InstanceIdentity.displayName
let ip = Self.primaryIPv4Address() ?? "ip-unknown"
let ip = SystemPresenceInfo.primaryIPv4Address() ?? "ip-unknown"
let version = Self.appVersionString()
let lastInput = Self.lastInputSeconds()
let lastInput = SystemPresenceInfo.lastInputSeconds()
let lastLabel = lastInput.map { "last input \($0)s ago" } ?? "last input unknown"
return "Node: \(host) (\(ip)) · app \(version) · \(lastLabel) · mode \(mode) · reason \(reason)"
}
@@ -87,50 +86,7 @@ final class PresenceReporter {
return "macos \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
}
private static func lastInputSeconds() -> Int? {
let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null
let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent)
if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil }
return Int(seconds.rounded())
}
private static func primaryIPv4Address() -> String? {
var addrList: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&addrList) == 0, let first = addrList else { return nil }
defer { freeifaddrs(addrList) }
var fallback: String?
var en0: String?
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
let flags = Int32(ptr.pointee.ifa_flags)
let isUp = (flags & IFF_UP) != 0
let isLoopback = (flags & IFF_LOOPBACK) != 0
let name = String(cString: ptr.pointee.ifa_name)
let family = ptr.pointee.ifa_addr.pointee.sa_family
if !isUp || isLoopback || family != UInt8(AF_INET) { continue }
var addr = ptr.pointee.ifa_addr.pointee
var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let result = getnameinfo(
&addr,
socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
&buffer,
socklen_t(buffer.count),
nil,
0,
NI_NUMERICHOST)
guard result == 0 else { continue }
let len = buffer.prefix { $0 != 0 }
let bytes = len.map { UInt8(bitPattern: $0) }
guard let ip = String(bytes: bytes, encoding: .utf8) else { continue }
if name == "en0" { en0 = ip; break }
if fallback == nil { fallback = ip }
}
return en0 ?? fallback
}
// (SystemPresenceInfo) last input + primary IPv4.
}
#if DEBUG
@@ -148,11 +104,11 @@ extension PresenceReporter {
}
static func _testLastInputSeconds() -> Int? {
self.lastInputSeconds()
SystemPresenceInfo.lastInputSeconds()
}
static func _testPrimaryIPv4Address() -> String? {
self.primaryIPv4Address()
SystemPresenceInfo.primaryIPv4Address()
}
}
#endif

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.15</string>
<string>2026.2.18</string>
<key>CFBundleVersion</key>
<string>202602150</string>
<string>202602180</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -0,0 +1,16 @@
import CoreGraphics
import Foundation
import OpenClawKit
enum SystemPresenceInfo {
static func lastInputSeconds() -> Int? {
let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null
let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent)
if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil }
return Int(seconds.rounded())
}
static func primaryIPv4Address() -> String? {
NetworkInterfaces.primaryIPv4Address()
}
}

View File

@@ -1,10 +1,8 @@
import AppKit
import Foundation
import Observation
import OpenClawDiscovery
import os
#if canImport(Darwin)
import Darwin
#endif
/// Manages Tailscale integration and status checking.
@Observable
@@ -140,7 +138,7 @@ final class TailscaleService {
self.logger.info("Tailscale API not responding; app likely not running")
}
if self.tailscaleIP == nil, let fallback = Self.detectTailnetIPv4() {
if self.tailscaleIP == nil, let fallback = TailscaleNetwork.detectTailnetIPv4() {
self.tailscaleIP = fallback
if !self.isRunning {
self.isRunning = true
@@ -178,49 +176,7 @@ final class TailscaleService {
}
}
private nonisolated static func isTailnetIPv4(_ address: String) -> Bool {
let parts = address.split(separator: ".")
guard parts.count == 4 else { return false }
let octets = parts.compactMap { Int($0) }
guard octets.count == 4 else { return false }
let a = octets[0]
let b = octets[1]
return a == 100 && b >= 64 && b <= 127
}
private nonisolated static func detectTailnetIPv4() -> String? {
var addrList: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&addrList) == 0, let first = addrList else { return nil }
defer { freeifaddrs(addrList) }
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
let flags = Int32(ptr.pointee.ifa_flags)
let isUp = (flags & IFF_UP) != 0
let isLoopback = (flags & IFF_LOOPBACK) != 0
let family = ptr.pointee.ifa_addr.pointee.sa_family
if !isUp || isLoopback || family != UInt8(AF_INET) { continue }
var addr = ptr.pointee.ifa_addr.pointee
var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let result = getnameinfo(
&addr,
socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
&buffer,
socklen_t(buffer.count),
nil,
0,
NI_NUMERICHOST)
guard result == 0 else { continue }
let len = buffer.prefix { $0 != 0 }
let bytes = len.map { UInt8(bitPattern: $0) }
guard let ip = String(bytes: bytes, encoding: .utf8) else { continue }
if Self.isTailnetIPv4(ip) { return ip }
}
return nil
}
nonisolated static func fallbackTailnetIPv4() -> String? {
self.detectTailnetIPv4()
TailscaleNetwork.detectTailnetIPv4()
}
}

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