Compare commits

..

589 Commits

Author SHA1 Message Date
openclaw-clownfish[bot]
38c4c36c35 fix(sandbox): bound docker exec during sandbox init 2026-06-16 14:07:01 +00:00
joshavant
6b3e23aba7 chore(ios): capture watch now face screenshot 2026-06-16 15:59:02 +02:00
joshavant
4542d3914c chore(ios): remove review information metadata 2026-06-16 15:59:02 +02:00
joshavant
d24a589f1c chore(ios): rename signing repository reference 2026-06-16 15:59:02 +02:00
joshavant
0af07bb378 chore(ios): migrate release signing to fastlane match 2026-06-16 15:59:02 +02:00
Vincent Koc
df8ceb5267 fix(update): avoid per-Node npm prefixes during self-update (#93650)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Soham Patankar <102520430+yaanfpv@users.noreply.github.com>
2026-06-16 21:54:08 +08:00
Vincent Koc
32d1ccd71c test(feishu): reset lifecycle monitor state 2026-06-16 15:44:08 +02:00
Vincent Koc
998445ea20 fix(qwen): place DashScope image prompts in user content (#93649)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-16 21:42:59 +08:00
Vincent Koc
5cebe96667 fix(doctor): archive superseded plugin install index conflicts (#93648)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-16 21:42:46 +08:00
Colin Johnson
c45c87acca feat(ios): add watch action surface (#93387)
* feat(ios): add watch action surface

* fix: harden watch action surface

* fix: clean watch codegen lint

* fix(ios): scope watch chat commands to gateway

---------

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-06-16 15:22:16 +02:00
liuhao1024
a4d013a9f3 fix(feishu): filter temporary card-action-c-* IDs from reply target to prevent Invalid open_message_id errors (fixes #56818) (#93618)
Merged via squash.

Prepared head SHA: cca7b25384
Co-authored-by: liuhao1024 <11816344+liuhao1024@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 21:18:05 +08:00
Vincent Koc
9de6a99c8f test(telegram): stabilize spool timeout recovery assertions 2026-06-16 15:11:45 +02:00
zhang-guiping
fa0116b0a0 fix #93044: control-ui webchat double-renders agent replies when dmScope=main (#93298)
Merged via squash.

Prepared head SHA: b7ab9ba0fa
Co-authored-by: zhangguiping-xydt <275915537+zhangguiping-xydt@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 21:10:00 +08:00
Vincent Koc
49572863d3 fix(telegram): preserve live spool claim owners 2026-06-16 15:03:11 +02:00
Dave Morin
c48b36a255 Keep key-free web search providers opt-in (#93616)
Merged via squash.

Prepared head SHA: 5de02da038
Co-authored-by: davemorin <78139+davemorin@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 21:02:07 +08:00
Nik
11b6c01198 fix(telegram): recover lone active spooled handler on timeout (#84158) (#93615)
Merged via squash.

Prepared head SHA: 2da361a345
Co-authored-by: 0xghost42 <151941421+0xghost42@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 20:36:11 +08:00
liuhao1024
aebf0bbd2d fix(gateway): compute sessions.usage aggregate totals from all sessions, not just the limited page (fixes #76496) (#93612)
Merged via squash.

Prepared head SHA: 349b8cd066
Co-authored-by: liuhao1024 <11816344+liuhao1024@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 20:33:10 +08:00
liuhao1024
e567986355 perf(tasks): memoize reconcileInspectableTasks for same-tick calls (fixes #73531) (#93607)
Merged via squash.

Prepared head SHA: f010d09db9
Co-authored-by: liuhao1024 <11816344+liuhao1024@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 20:31:51 +08:00
liuhao1024
c4940a4ff9 fix(ui): clear stale Talk error when session transitions to non-error state (fixes #88176) (#93606)
Merged via squash.

Prepared head SHA: 468b0bd01d
Co-authored-by: liuhao1024 <11816344+liuhao1024@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 20:30:35 +08:00
Vincent Koc
ed16f8fcf0 fix(ci): require billable Anthropic release key 2026-06-16 20:02:03 +08:00
iloveleon19
65805e519d fix(mattermost): keep bare @mention with empty body instead of dropping it (#93242)
Merged via squash.

Prepared head SHA: 7f6d21677b
Co-authored-by: iloveleon19 <37945260+iloveleon19@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 19:40:25 +08:00
Vincent Koc
fd61b1b6ee fix(ci): update test helper expectations 2026-06-16 13:35:59 +02:00
Alix-007
9dbc423aa4 fix(whatsapp): bound stalled read-receipt socket operations (#93303)
Merged via squash.

Prepared head SHA: 27752b17a0
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 19:21:12 +08:00
Vincent Koc
35ffbf93b9 fix(ci): prefer Anthropic OAuth in live validation 2026-06-16 19:12:02 +08:00
BitmapAsset
1881a0188b fix(plugins): resolve provider policy surface for plugin-owned CLI backends (#93261)
Merged via squash.

Prepared head SHA: 27ebfc7c4d
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 19:05:12 +08:00
Eldar Shlomi
33bf9874bf fix(telegram): hydrate group reply-chain media into model context [AI-assisted] (#93575)
Merged via squash.

Prepared head SHA: f108f6eae4
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 18:58:31 +08:00
Moeed Ahmed
ecd0d17243 Fix tokenjuice bash results without details (#93269)
Merged via squash.

Prepared head SHA: 33de08d9a1
Co-authored-by: moeedahmed <5780040+moeedahmed@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 18:53:52 +08:00
Eldar Shlomi
e4f448c74f fix(feishu): suppress log noise for bot_p2p_chat_entered_v1 event [AI-assisted] (#93574)
Merged via squash.

Prepared head SHA: 598af62d5b
Co-authored-by: eldar702 <72104254+eldar702@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 18:47:31 +08:00
Daniel Morandini
6163425a2d Fix SSH sandbox remote directory args (#93367)
Merged via squash.

Prepared head SHA: 02e3d7eb9f
Co-authored-by: dmorn <10097445+dmorn@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 18:45:18 +08:00
Yzx
b0a2b65d81 fix(cron): emit isolated model usage diagnostics (#93398)
Merged via squash.

Prepared head SHA: 727c189774
Co-authored-by: 849261680 <53250620+849261680@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 18:42:01 +08:00
Eldar Shlomi
fcdbef732c fix(acp): keep bridge sessions out of stale ACP classification [AI-assisted] (#93573)
Merged via squash.

Prepared head SHA: f26140a4b4
Co-authored-by: eldar702 <72104254+eldar702@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 18:39:53 +08:00
Alix-007
a6dd20ae9d fix(skills): preserve ClawHub origin provenance on readback (#93314)
Merged via squash.

Prepared head SHA: 8bd8df1549
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 18:33:40 +08:00
Vincent Koc
fa33f5bbb8 fix(plugin-sdk): refresh API baseline hash 2026-06-16 12:32:39 +02:00
zhanxingxin1998
a117064697 fix(read): route text decoding through shared Windows codepage fallba… (#93555)
Merged via squash.

Prepared head SHA: ab97624258
Co-authored-by: zhanxingxin1998 <293803800+zhanxingxin1998@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 18:31:39 +08:00
Vincent Koc
4292f0fe7f refactor(agents): narrow unused helper exports 2026-06-16 18:22:02 +08:00
Vincent Koc
623761e5c5 fix(ci): pin Rocky install smoke platform 2026-06-16 12:21:39 +02:00
Vincent Koc
8415887646 refactor(agents): remove unused helper exports 2026-06-16 18:19:56 +08:00
zhang-guiping
f1b6a60583 Clarify plugin channel config additional-property errors (#93274)
Merged via squash.

Prepared head SHA: dce4a38bbc
Co-authored-by: zhangguiping-xydt <275915537+zhangguiping-xydt@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 18:13:49 +08:00
Vincent Koc
b500a488e4 fix(ci): support Anthropic OAuth release validation 2026-06-16 18:10:49 +08:00
Vincent Koc
645fe838ff fix(ci): align checkout guard timeout 2026-06-16 12:10:23 +02:00
Vincent Koc
4fee348764 refactor(agents): remove unused credential comparator 2026-06-16 18:08:32 +08:00
Vincent Koc
0471275270 refactor(agents): remove unused process registry export 2026-06-16 18:07:18 +08:00
Vincent Koc
203bddcdb7 refactor(agents): drop unused truncation export 2026-06-16 18:06:05 +08:00
Vincent Koc
c6d549c5a7 test(ci): update checkout timeout guard 2026-06-16 18:04:58 +08:00
Alix-007
176572cb35 fix(skills): clear orphaned idempotency pointer on corrupt-metadata re-begin (#93509)
Merged via squash.

Prepared head SHA: 0dd53d2dac
Co-authored-by: Alix-007 <267018309+Alix-007@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 18:04:47 +08:00
Vincent Koc
55c047e77e fix(ci): relax checkout fetch timeout 2026-06-16 17:51:06 +08:00
Vincent Koc
58a8142a33 chore(deadcode): drop duplicate unused-file allowlist entry 2026-06-16 17:35:41 +08:00
Vincent Koc
2e7caba557 refactor(plugins): reuse dependency status core 2026-06-16 17:35:41 +08:00
Vincent Koc
0fd0e7cb92 fix(ci): align main CI fixtures 2026-06-16 17:33:39 +08:00
xiayu
a89e6e05ef fix(cli): summarize cleanup dry-run by label (#93565)
Merged via squash.

Prepared head SHA: b0dd1d0833
Co-authored-by: AgentArcLab <19233945+AgentArcLab@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 17:22:54 +08:00
Vincent Koc
08ff253e5f fix(ci): repair test helper type checks 2026-06-16 11:12:21 +02:00
Vincent Koc
033bb86133 fix(scripts): update plugin SDK surface budget 2026-06-16 11:01:41 +02:00
Harjoth Khara
790e00a303 fix(agents): honor embedded run default model (#93439)
Merged via squash.

Prepared head SHA: 171165c3eb
Co-authored-by: harjothkhara <48686985+harjothkhara@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 16:55:58 +08:00
Matt Gunnin
2cfcb3c932 AGT-80 AGT-81 Fix Discord ingress ack ordering (#93407)
Merged via squash.

Prepared head SHA: 55718a24fd
Co-authored-by: mgunnin <321368+mgunnin@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 16:54:46 +08:00
ZengWen-DT
9ed9d389e0 fix(feishu): dedupe redelivered text by stable retry identity (#93449)
Merged via squash.

Prepared head SHA: 230266c0ba
Co-authored-by: ZengWen-DT <290981215+ZengWen-DT@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 16:53:29 +08:00
Bé Mi Agent
d697ecf172 fix: avoid parent group allowlist false positive (#93434)
Merged via squash.

Prepared head SHA: da2ce686af
Co-authored-by: kingrubic <116256161+kingrubic@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 16:51:04 +08:00
Vincent Koc
6d22b8eb24 fix(ci): repair main type and lint checks 2026-06-16 10:43:21 +02:00
WhatsSkiLL
c14793d35a fix(gateway): project failed agent turns in chat history (#89483)
Merged via squash.

Prepared head SHA: d7b510a90d
Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 16:35:21 +08:00
Vincent Koc
f90ec6d7be fix(tests): avoid runtime discovery in routed reply checks 2026-06-16 10:21:46 +02:00
snowzlm
1a002c2d9d fix(agents): preserve prompt-released session state (#93194)
Preserve concurrent prompt-time transcript updates across stale session managers, side appends, transcript navigation, nested owned writes, and doctor repair.

Fixes #93193.

Thanks @snowzlm for the report and original fix.

Co-authored-by: snowzlm <snowzlm@noreply.codeberg.org>
2026-06-16 10:21:01 +02:00
Sebastien Tardif
a55f625b09 fix(discord): resolve guildId from session channel for search actions (#88796)
Merged via squash.

Prepared head SHA: 6b0c282908
Co-authored-by: SebTardif <1413412+SebTardif@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 16:15:12 +08:00
sunlit-deng
21d3a70826 fix(plugins): load externally-installed channel plugins at gateway startup (#93470)
Merged via squash.

Prepared head SHA: 934dfd3c57
Co-authored-by: sunlit-deng <253064511+sunlit-deng@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 16:13:42 +08:00
Vincent Koc
9b49387ad8 fix(tui): refresh after external session reset (#93562)
* fix(tui): refresh after external session reset

* fix(clownfish): address review for ghcrawl-157015-autonomous-smoke (1)

Co-authored-by: Jason <31175216+wsyjh8@users.noreply.github.com>

* fix(clownfish): address review for ghcrawl-157015-autonomous-smoke (1)

Co-authored-by: Jason <31175216+wsyjh8@users.noreply.github.com>

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Jason <31175216+wsyjh8@users.noreply.github.com>
2026-06-16 16:10:37 +08:00
Goutam Adwant
7e9b9421bd fix(codex): log app-server compaction completion (#93463)
Merged via squash.

Prepared head SHA: 49f4423dd7
Co-authored-by: goutamadwant <8672451+goutamadwant@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 16:05:29 +08:00
jason
ff5e73539a fix(agents): drop partialJson streaming artifacts from session history repair (#93469)
Merged via squash.

Prepared head SHA: 86fe9d5a43
Co-authored-by: drvoss <3031622+drvoss@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 16:03:34 +08:00
zhang-guiping
b5648b1d5e fix(cli): skip compile cache on early Node 24.x to avoid startup deadlock (#89799)
Merged via squash.

Prepared head SHA: 46341b26fb
Co-authored-by: zhangguiping-xydt <275915537+zhangguiping-xydt@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 15:59:34 +08:00
weiqinl
0ab4cd7c52 fix(bedrock): strip inference profile prefix from model ID in embedding adapter (#93452)
Merged via squash.

Prepared head SHA: aaaee01ebe
Co-authored-by: LiuwqGit <7065327+LiuwqGit@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 15:57:40 +08:00
ooiuuii
042ebb4f75 fix(cli): accept --log-level after subcommands (#93455)
Merged via squash.

Prepared head SHA: b6d3aa5719
Co-authored-by: ooiuuii <169449607+ooiuuii@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 15:48:19 +08:00
Vincent Koc
1ae0eacf4b fix(scripts): avoid downgrade release upgrade baselines 2026-06-16 09:35:39 +02:00
Vincent Koc
c06b7959ec fix(proxy): preserve in-memory capture databases 2026-06-16 09:22:48 +02:00
Vincent Koc
aeb5b794c9 fix(sqlite): tolerate unsupported private modes 2026-06-16 09:22:48 +02:00
Vincent Koc
e83926747c fix(proxy): keep capture storage private 2026-06-16 09:22:48 +02:00
Vincent Koc
e51c0c8cea fix(sqlite): include rollback journals in security paths 2026-06-16 09:22:48 +02:00
Vincent Koc
67c55ccce8 fix(e2e): avoid Linux snapshot apt races 2026-06-16 15:18:44 +08:00
Yuan
385d1ada91 fix(feishu): drop self-authored receive events (#90572) 2026-06-16 15:11:51 +08:00
Yuval Dinodia
7fc124dcf1 fix(reply): preserve pending thread evidence when reconciling partial send results (#93291)
* fix(reply): preserve pending thread evidence when reconciling partial send results

extractMessagingToolSendResult re-derived threadId/threadImplicit/threadSuppressed
straight from the provider result. Mattermost is the only production provider that
implements extractToolSendResult, and for an implicitly threaded send it reports only
{ to }, so the reconciler overwrote the correct pending thread evidence with undefined.
That defeated same-thread reply suppression in reply-payloads dedupe and delivered the
agent's final reply twice in the thread, on both the native and Codex harnesses.

A partial provider result now keeps the pending thread evidence it does not speak to: a
provider-reported threadId still wins (and clears the implicit flag), but an absent one
no longer erases the pending threadId/threadImplicit/threadSuppressed.

Regression introduced by c67dc59b02 (#90943).

* test(reply): use a core-local stub provider instead of the bundled Mattermost import

The reconcile-thread regression test deep-imported extensions/mattermost from a
core test, which trips the core/extension package boundary (boundary-invariants
"keeps core tests off bundled extension deep imports", extension-test-boundary,
and check-tsgo-core-boundary pulling extensions/mattermost transitively).

Replace it with a core-local channel test plugin that reproduces the same
contract: an implicit-threading extractToolSend, a partial extractToolSendResult
that reports only { to, threadId? }, and no targetsMatchForReplySuppression
matcher. The test now exercises the generic reconciler contract with no
extension dependency. It still fails on pristine main and passes with the fix.

* fix(reply): reconcile thread evidence atomically

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 15:10:00 +08:00
Vincent Koc
63825369a2 fix(auto-reply): allow attachment sends in legacy group automatic replies (#93529)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-16 14:48:16 +08:00
Vincent Koc
f2522a535d fix(e2e): wait for Ubuntu package maintenance 2026-06-16 14:45:24 +08:00
Vincent Koc
20964d3e3b fix(gateway): tolerate transient pre-hello clean closes (#93528)
* fix(gateway): tolerate transient pre-hello clean closes

Co-authored-by: RayRuan <43744645+ruanrrn@users.noreply.github.com>

* fix(clownfish): address review for ghcrawl-156871-autonomous-smoke (1)

Co-authored-by: RayRuan <43744645+ruanrrn@users.noreply.github.com>

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: RayRuan <43744645+ruanrrn@users.noreply.github.com>
2026-06-16 14:43:36 +08:00
JC
75141775db fix(openai): request SSE for native ChatGPT streams (#90487)
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 14:43:02 +08:00
Vincent Koc
999d44340f fix(cron): preserve model overrides for text payloads (#93527)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Andi Liao <31417269+liaoandi@users.noreply.github.com>
2026-06-16 14:41:22 +08:00
Stellar鱼
ca1a53aca4 feat(cron): add compact list responses (#93395)
Merged via squash.

Prepared head SHA: 4965e7e630
Co-authored-by: yu-xin-c <175149126+yu-xin-c@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 14:40:00 +08:00
Goutam Adwant
46c12b6c54 fix(mattermost): preserve Codex progress preview (#93476)
Merged via squash.

Prepared head SHA: f1dd666451
Co-authored-by: goutamadwant <8672451+goutamadwant@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 14:39:19 +08:00
Stellar鱼
bbfea21a18 fix(security): audit open dm tool exposure (#92883)
* fix(security): audit open dm tool exposure

* fix(security): align open DM audit precedence

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 14:38:39 +08:00
Jason (Json)
1e0062b44a feat: add Codex hosted web search (#93446)
Adds Codex as a selectable hosted web-search provider, routes native Codex search safely across model overrides, and isolates bounded hosted-search workers from configured tools.\n\nVerification: focused post-merge regression suite passed 202/202 tests on exact head 23824af49a.
2026-06-16 00:38:16 -06:00
Vincent Koc
23589d9e7c agents: notify chat exec empty-success completions (#93525)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: wenkang.xie <58462870+wenkang-xie@users.noreply.github.com>
2026-06-16 14:37:59 +08:00
Vincent Koc
15166e81ca fix(skills): trust verified ClawHub source provenance (#93506)
Merged via squash.

Prepared head SHA: a9ec22fa47
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 14:36:42 +08:00
zengLingbiao
4c9e7f6c61 fix(nodes): return screen snapshots as media (#93499)
Merged via squash.

Prepared head SHA: 6a69c5cdcc
Co-authored-by: zenglingbiao <290951975+zenglingbiao@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 14:35:58 +08:00
Martin Kessler
840cfd69cd fix(telegram): bind bot mentions to assistant identity (#93088)
* fix(telegram): bind bot mentions to assistant identity

* fix(telegram): satisfy context payload mention typing

* refactor(telegram): carry mention facts as one context object

* test(telegram): use neutral bot handle fixture

* fix(ci): terminate heartbeat command groups

* fix(ci): preserve heartbeat shell functions

* fix(telegram): project effective mention facts

* fix(telegram): keep mention identity portable

* test(telegram): align mention facts mock

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 14:35:14 +08:00
zhaoqj2016
b037280ea9 fix(ui): preserve CJK IME composition (#93498)
Merged via squash.

Prepared head SHA: c84ef0bdf5
Co-authored-by: Zhaoqj2016 <21196165+Zhaoqj2016@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 14:34:40 +08:00
Andy Ye
6aff1e8f9e fix(memory): report skipped QMD embedding probe (#93473)
Merged via squash.

Prepared head SHA: eea1ba563b
Co-authored-by: TurboTheTurtle <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 14:34:01 +08:00
Bhargav Chinta
e06f5f2edc fix(cron): preserve aborted isolated-run failure (#93471)
Merged via squash.

Prepared head SHA: dfbba9aa40
Co-authored-by: BhargavSatya <24696554+BhargavSatya@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 14:33:25 +08:00
Harjoth Khara
d7cebdc215 fix(gateway): rotate already-stale generated transcript filename on /reset (#93496)
Merged via squash.

Prepared head SHA: 6ae356c34a
Co-authored-by: harjothkhara <48686985+harjothkhara@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-16 14:32:38 +08:00
Vincent Koc
53da30dd98 fix(e2e): repair omitted Codex platform package 2026-06-16 14:31:23 +08:00
Vincent Koc
e46bcb834f fix(feishu): send post mentions as native at elements (#93522)
* fix(feishu): use native at elements for blue @mention rendering

* fix(clownfish): address review for ghcrawl-156842-autonomous-smoke (1)

Co-authored-by: gavin-ali <223589024+gavin-ali@users.noreply.github.com>

Co-authored-by: Yizuki_Ame <104178195+YizukiAme@users.noreply.github.com>

Co-authored-by: Pnant <73925474+Panniantong@users.noreply.github.com>

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Pnant <73925474+Panniantong@users.noreply.github.com>
2026-06-16 14:30:40 +08:00
Vincent Koc
d2439d2f7d fix(onboard): skip Homebrew prompt on unsupported platforms (#93521)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-16 14:30:27 +08:00
Vincent Koc
52280351bb fix(workspace): store setup state outside workspace dot-dir (#93520)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Lai Quang Huy <64073540+1qh@users.noreply.github.com>
2026-06-16 14:30:01 +08:00
openclaw-clownfish[bot]
e1d3f12d7f fix(memory): use per-keyword FTS search in hybrid mode #39484 (#73976)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-16 14:25:02 +08:00
Vincent Koc
ce6fd93279 fix(skills): quote skill-creator template description (#93517)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: parubets <1392109+parubets@users.noreply.github.com>
2026-06-16 14:24:19 +08:00
Vincent Koc
1884cedd35 fix(skills): refresh persisted snapshots after restart (#93513)
* fix(skills): refresh persisted snapshots after restart

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>

Co-authored-by: Oleksandr Zakotyanskyi <28755978+fif911@users.noreply.github.com>

Co-authored-by: Stephan Kadauke <10904538+skadauke@users.noreply.github.com>

* fix(clownfish): address review for ghcrawl-156600-autonomous-smoke (1)

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>

Co-authored-by: Oleksandr Zakotyanskyi <28755978+fif911@users.noreply.github.com>

Co-authored-by: Stephan Kadauke <10904538+skadauke@users.noreply.github.com>

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Stephan Kadauke <10904538+skadauke@users.noreply.github.com>
2026-06-16 14:20:47 +08:00
zhang-guiping
610c76087b [Bug]: ollama-cloud runtime fails DNS lookup for ai.ollama.com, while ollama/<model>:cloud works (#92594)
* fix(ollama): repair retired cloud provider endpoint

Route configured Ollama Cloud provider ids through plugin doctor compatibility migrations so doctor --fix can rewrite the retired ai.ollama.com endpoint before runtime reads persisted config.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(doctor): align provider fixture with typed config

Ensure the doctor registry provider-scoped migration test uses a fully typed provider fixture so the test type-check shard validates the intended behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(ollama): align doctor fixture with typed config

Use fully typed provider and model fixtures in the Ollama doctor contract tests so the extension test type-check shard validates the migration behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ollama): preserve custom cloud provider base url

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ollama): avoid logging retired endpoint secrets

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 14:20:24 +08:00
Vincent Koc
a664c44375 fix(scripts): create extension memory report dirs 2026-06-16 08:19:39 +02:00
Vincent Koc
add00d747b build(docs): finish PowerShell-safe docs formatting (#93512)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: yil337 <220073147+yil337@users.noreply.github.com>
2026-06-16 14:19:01 +08:00
zhang-guiping
bb164384c2 [Bug]: ollama-cloud runtime fails DNS lookup for ai.ollama.com, while ollama/<model>:cloud works (#92594)
* fix(ollama): repair retired cloud provider endpoint

Route configured Ollama Cloud provider ids through plugin doctor compatibility migrations so doctor --fix can rewrite the retired ai.ollama.com endpoint before runtime reads persisted config.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(doctor): align provider fixture with typed config

Ensure the doctor registry provider-scoped migration test uses a fully typed provider fixture so the test type-check shard validates the intended behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(ollama): align doctor fixture with typed config

Use fully typed provider and model fixtures in the Ollama doctor contract tests so the extension test type-check shard validates the migration behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ollama): preserve custom cloud provider base url

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ollama): avoid logging retired endpoint secrets

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 14:17:57 +08:00
Vincent Koc
4a0e376d1f fix(imessage): normalize leading NUL echo-cache prefixes (#93511)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: jason <3031622+drvoss@users.noreply.github.com>
2026-06-16 14:17:42 +08:00
zhang-guiping
2196ea2930 fix #85871: [Bug]: Heartbeat scheduler silently fails to fire on 5.20 and all 5.x versions (regression from 4.23) (#88970)
* fix heartbeat deferral during active embedded runs

* fix heartbeat admission busy retry

* fix(heartbeat): bind retry to local admission

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 14:17:37 +08:00
Vincent Koc
6aa83374d9 fix(scripts): pin Docker preflight platform 2026-06-16 08:06:29 +02:00
openclaw-clownfish[bot]
59950f7b52 fix(ui): preserve gateway token during safe websocket url edits (#73923)
* fix(ui): preserve gateway token during safe websocket url edits

* fix(ui): preserve gateway token during safe websocket url edits

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
2026-06-16 14:03:13 +08:00
Vincent Koc
ccf83ace38 fix(plugins): repair missing required platform packages 2026-06-16 14:00:11 +08:00
xydigit-sj
2b752ac0d1 fix(doctor): repair null agents.list[].workspace values (#93105)
A literal null `workspace` field in an agent entry failed schema validation at
startup, producing a crash loop that `openclaw doctor --fix` could not recover
from because the compatibility pipeline never normalized the malformed field.

Add a narrow doctor migration that removes null `workspace` values from
`agents.list` entries and relies on the existing fallback path (defaults or
stateDir-derived workspace) at runtime.

Fixes #77718.
2026-06-16 13:59:36 +08:00
Alix-007
01d3505d7c fix(auto-reply): redact secrets in /debug show and /debug set output (#93333)
PR #88496 routed /config show and /config set chat output through the
shared schema-aware redaction path, but the sibling /debug commands in
the same handler were left untouched. /debug show JSON-stringified the
full runtime override tree verbatim and /debug set echoed the raw value,
so a secret-shaped override (e.g. gateway.auth.token, channels.*.botToken)
set via /debug set was rendered in plaintext to chat-visible output.

Apply redactConfigObject(overrides, schema.uiHints) to the override tree
before rendering /debug show, and reuse formatConfigSetValueLabel for the
/debug set acknowledgement, matching the existing /config redaction
contract. Non-secret fields and env placeholders are preserved.
2026-06-16 13:59:28 +08:00
Agent外设王东旭
37636ac8e2 Fix Matrix bracketed display-name mentions (#83156)
Co-authored-by: dxw <wdx@me.com>
2026-06-16 13:57:56 +08:00
Harjoth Khara
5a9396ef6d fix(ui): restore provider usage pill in desktop chat composer [AI] (#93055)
* fix(ui): restore provider usage pill in desktop chat composer (#93041)

Composer refactors dropped the quota pill from renderChatControls and left the
desktop renderChatSessionSelect wrapper orphaned, so it rendered nowhere on
desktop. Re-attach the existing pill, add modelAuthStatusResult to the guarded
controls dep list so it updates when usage windows arrive async, and hide it on
the 2-col mobile composer grid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(ui): add real-browser e2e proof for chat quota pill (#93041)

Playwright/Chromium test that mocks models.authStatus usage windows and asserts
the restored provider usage pill renders in the desktop chat composer (and is
absent without usage). Skips gracefully when Chromium is unavailable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(ui): write quota-pill e2e screenshots to ignored .artifacts path (#93041)

Match the control-ui-e2e convention (.artifacts/control-ui-e2e/...) so the proof
run does not leave untracked root-level files. Addresses ClawSweeper review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:57:40 +08:00
Vincent Koc
e934e1cad7 fix(scripts): share Docker E2E artifact bounds 2026-06-16 07:39:44 +02:00
lizeyu-xydt
5afddf547e fix(discord): apply tool status emojis immediately to avoid override by thinking reactions (#93488)
* fix(discord): apply tool status emojis immediately to avoid override by thinking reactions

Tool emoji reactions (🛠️, 🌐, 🔎, etc.) during Discord tool/skill execution
were not appearing because setTool() used a 700ms debounce shared with
setThinking(). Rapid onReasoningStream calls from overlapping reasoning
would repeatedly overwrite the pending tool emoji with 🧠, so the tool
emoji never reached Discord.

Fix by making setTool() apply emojis immediately (skip debounce). Tool
transitions are user-facing state changes that should be visible without
delay, and the terminal done/error transitions already flush any pending
state.

Fixes #92715.

* fix(discord): forward quiet tool lifecycle status

* fix(slack): preserve tool status reactions

* test(channels): type quiet tool lifecycle options

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 13:33:32 +08:00
Vincent Koc
9a0aefb73f fix(scripts): bound Docker E2E JSON helpers 2026-06-16 07:31:17 +02:00
Goutam Adwant
325d0208d0 fix(ui): add agent selector to skills page (#93487)
* fix(ui): add agent selector to skills page

* test(ui): stabilize skills agent selector checks

* fix(skills): preserve agent-scoped state

* fix(skills): refresh current scope after config updates

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 13:30:25 +08:00
Vincent Koc
983e0f2ba0 docs: refresh generated API baselines 2026-06-16 07:26:19 +02:00
Jason (Json)
37c1dacac9 docs: point PR landing at maintainer workflow (#93494)
* docs: point PR landing at maintainer workflow

* docs: name PR landing scripts

* docs: specify PR landing commands

* docs: point PR landing at canonical wrapper

* docs: document canonical PR wrapper flow

* docs: scope PR wrapper flow to main

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 13:24:51 +08:00
ly-wang19
ca5c3e677a fix(cron): clear delivery routing fields from cron edit (#93495)
* fix(cron): clear delivery routing fields from cron edit

cron edit could set delivery channel/to/thread-id/account but could not unset them: an empty value (e.g. --to "") builds delivery.X = undefined, which is omitted from the JSON-RPC patch, so mergeCronDelivery never sees the key and the field is silently kept. The gateway RPC already accepts an explicit null to clear each field (CronDeliveryPatchSchema + mergeCronDelivery via normalizeOptionalString); the CLI just never sent it.

Add --clear-channel/--clear-to/--clear-thread-id/--clear-account, each emitting null (mirroring the existing --clear-model), with mutual-exclusion guards against the matching set flag and against --webhook.

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

* fix(cron): preserve delivery defaults when clearing routes

* fix(cron): validate cleared prefixed routes

---------

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 13:24:33 +08:00
Vincent Koc
2fec8b12d5 test(feishu): make fallback dispatch assertion observable 2026-06-16 13:23:44 +08:00
Vincent Koc
a89c9937c2 fix(ci): normalize Windows WSL probe output 2026-06-16 07:21:33 +02:00
Vincent Koc
9bdf89598e fix(e2e): retry macOS Parallels npm install 2026-06-16 13:19:02 +08:00
Vincent Koc
df17e01cac test(feishu): prove partial runtime fallback dispatches 2026-06-16 13:16:19 +08:00
ooiuuii
9d1dec4678 fix(cli): honor route-first log level (#93460) 2026-06-16 13:15:52 +08:00
Vincent Koc
350f06362b fix(e2e): budget macOS Parallels onboarding 2026-06-16 13:15:43 +08:00
xydt-tanshanshan
a2bc7ab269 [AI] fix(feishu): guard against missing inbound in channelRuntime fallback (#93466)
* [AI] fix(feishu): guard against missing inbound in channelRuntime fallback

When channelRuntime from gateway context is truthy but lacks the inbound
property, the ?? operator still selects it over getFeishuRuntime().channel,
causing TypeError at core.channel.inbound.run().

The ChannelGatewayContext types channelRuntime as ChannelRuntimeSurface
(only guarantees runtimeContexts), but channel.ts casts it to
PluginRuntimeChannel via type assertion. If a partial runtime object
without inbound is provided, the type lie becomes a runtime crash.

Fix: check channelRuntime?.inbound before using it; fall back to
getFeishuRuntime().channel when inbound is absent.

Related to #93453

* [AI] test(feishu): add regression for partial channelRuntime lacking inbound

When channelRuntime has runtimeContexts but no inbound, the guard in
bot.ts should fall back to getFeishuRuntime().channel. Add a test that
passes a partial channelRuntime and verifies dispatch does not crash.

Refs #93453
2026-06-16 13:15:08 +08:00
Vincent Koc
7ac2bbaaf0 fix(qa): install gauntlet plugin requirements 2026-06-16 06:59:58 +02:00
Vincent Koc
96404a7bd5 fix(scripts): bound gauntlet QA summaries 2026-06-16 06:59:15 +02:00
Vincent Koc
484ee14273 fix(scripts): bound plugin install index artifacts 2026-06-16 06:43:01 +02:00
Vincent Koc
88c9e4d644 fix(e2e): clear restored macOS npm cache 2026-06-16 12:41:00 +08:00
Patrick Erichsen
99a398a4b1 docs: add ClawHub content rights to sidebar (#93489) 2026-06-15 21:35:24 -07:00
Edward Abrams
ef3e5f5e31 perf(plugins): thread prepared manifest plugins through runtime normalization (#85254)
Carry prepared manifest model-id normalization records through the runtime bridge so hot callers reuse existing metadata instead of consulting the snapshot fallback.

The final change preserves the existing no-prepared-record behavior, adds focused forwarding coverage, and removes the one-off proof script before landing.

Thanks @zeroaltitude.

Verification:
- 224 focused tests
- full CI run 27594070734
- real behavior proof run 27594081022
- final whole-branch autoreview clean

Co-authored-by: zeroaltitude <zeroaltitude@gmail.com>
2026-06-16 06:31:36 +02:00
Vincent Koc
6f53f84af3 fix(gateway): normalize paired access lists 2026-06-16 12:31:24 +08:00
wangmiao0668000666
d6eefa191f fix(device-pairing): guard mergeRoles/mergeScopes against non-string entries (#90654) 2026-06-16 12:31:24 +08:00
wangmiao0668000666
481652d78a fix(gateway): guard formatAuditList against non-string items to prevent handshake trim crash (#90654) 2026-06-16 12:31:24 +08:00
Vincent Koc
9a86a2b30b fix(qa): ignore setup gauntlet observations 2026-06-16 06:30:11 +02:00
Agustin Rivera
300794520b fix(discord): block cross-provider guild admin actions (#93354)
* fix(discord): block cross-provider guild admin actions

* fix(discord): reject cross-provider moderation actions

* fix(discord): preserve manual admin action trust

* fix(discord): align admin action trust guard
2026-06-15 21:28:53 -07:00
Vincent Koc
f9376b16d4 fix(scripts): bound npm onboard status artifacts 2026-06-16 06:19:32 +02:00
Agustin Rivera
ee81082f57 fix(openshell): pin mirror remote mutations (#93361)
* fix(openshell): pin mirror remote mutations

* fix(openshell): pin mirror remote mutations

* fix(openshell): keep mirror state pinned

* fix(openshell): preserve pinned remove failures
2026-06-15 21:16:59 -07:00
Marvinthebored
395a082348 fix(codex): dedupe commentary raw response echoes (#93343)
Suppress each raw commentary echo paired with a typed Codex item completion by protocol order, while preserving later raw-only notes and contributor-rewritten completion text.

Fixes #93296.
Thanks @Marvinthebored.

Verification:
- 95 focused projector tests
- full CI run 27593515603
- real behavior proof run 27593522821
- local and whole-branch autoreview clean

Co-authored-by: Peter Lindsey <peter@lindsey.jp>
2026-06-16 06:13:47 +02:00
Onur Solmaz
8c108c294d fix(agents): honor disabled envelope timestamps at model boundary (#93238)
Merged via squash.

Prepared head SHA: 53f7117a4b
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
Reviewed-by: @osolmaz
2026-06-16 12:13:24 +08:00
Vincent Koc
48d96cd8a1 fix(update): retry launchd handoff recovery 2026-06-16 12:09:01 +08:00
yetval
d1f6ca20a1 fix(update): keep CLI plugin post-update failure behavior unchanged 2026-06-16 12:09:01 +08:00
yetval
c9418b8afd fix(update): restart managed gateway when update handoff fails after stop 2026-06-16 12:09:01 +08:00
Vincent Koc
0c657190ec fix(qa): fail runtime parity on cell failures 2026-06-16 05:53:19 +02:00
Vincent Koc
6ffa0fb348 test(plugins): narrow session extension registry coverage 2026-06-16 11:36:43 +08:00
杨浩宇0668001029
0fb0c2cb8e fix(plugins): keep empty session extension pins authoritative
Pinned session-extension registries now remain the owner even when empty, preventing later active registry churn from leaking agent-owned extensions into the gateway surface.
2026-06-16 11:36:43 +08:00
杨浩宇0668001029
0e71ce1174 test(plugins): cover session extension registry lifecycle
Exercise pinned startup session extensions through WebSocket patching, release cleanup, standalone loading, and active-registry churn.
2026-06-16 11:36:43 +08:00
杨浩宇0668001029
ffa736f713 fix(plugins): satisfy session extension lint 2026-06-16 11:36:43 +08:00
杨浩宇0668001029
b85ae9fb1b fix(plugins): pin session extension registry 2026-06-16 11:36:43 +08:00
Alex Knight
67c80e941e fix(gateway): fall back to managed path when inbound PDF sandbox staging fails (#90097) 2026-06-16 13:19:32 +10:00
Alex Knight
e850750754 fix(media): extract large managed inbound PDFs via media-understanding (#90096, #90097)
Inbound PDF/document text already flows to agents through the canonical
media-understanding pipeline (applyMediaUnderstanding -> extractFileBlocks),
but it inherited the OpenResponses input_file limits (5MB / 4 pages), so large
managed PDFs from channels/Control UI were skipped and locked-down agents saw
only an attachment marker.

- Size inbound file extraction from agents.defaults.mediaMaxMb (default 20MB,
  cap 25MB) and pdfMaxPages (default 20, cap 150) via a new
  resolveFileExtractionLimits; explicit gateway responses.files config still
  wins per-field. (#90096)
- chat.send: let oversized (>5MB) managed inbound PDFs pass through sandbox
  staging with their managed media path instead of a 4xx, so host-side
  extraction reaches sandboxed agents without copying the file into every
  sandbox; non-PDF oversize files are still rejected. (#90097)

Reuses the existing extraction/injection path; no parallel module or extra
prompt-injection sites.
2026-06-16 13:19:32 +10:00
Vincent Koc
7c6ad2327c fix(infra): narrow inherited gateway pid protection 2026-06-16 11:11:25 +08:00
amittell
d88f1bf217 fix(infra): preserve inherited gateway PID across reparent during cleanStaleGatewayProcessesSync
When a child openclaw process is spawned via a backgrounded subshell that
exits before the new process reaches the stale-pid sweep, the new process
is reparented to the supervisor (PID 1 / launchd) and the ancestor walk
in getSelfAndAncestorPidsSync can no longer see the running gateway. The
running gateway then shows up on lsof as an unrelated sibling on the
port and gets SIGKILL'd by cleanStaleGatewayProcessesSync, recreating
the issue #68451 supervisor restart loop across a reparent boundary.

Real-world trigger: a user ~/.zshrc auto-start block
  if ! pgrep -x openclaw-gateway >/dev/null; then
    (openclaw gateway >/dev/null 2>&1 &)
  fi
combined with codex per-turn `zsh -c "set -e; . shell_snapshot"` invocations
caused every chat turn on rh-bot to SIGKILL its launchd-managed gateway,
producing HTTP 000 errors and ~33 kill events captured by a forensic
launchd unified-log tracker before the zshrc was patched.

Fix: gateway-cli captures OPENCLAW_GATEWAY_SERVICE_PID from inherited env
BEFORE overwriting it with process.pid, then threads the captured PID
through cleanStaleGatewayProcessesSync into getSelfAndAncestorPidsSync's
exclusion set. The protection is opt-in per call site so existing
maintainer paths (openclaw update / openclaw doctor restart helpers) keep
their ability to terminate a running gateway intentionally.

The inherited-PID parser is strict positive-integer only: a malformed
inherited env value (`"123abc"`, `"123.4"`, `"0x7b"`, etc.) is rejected
rather than silently protecting PID 123 from cleanup and leaving the
stale listener alive. New focused unit tests cover the parser
contract.

Existing regression tests cover the reparent suicide-kill scenario and
the defensive ignore-non-positive-PID contract on the cleanup side.
2026-06-16 11:11:25 +08:00
Vincent Koc
6da2d6ac5a fix(crabbox): bootstrap absolute macOS env pnpm 2026-06-16 04:59:05 +02:00
Vincent Koc
2b05bd7b0d fix(cli): preserve sessions_yield over MCP 2026-06-16 10:53:58 +08:00
张贵萍0668001030
eea350f2ff fix: preserve yielded CLI lifecycle state 2026-06-16 10:53:58 +08:00
张贵萍0668001030
c8c94e15ad fix(gateway): refresh loopback yield cache lifecycle 2026-06-16 10:53:58 +08:00
张贵萍0668001030
d7a09b13e6 fix(gateway): satisfy lint for MCP yield context 2026-06-16 10:53:58 +08:00
张贵萍0668001030
88e4a0f0d5 fix(gateway): propagate MCP yield session context 2026-06-16 10:53:58 +08:00
Vincent Koc
db194a6887 fix(crabbox): detect direct env changed gates 2026-06-16 04:40:09 +02:00
Vincent Koc
00160ea6ee fix(workboard): refuse unsafe SSHFS SQLite storage
Preserve rollback journaling for NFS and SMB-backed stores, refuse SSHFS after symlink-aware mount classification, and close Workboard database handles when filesystem policy rejects initialization.
2026-06-16 04:34:14 +02:00
Vincent Koc
ffb67d2d2e fix(qa): suppress empty WhatsApp debug artifacts
Suppress empty WhatsApp gateway-debug artifact publication and keep the public QA run view redacted and consistent across report/evidence output.

Verification:
- Testbox focused WhatsApp QA runtime format/lint/test run passed: https://github.com/openclaw/openclaw/actions/runs/27589031659
- Testbox changed gate passed: https://github.com/openclaw/openclaw/actions/runs/27589128132
- PR CI passed on final head: https://github.com/openclaw/openclaw/actions/runs/27589903708
- git diff --check passed locally
2026-06-16 10:32:53 +08:00
Vincent Koc
d89ab2c014 fix(e2e): wait for Parallels update cleanup 2026-06-16 04:19:54 +02:00
Aniruddha Adak
11a0ad10e9 test: make install-safe-path symlink tests compatible with Windows 2026-06-16 10:12:47 +08:00
Vincent Koc
9b6bed7a75 fix(memory): release reindex lock after failed init 2026-06-16 04:04:19 +02:00
Vincent Koc
f87d194b8b fix(memory): prevent peer-write loss during reindex 2026-06-16 04:04:19 +02:00
Vincent Koc
386b0e6c74 fix(backup): snapshot all live SQLite state
Use transactionally consistent VACUUM INTO snapshots for every state-root SQLite database and exclude original journal sidecars so verified backups cannot restore torn plugin or memory state.
2026-06-16 04:00:43 +02:00
Vincent Koc
ee495abda1 fix(release): satisfy retry delay lint 2026-06-16 09:58:25 +08:00
Vincent Koc
147e979713 fix(postinstall): bound packaged dist scans 2026-06-16 03:57:44 +02:00
Vincent Koc
1ee788189a fix(release): accept trusted main Telegram evidence 2026-06-16 09:54:56 +08:00
Vincent Koc
e71cf0ffcb fix(release): tolerate npm propagation after publish 2026-06-16 09:51:47 +08:00
Vincent Koc
3c65127827 fix(qa): preserve WhatsApp live failure diagnostics 2026-06-16 03:42:26 +02:00
Vincent Koc
a4e7d9a0db test(plugin-sdk): ratchet SQLite helper surface 2026-06-16 09:36:32 +08:00
Vincent Koc
ac8a3f367c fix(sqlite): disable WAL on network filesystems 2026-06-16 09:36:32 +08:00
openclaw-clownfish[bot]
8694fe7e81 fix(gateway): block internal HTTP session overrides
Reject HTTP session-key overrides that target reserved internal session namespaces while preserving normal explicit session keys.

Co-authored-by: RichardCao <4612401+RichardCao@users.noreply.github.com>
2026-06-16 09:30:27 +08:00
openclaw-clownfish[bot]
073343e2e2 fix(outbound): ignore schema-padded poll metadata on send
Ignore schema-padded poll metadata on plain send actions unless content-bearing poll fields are present.

Co-authored-by: 鄧 偉程 <148790968+weichengdeng@users.noreply.github.com>
2026-06-16 09:29:16 +08:00
Vincent Koc
aa0d710085 fix(release): bound artifact package scans 2026-06-16 03:24:57 +02:00
Vincent Koc
c70b9849d9 fix(agents): handle string assistant message content 2026-06-16 09:23:40 +08:00
Vincent Koc
919c5b7c7b fix(plugin-sdk): calibrate callable surface budget 2026-06-16 03:19:24 +02:00
Vincent Koc
5296dc378f fix(release): bound postpublish dist scans 2026-06-16 03:15:52 +02:00
Shakker
a447f9a43d fix: guard session message cache payloads 2026-06-16 02:08:24 +01:00
Vincent Koc
04b7e192af fix(release): require full validation child run urls 2026-06-16 03:06:01 +02:00
Dallin Romney
450060d7a2 test(qa): expand smoke-ci and release categories and coverage (#93175)
* test(qa): add smoke ci primary coverage evidence

* test(qa): remove overstated primary coverage claims

* test(qa): make release profile include smoke ci

* test(qa): trim taxonomy formatting churn

* test(qa): avoid hardcoded profile names in coverage test

* test(qa): make release profile cover taxonomy

* test(qa): type profile fixture all category flag

* test(qa): include channel delivery in smoke ci profile
2026-06-15 18:05:52 -07:00
Vincent Koc
6bc57ca73a fix(migrate-hermes): snapshot live SQLite archive 2026-06-16 03:01:37 +02:00
Vincent Koc
ea346f4361 fix(sqlite): close databases after failed initialization 2026-06-16 03:00:23 +02:00
Vincent Koc
d5c9e7ea99 test(plugin-sdk): ratchet surface budget checks 2026-06-16 02:56:41 +02:00
Vincent Koc
9eed9c5758 fix(e2e): derive lifecycle proc units 2026-06-16 02:56:41 +02:00
Vincent Koc
1c2363def6 fix(plugin-sdk): refresh QA self-check API baseline 2026-06-16 02:56:41 +02:00
Vincent Koc
b7d53800d6 fix(release): require beta smoke run url 2026-06-16 02:55:19 +02:00
Vincent Koc
6326395c0a fix(state): make SQLite sidecar archives retriable
Archive the canonical legacy database before SQLite sidecars, then detect and finish pending sidecar cleanup on retry without reopening the migrated database.
2026-06-16 02:52:29 +02:00
Vincent Koc
568f2d5631 fix(sessions): guard unsafe transcript serialization cache 2026-06-16 02:43:58 +02:00
Vincent Koc
e94b666e45 fix(mac): isolate dmg image cleanup 2026-06-16 02:43:16 +02:00
Dirk
ee3b7eb7c0 fix(telegram): forward Bot API 10.1 rich_message content to agent (#93418)
* fix(telegram): surface unsupported inbound rich messages

* fix(telegram): isolate rich message placeholders

* fix(telegram): accept typed rich message inputs

* fix(telegram): preserve rich message cache marker

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 08:42:48 +08:00
Alex Knight
2365a137d8 fix(mattermost): keep message tool replies in threads (#93424)
* fix(mattermost): keep message tool replies in threads

* fix(outbound): preserve one-root reply threading

* fix(outbound): preserve explicit reply target precedence

* fix(mattermost): mirror inherited replies to root session

* test(outbound): align reply transport contract

* fix(mattermost): align mirrored thread root

---------

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 08:36:59 +08:00
Alex Knight
dc09d148bb fix(guards): allow auth profile sqlite reader (#93448)
Allow the auth-profile read-only SQLite bootstrap path through the Kysely guardrail. The runtime already wraps reads with Kysely; the raw DatabaseSync boundary is the short-lived read-only bootstrap.

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-06-16 10:34:13 +10:00
Gio Della-Libera
55263b3dfa feat(policy): cover exec approvals artifact (#90003)
Add exec approvals artifact evidence to Policy.

- add the execApprovals policy namespace and check IDs for required artifact presence, default/per-agent security posture, autoAllowSkills, and allowlist drift
- read the active exec-approvals.json artifact only when execApprovals policy rules are configured, honoring OPENCLAW_STATE_DIR before the default ~/.openclaw path
- emit redacted posture evidence and stable oc:// references without socket tokens, command text, resolved paths, timestamps, or approval-session details
- document the public policy surface and add focused scanner, doctor, conformance, and CLI coverage

Validation:
- GitHub Actions for head b82eefe492 are green, including Real behavior proof.
- ClawSweeper re-review completed for the same head with proof: sufficient and status: ready for maintainer look.
- Maintainer artifact-boundary acceptance is recorded in the PR discussion and body.

Co-authored-by: Gio Della-Libera <235387111+giodl73-repo@users.noreply.github.com>
2026-06-15 17:30:48 -07:00
ZengWen-DT
01acb34bdb fix(tui): show activity indicator for system-injected runs (#93427)
* fix(tui): show activity indicator for system-injected runs

System-injected runs (bridge-notify, webhook, cron) never go through the
TUI submit path, so no active/pending run id exists when their lifecycle
"start" event arrives. handleAgentEvent dropped events for untracked runs,
leaving the status bar idle until the response landed.

Adopt an untracked lifecycle "start" for the current session (lifecycle
events always carry sessionKey) so the activity indicator shows work is
happening, mirroring how chat deltas adopt runs in handleChatEvent. Local
side-question (btw) runs never claim the active slot.

Closes #51825

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tui): preserve concurrent injected run activity

---------

Co-authored-by: zengwen <zeng_wen@foxmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 08:27:59 +08:00
zengLingbiao
03e3ef86af fix(agents): resolve configured default model in runEmbeddedAgent (fixes #93419) (#93428)
* fix(agents): honor configured default model in embedded runs

* fix(agents): resolve embedded defaults from runtime config

* fix(agents): preserve embedded model routing semantics

* test(agents): model current embedded attempts explicitly

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 08:27:19 +08:00
Harjoth Khara
eac3e08cfd fix(line): cap carousel column text at 60 chars when a title or image is set (#93429)
* fix(line): cap carousel column text at 60 chars with title or image

LINE limits a carousel column's text to 60 characters when the column has
a title or thumbnail image, and 120 characters otherwise. createCarouselColumn
always truncated to 120, so a column with a title/image and 61-120 char text
exceeded the limit and made LINE reject the entire carousel reply (HTTP 400).
Apply the conditional limit (mirroring the buttons template) and drop the now
redundant slice in createProductCarousel.

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

* fix(line): apply conditional text limits across templates

* fix(line): truncate template text by code point

* fix(line): preserve grapheme clusters when truncating

* fix(line): apply compact limit for default actions

* fix(line): follow title and thumbnail text limits

* fix(line): truncate template text within UTF-16 limits

* fix(line): preserve required text within template limits

* fix(line): preserve carousel product prices

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 08:23:05 +08:00
NianJiu
a375d6c849 fix(telegram): gate rich messages behind opt-in (#93279)
Restore readable standard Telegram text delivery by default after Bot API 10.1 rich messages rendered as unsupported in current clients. Keep native rich tables and structured messages available through the account-level richMessages opt-in, with account-aware capability advertising and documented structural limits.

Fixes #93263.
2026-06-15 17:22:41 -07:00
Vincent Koc
9dbf8f718f fix(sessions): guard append cache after extension serialization 2026-06-16 08:13:38 +08:00
Vincent Koc
fd806ada64 fix(agents): bound autoreview scope (#93435) 2026-06-16 07:58:07 +08:00
Vincent Koc
4ca8bf086c fix(proxy): close cached SQLite stores by path
Track debug proxy capture stores per database path so replacing or concurrently leasing capture paths cannot orphan SQLite handles and WAL checkpoint timers.
2026-06-16 01:54:09 +02:00
Vincent Koc
b41c0b6746 fix(cli): preserve gateway request errors in json mode 2026-06-16 01:52:23 +02:00
Marcus Castro
52d9d16e1b fix(whatsapp): preserve auth on terminal disconnects (#93076)
* fix(whatsapp): preserve auth on passive terminal stops

* fix(whatsapp): recover stale web auth during relink

* fix(gateway): defer channel stop until qr takeover
2026-06-15 20:50:22 -03:00
Vincent Koc
0ef8620746 fix(auth): wait through SQLite read contention
Apply the canonical SQLite busy timeout to short-lived read-only auth profile reads so a brief rollback-journal exclusive lock cannot make valid persisted credentials appear missing.
2026-06-16 01:41:05 +02:00
Vincent Koc
74c6f175c7 fix(ci): skip transcript guard for older release targets 2026-06-16 07:40:47 +08:00
Alix-007
0d50ec77de fix(memory): swap rollback-journal sidecar during atomic reindex (#93295)
The atomic reindex file ops hardcoded the WAL sidecar pair (-wal/-shm)
when moving, removing, and backing up index files. NFS-backed memory
stores run SQLite under journal_mode=DELETE, which produces a
rollback-journal (-journal) sidecar instead. As a result an index swap
left the previous targets stale -journal next to the freshly published
2026-06-16 07:37:17 +08:00
Dr Rushindra Sinha
eccfacb02c fix(whatsapp): stop markdownToWhatsApp dropping code spans followed by a digit (#93409)
The inline-code/fence restore step matched the placeholder index with a
greedy `(\d+)`, so a digit in user text immediately after a code span
(e.g. `code`5) was absorbed into the index, resolved to undefined, and
`?? ""` deleted both the code span and the digit. Terminate the
placeholder index with the existing NUL marker so the index boundary is
unambiguous.

Co-authored-by: Dr Rushindra Sinha <5796457+rushindrasinha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 07:35:28 +08:00
Matt Gunnin
f08b24e63c fix(discord): suppress tool progress for message-tool replies (#93412)
* fix(discord): suppress tool progress for message-tool replies

* fix(discord): preserve explicit status reactions for tool-only replies

* fix(discord): keep explicit status reactions private

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-16 07:35:15 +08:00
Vincent Koc
c32ba171db fix(qa): fail unsuccessful self-checks 2026-06-16 01:32:47 +02:00
Shakker
e64379dddb fix: stabilize transcript cache and CLI env isolation 2026-06-16 00:27:53 +01:00
Josh Lehman
127e174c9e refactor: route auto-reply sessions through session seam (#89124)
* clawdbot-6f0: route agent runtime session writes through seam

* clawdbot-6f0: route command entry persistence through seam

* test: ratchet auto-reply session accessor writes

* refactor: scope embedded attempt quota reads

* test: ratchet session store save writer
2026-06-15 15:50:38 -07:00
joshavant
21e3cfa5e9 Add Apple Watch screenshot pipeline 2026-06-16 00:40:40 +02:00
Vincent Koc
c8e70708e9 fix(ci): restore main checks 2026-06-16 06:36:10 +08:00
Harjoth Khara
0bb415bf66 fix(sessions): preserve behavior overrides across rollover
Carry user behavior overrides across implicit daily/idle session rollover while preserving existing model fallback clearing behavior.
2026-06-16 06:35:26 +08:00
Dr Rushindra Sinha
00c58b6613 fix(whatsapp): notify when trailing media send fails
Notify users when a later WhatsApp media attachment fails instead of silently dropping it.
2026-06-16 06:35:09 +08:00
Vincent Koc
50dbd46aa6 test(agents): mark runtime contract delivery as sent 2026-06-16 06:31:06 +08:00
Keeley Hoek
baeedaa316 fix(discord): reject malformed realtime consult calls
Catch malformed realtime consult tool-call args, return a tool error, and avoid crashing Discord voice handling. Includes e2e coverage.
2026-06-16 06:27:49 +08:00
litang9
0f71a665ed fix(logging): avoid stalled warnings for active model calls
Classify owned silent model calls as long-running until the abort threshold while preserving stalled handling for ownerless stale activity, with diagnostics tests and docs.
2026-06-16 06:27:40 +08:00
lundog
bcd1fdb1db fix(media): stop pruning media on write
Stop media writes from triggering opportunistic pruning and leave retention cleanup to the configured maintenance timer. Preserve explicit cleanup options and cover shallow/root/recursive cleanup behavior.
2026-06-16 06:27:22 +08:00
Alix-007
1b5ac45e60 fix(doctor): separate platform-incompatible skills
Track platform-incompatible skills separately from missing requirements, keep doctor --fix from treating them as broken installs, and cover the status output.
2026-06-16 06:26:20 +08:00
Bakhtiyar
3d3d291902 fix(auth): keep alias-compatible auth-profile overrides
Use alias-aware credential compatibility before clearing auth-profile overrides, preventing compatible CLI sessions from flapping auth profiles. Includes regression coverage.
2026-06-16 06:26:11 +08:00
Andy Ye
57bad4bdf8 fix(cron): suppress announce control replies
Use the shared suppressed-control-reply detector for cron delivery so NO_REPLY, ANNOUNCE_SKIP, and REPLY_SKIP do not leak to outbound channels, with direct/text delivery coverage.
2026-06-16 06:25:21 +08:00
James W. Niu
923828ccd7 fix(infra): drop duplicated restart word
Avoid rendering "Gateway restart restart" for restart sentinel summaries, while keeping the existing wording for other restart kinds.
2026-06-16 06:25:12 +08:00
Franco Viotti
7305d9baca fix(googlechat): return sent thread metadata
Return Google Chat message and thread metadata from message sends, and cover the action/API result shape in tests.
2026-06-16 06:23:07 +08:00
ooiuuii
212fef8703 fix(cli): accept --no-color after subcommands
Normalize root --no-color before command parsing while preserving command option values, and add coverage for subcommand/root option placement.
2026-06-16 06:20:56 +08:00
pick-cat
7a008c53f4 fix(control-ui): keep workboard card titles visible
Keep workboard card titles visible when a column overflows by pinning implicit rows to content height, and add e2e coverage for the overflow case.

Fixes #91717
2026-06-16 06:18:22 +08:00
Vincent Koc
93fa065fb3 fix(ci): update Vitest past browser advisory 2026-06-16 06:16:04 +08:00
Vincent Koc
bdc3017c1a test(agents): require delivery evidence in suppression fixtures 2026-06-16 06:16:02 +08:00
Vincent Koc
6cb631afc9 fix(agents): distinguish delivery routes from references 2026-06-16 05:56:48 +08:00
Vincent Koc
fec6a0407f fix(agents): validate visible delivery receipts 2026-06-16 05:56:48 +08:00
Vincent Koc
43cef3a80c fix(agents): record visible delivery target evidence 2026-06-16 05:56:48 +08:00
Dallin Romney
e32929e12c Add slim evidence mode for QA profile evidence (#93179)
* test(qa): compact profile evidence execution metadata

* docs(qa): document compact profile evidence

* test(qa): support compact evidence mode

* test(qa): rename compact evidence mode to slim

* docs(qa): trim slim evidence wording

* fix(qa): avoid commander runtime import
2026-06-15 14:50:40 -07:00
pick-cat
f018945eff fix(doctor): import default-agent auth profiles into sqlite (#93156)
* fix(doctor): import default-agent auth profiles into sqlite

* fix(doctor): type legacy config auth imports

* fix(doctor): preserve auth json precedence

* fix(doctor): import config auth with state-only stores

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

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: sallyom <somalley@redhat.com>
2026-06-15 17:42:51 -04:00
Agustin Rivera
8b0eac7927 fix(plugins): enforce install policy in wrappers (#93357) 2026-06-15 13:59:37 -07:00
Josh Lehman
efa9a6110b refactor: add transcript runtime identity contract (#89201) 2026-06-15 13:04:03 -07:00
Michael Appel
32af1c0697 Control Telegram group history context (#89547)
* fix(telegram): control group history context

* fix(telegram): keep history mode type local

* fix(telegram): respect history mode during forum recovery
2026-06-15 12:37:04 -07:00
Vincent Koc
b3128ba93d fix(cron): require explicit poll delivery targets 2026-06-16 03:09:36 +08:00
Shakker
eb67ac5cbe fix: trim whatsapp admission sender identity 2026-06-15 20:06:16 +01:00
Marcus Castro
ef6b7e3659 docs(plugins): record whatsapp admission compatibility 2026-06-15 20:06:16 +01:00
Marcus Castro
a355825060 refactor(whatsapp): deprecate admission top-level fields 2026-06-15 20:06:16 +01:00
Marcus Castro
2cc25aa909 test(whatsapp): cover inbound admission contract 2026-06-15 20:06:16 +01:00
Marcus Castro
2758140607 refactor(whatsapp): add inbound admission envelope 2026-06-15 20:06:16 +01:00
Agustin Rivera
55d1324c7d fix(flock): bind allow-always to wrapped command (#93362)
* fix(flock): bind allow-always to wrapped command

* fix(flock): handle wrapper option aliases

* fix(flock): handle wrapper option aliases
2026-06-15 11:56:15 -07:00
Shakker
dc573a38dc fix: update dependency pins 2026-06-15 19:48:43 +01:00
Agustin Rivera
9dbe25e0f6 fix(outbound): guard cross-context message mutations (#93358) 2026-06-15 11:46:16 -07:00
Vincent Koc
e3ca10438a fix(agents): recognize bare-ok broadcast sends 2026-06-16 02:45:41 +08:00
Vincent Koc
68cbd1ae63 test(agents): align fallback delivery mock 2026-06-16 02:45:41 +08:00
Vincent Koc
0ea08076c3 fix(agents): preserve CLI message delivery evidence 2026-06-16 02:45:41 +08:00
Agustin Rivera
04d8a96b6c fix(ci): verify performance workflow downloads (#93355) 2026-06-15 11:43:55 -07:00
Shakker
5c2487dc9a test: type cli test mocks 2026-06-15 19:34:16 +01:00
Shakker
c40db057da fix: clean ios release signing lint 2026-06-15 19:29:26 +01:00
Chunyue Wang
df521a6459 fix(gateway): guard fast-path startup migrations (#93118)
* fix(cron): run legacy cron store migration in gateway fast path

* fix(cli): run gateway startup migrations

* fix(gateway): guard startup migrations and config selection

* fix(gateway): reconcile final startup environment

* fix(gateway): preserve guarded startup env semantics

* fix(gateway): guard service-mode recovery candidates

* fix(config): reconcile normalized env precedence

* fix(cli): clear replaced proxy signal handlers

* fix(gateway): reject invalid final config

* test(gateway): cover invalid future config reset guard

* test(cli): remove unused recovery state
2026-06-16 01:54:29 +08:00
Alix-007
a0b16f37e8 fix(sessions): cache validated transcripts across turns (#90412)
Avoid repeated full JSONL parsing and cloning on every embedded-agent turn by keeping a bounded, validated transcript cache and advancing repair incrementally.

The final implementation preserves lock ownership and exact fingerprint validation, publishes only verified writes, handles header rewrites and unterminated JSONL safely, and adds focused regression coverage.

Fixes #83943.

Co-authored-by: Alix-007 <li.long15@xydigit.com>
2026-06-15 10:32:35 -07:00
Michael Appel
cc000e0ffe fix(cron): preserve scheduled turn tool policy [AI] (#91499)
* fix(cron): preserve creator tool policy for scheduled turns

* fix(cron): cap edited scheduled turns

* fix(cron): preserve scheduled turn allowlists

* fix(cron): cap filtered tool surfaces

* fix(cron): cap scheduled turn conversions

* fix(cron): include bundled scheduled tools
2026-06-15 10:29:11 -07:00
joshavant
9092578d8d feat: configure ios app store release upload 2026-06-15 19:27:55 +02:00
joshavant
a23de348b2 feat: configure ios app store release signing 2026-06-15 19:27:55 +02:00
joshavant
7650397a22 fix: align ios agent rows 2026-06-15 19:27:55 +02:00
joshavant
5a2641fc41 test: add ios screenshot automation 2026-06-15 19:27:55 +02:00
joshavant
377f6181a9 feat: add ios screenshot fixture mode 2026-06-15 19:27:55 +02:00
joshavant
b896e22e10 fix: stabilize ios screenshot status bar 2026-06-15 19:27:55 +02:00
joshavant
1ffda5d3ca fix: migrate ios identifiers to openclawfoundation 2026-06-15 19:27:55 +02:00
joshavant
ec2788cf80 fix: launch configured ios bundle 2026-06-15 19:27:55 +02:00
joshavant
379de52b59 fix: use canonical ios bundle identifiers 2026-06-15 19:27:55 +02:00
joshavant
0944045b75 chore: prefer canonical ios signing team 2026-06-15 19:27:55 +02:00
joshavant
c932bf377b chore: update ios configuration 2026-06-15 19:27:54 +02:00
Agustin Rivera
ddb07fc597 fix(plugins): require owner for plugin writes (#93353) 2026-06-15 10:22:28 -07:00
Yuval Dinodia
caab343461 fix(reply): project preflight compaction gate by next-input on fresh tokens (#91488)
The fresh-tokens path of runPreflightCompactionIfNeeded fed the prompt-only
entry.totalTokens snapshot straight into the budget threshold check, dropping
the current user prompt estimate and the previous turn's output. The sibling
memory-flush gate and this function's own stale branch already project
base + output + estimate via resolveEffectivePromptTokens, so the preflight
gate under-triggered and let over-budget requests through to overflow-retry.

Project the fresh persisted base the same way: read transcript output when near
the threshold (mirroring the memory-flush gate's buffer) and run the fresh base
through resolveEffectivePromptTokens before the threshold check.
2026-06-16 01:10:42 +08:00
ZZ.No.1
8e55348ff9 fix(agents): canonicalize node binding selectors (#66985) 2026-06-16 01:09:01 +08:00
Bakhtiyar
8e56cf591d fix(cli): disable ScheduleWakeup/CronCreate in --print claude runs (#84434)
Claude Code built-ins ScheduleWakeup and CronCreate schedule a deferred
re-invocation managed by the persistent CLI runtime. In OpenClaw's
one-shot `claude -p` invocations the process exits at end_turn, so any
wakeup or cron registered during the run has no host to fire into and is
silently lost. Symptom: a CLI session spawns a background sub-agent,
calls ScheduleWakeup to poll for completion, ends the turn, and never
picks up the result — the work finishes unreviewed.

Append `--disallowedTools "ScheduleWakeup,CronCreate"` to both `args`
and `resumeArgs` in the anthropic CLI backend so the model cannot reach
for tools that don't survive the run mode. The right pattern in CLI
sessions is Monitor on the background output file, or a synchronous
sub-agent.
2026-06-16 01:08:51 +08:00
Peter Steinberger
40f1190c8b fix(configure): remove duplicate password wrapper 2026-06-15 12:54:54 -04:00
Josh Lehman
8ded756284 refactor: add transcript reader seam (#89121)
Merged via squash.

Prepared head SHA: 7ea7ea47ef
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-06-15 09:41:50 -07:00
Vyctor H. Brzezowski
f00de6b06a docs(cli): add agent selector to cli backend quick start (#74613) 2026-06-16 00:39:48 +08:00
Harjoth Khara
158d3db85a fix(gateway): pass managed inbound PDFs through chat.send (#90115)
* fix(gateway): pass managed inbound PDFs through when sandbox staging fails

chat.send force-stages offloaded non-image media into the sandbox workspace when
one exists. If that optional staging was unavailable or incomplete,
prestageMediaPathOffloads deleted the media buffers and failed the whole send
with a 5xx — even for already-managed inbound PDFs that are safe to read
host-side. A Control-UI-uploaded PDF could fail to send.

When staging throws or is incomplete, fall back to the absolute managed paths
iff every non-image offloaded ref is a managed-inbound application/pdf (reusing
the existing resolveInboundMediaReference allow-check + the PDF mime type). This
mirrors the existing no-sandbox passthrough: with MediaWorkspaceDir unset the
managed media dir is a default media-understanding local root, so the absolute
path resolves host-side. Gated all-or-nothing so a single non-managed or non-PDF
ref keeps the previous delete + 5xx behavior. Success path and oversized 4xx are
unchanged; managed buffers are not deleted on the fallback.

Fixes #90097

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(gateway): exempt managed PDFs from staging cap

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 00:39:17 +08:00
Alix-007
6cd6e3f39e fix(plugins): serialize binding approval saves (#88945) 2026-06-16 00:39:01 +08:00
Gio Della-Libera
4bb4252042 fix(cli): report Gemini CLI runtime auth status (#86544) 2026-06-16 00:38:27 +08:00
Spencer Fuller
95da644f6d docs(windows): fix WSL gateway-autostart recipe for WSL ≥ 2.6.1.0 idle-termination (#90992)
* docs(windows): fix WSL gateway-autostart recipe for WSL ≥ 2.6.1.0

Replace /bin/true with dbus-launch true to work around the WSL ≥ 2.6.1.0
idle-termination regression (microsoft/WSL #13416): the distro exits 15-20 s
after the last wsl.exe client detaches even with loginctl linger and an active
user service. dbus-launch true keeps a child-of-init process alive (workaround
from microsoft/WSL discussion #9245, validated on WSL 2.7.3.0).

Also replace /ru SYSTEM with /ru "$env:USERNAME". Per-user WSL distros (the
default setup) are not enumerable by the SYSTEM account — the task runs
silently without starting the distro. Running as the installing user account
fixes this; Windows prompts for the password at task creation time.

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

* docs(windows): add dbus-x11 prerequisite for WSL keepalive

dbus-launch is provided by dbus-x11, which is not installed by default
on fresh Ubuntu WSL distros. Without it the scheduled task hits
command-not-found silently. Add the apt-get install step before the
linger and gateway-install steps so the recipe is self-contained.

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

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-16 00:38:03 +08:00
ToToKr
6451550cd7 fix(heartbeat): refresh stale Current time line on every helper call (#44993) (#75025)
Rebase onto current upstream/main (head 4780546c12). Resolves the conflict from upstream's two-line Current time + Reference UTC helper output: appendCronStyleCurrentTimeLine now refreshes/collapses any prior helper-injected block via CURRENT_TIME_LINE_RE instead of returning early on a stale base.includes('Current time:') match. Preserves upstream-added doc comments. 16/16 current-time.test.ts pass; tsgo core clean.
2026-06-16 00:37:40 +08:00
Josh Lehman
10a4c7c10b feat(status): surface plugin health (#91952)
Merged via squash.

Prepared head SHA: 2cd914cec1
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-06-15 09:36:13 -07:00
Ayaan Zaidi
fa9b8c8c6b test(telegram): cover raw rich progress output 2026-06-15 22:02:39 +05:30
Ayaan Zaidi
f5a9456219 fix(telegram): preserve rich progress command output 2026-06-15 22:02:39 +05:30
ZengWen-DT
7208567382 fix(control-ui): respect agents.defaults.timeFormat for timestamps (#93297)
Thread the existing agents.defaults.timeFormat setting through the Control UI
bootstrap config so WebChat/Control UI timestamps render in the configured
12h/24h clock instead of always using the browser locale default. "auto"
keeps the browser default, so existing deployments are unchanged.

Closes #58147

Co-authored-by: zengwen <zeng_wen@foxmail.com>
2026-06-16 00:29:44 +08:00
fsdwen
b90f7d2fd0 fix(cron): trust agent output when channel is unresolved without explicit delivery (#92817)
resolveCronChannelOutputPolicy checked deliveryRequested === false
when there is no channel. Since deliveryRequested is optional
(?: boolean), undefined and missing opts both returned false,
blocking the hasRecoveredToolWarning rescue path for --no-deliver
cron runs whose agent recovered successfully.

Change === false to !== true: when no channel exists, prefer the
agent's final visible text unless delivery was explicitly
requested.

Fixes #90664

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 00:28:22 +08:00
Sash Zats
1e2363b687 fix(ios): refresh permission rows after grants (#91776) 2026-06-16 00:26:54 +08:00
hpt
c082a3b740 fix(tui): avoid inserting spaces into long CJK text (#78765) 2026-06-16 00:25:18 +08:00
ly-wang19
53541b2141 fix(cron): resolve lastRunStatus in cron list/show human output (#93245)
cron list/show printed "idle" for a job whose status is ok/error/skipped
when only lastRunStatus (the primary field) was set: formatStatus used
`lastStatus ?? "idle"` and omitted lastRunStatus, diverging from computeStatus
(the --json status resolver) whose JSDoc says it mirrors the human output.
Delete the duplicate formatStatus and render via the canonical computeStatus.

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 00:23:14 +08:00
Sash Zats
a3070e3ddf fix(ios): respect chat header safe area (#91768) 2026-06-16 00:22:59 +08:00
Anurag Bheemappa Gnanamurthy
865e4db1cd fix(configure): mask gateway password input in CLI wizard prompt
Refs #90571
2026-06-16 00:11:52 +08:00
Anurag Bheemappa Gnanamurthy
31a10ee388 fix(models): mask paste-token input in CLI auth prompt
Mask paste-token TTY entry via the existing password prompt path while preserving stored token behavior.

Refs #90894.
Refs #90895.
2026-06-16 00:11:48 +08:00
苹果小姐
a34ddce9a2 refactor: remove dead code and improve string concatenation
Refs #91117
2026-06-16 00:11:18 +08:00
Alix-007
f6a3ac7e58 fix(daemon): prefer stderr over stale stdout in gateway restart diagnostics
Refs #93001.
2026-06-16 00:09:41 +08:00
Alix-007
95772c8541 fix(tui): keep stderr visible when local shell stdout fills the output cap
Keeps tail-truncated local shell output aligned with streaming behavior so trailing stderr remains visible when stdout fills the output cap.
2026-06-16 00:09:32 +08:00
xydt-tanshanshan
85a635368e fix(memory): prevent empty expected model in memory index identity
Guard memory index identity resolution against empty or whitespace provider models by falling back to fts-only, and use fts-only as the fallback source model when an adapter fallback cannot resolve a model.

This prevents empty expectedModel mismatch reasons that can leave memory search dirty while preserving registered adapter default-model resolution.

Refs #90787
2026-06-15 23:56:39 +08:00
ly-wang19
0888debcd3 fix(cron): add cron edit --clear-model
Completes the CLI half of #91298.
2026-06-15 23:54:00 +08:00
Rohit
794bd89fa0 fix(cli): allow zero Discord timeout duration
Fixes #93327
2026-06-15 23:42:51 +08:00
openclaw-clownfish[bot]
93b7e3d717 fix(memory-core): safely refresh qmd index during collection repair
Squash merge ProjectClownfish replacement PR #92910.\n\nSource PR credit: thanks @imadal1n for the original index rewrite direction in #68590.
2026-06-15 23:34:01 +08:00
Ayaan Zaidi
da92615816 feat(telegram): send rich messages as rich html (#93286)
* feat(telegram): render rich messages through rich html

* docs(telegram): teach agents rich formatting

* fix(telegram): bound rich draft payloads (#93286)

* fix(telegram): narrow rich draft payload type (#93286)

* fix(telegram): preserve rich table cell formatting (#93286)

* fix(telegram): honor rich table mode config (#93286)

* fix(telegram): default rich markdown tables (#93286)

* fix(telegram): gate rich table block mode (#93286)

* fix(telegram): normalize raw rich html limits (#93286)

* fix(telegram): preserve link preview suppression (#93286)

* fix(telegram): preserve rich markdown headings (#93286)

* fix(telegram): reject unsupported rich media sources (#93286)

* fix(telegram): honor link preview off in rich chunks (#93286)

* fix(telegram): avoid double escaping markdown media (#93286)

* fix(telegram): render markdown media via placeholders (#93286)

* fix(telegram): preserve table text in prompt context (#93286)
2026-06-15 20:38:41 +05:30
Vincent Koc
767e8280ac fix(cli): harden official plugin recovery (#93325)
* fix(cli): harden official plugin recovery

* fix(config): preserve include write context

* fix(config): reject external include mutations

* fix(config): bind snapshots to config paths

* fix(config): preserve write ownership

* fix(cli): preflight plugin config mutations

* chore(plugin-sdk): refresh api baseline

* test(config): prove install env policy mutations

* fix(cli): preflight plugin updates

* fix(cli): preflight non-npm id migrations

* chore(plugin-sdk): refresh api baseline

* fix(cli): satisfy plugin recovery checks
2026-06-15 23:07:29 +08:00
Ayaan Zaidi
c1219d161d fix(android): preserve history context usage (#92837) (thanks @Tosko4) 2026-06-15 19:55:17 +05:30
Ayaan Zaidi
636aab6891 refactor(android): distill session event refresh 2026-06-15 19:55:17 +05:30
Tosko4
8f9493c213 fix(android): clear stale context usage snapshots 2026-06-15 19:55:17 +05:30
Tosko4
826ea2bf85 fix(android): show live chat context usage 2026-06-15 19:55:17 +05:30
Peter Steinberger
0bbac63d00 fix(tasks): migrate legacy agent attribution 2026-06-15 07:09:01 -07:00
Peter Steinberger
1fef20c96b fix(tasks): preserve requester agent attribution 2026-06-15 07:09:01 -07:00
Alix-007
77d6ad6f65 fix(tasks): honor explicit agentId in gateway tasks.list and repair test helper signature
Two code-review findings. (1) gateway taskMatchesAgent fell through to a requester/owner/child session-key scan even when the task had an explicit agentId, so a worker subagent task owned by agent:main:main also matched agentId:main; make explicit task.agentId authoritative and keep the session-key fallback only for legacy records without an agentId, with a gateway tasks.list regression. (2) the cross-agent attribution test passed async (root) to the zero-arg withTaskRegistryTempDir helper (TS2345/TS7006); drop the unused parameter and redundant env assignment.
2026-06-15 07:09:01 -07:00
Alix-007
206ac169e6 fix: attribute spawned task runs to child agent 2026-06-15 07:09:01 -07:00
Peter Steinberger
66079161d7 fix(macos): route Peekaboo through app bridge 2026-06-15 09:14:16 -04:00
Momo
dd7f2ef002 Persist ClawHub skill install provenance (#93283)
Summary:
- The PR adds artifact, installed skill-file, source URL, and verification-envelope fields to ClawHub skill origin/lock metadata while keeping install telemetry restricted to the older version/registry shape.
- PR surface: Source +144, Tests +139. Total +283 across 2 files.
- Reproducibility: not applicable. as a bug reproduction. Source inspection shows current main lacks the richer `.clawhub` provenance fields, and the PR body provides after-patch live output from a ClawHub install.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Persist ClawHub skill install provenance

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

Prepared head SHA: 65774f4f4b
Review: https://github.com/openclaw/openclaw/pull/93283#issuecomment-4707787041

Co-authored-by: momothemage <niuzhengnan@163.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: momothemage
2026-06-15 13:04:08 +00:00
Alix-007
018d279468 fix(memory): clean rollback-journal reindex temp sidecar on NFS stores
cleanupAgedMemoryReindexTempFiles only removed WAL sidecars (-wal/-shm) of orphaned reindex temp DBs. On NFS-backed stores configureMemorySqliteWalMaintenance -> requireRollbackJournalMode forces journal_mode=DELETE, so the reindex temp DB uses a rollback journal; a hard crash leaves an orphaned .tmp-<uuid>-journal that leaked forever (cleanup neither deleted nor even discovered it). Add -journal to both the delete set (memoryIndexFileSuffixes) and the discovery set (reindexTempEntrySuffixes), with regression tests for the temp-plus-journal and stranded-journal cases.
2026-06-15 05:49:12 -07:00
Peter Steinberger
a09f6b1b27 fix(codex): preserve terminal outcome ordering (#93287) 2026-06-15 05:31:46 -07:00
Ayaan Zaidi
d1b33a6040 fix(telegram): recover pid-reused ingress claims 2026-06-15 17:31:03 +05:30
Alex Knight
8682d0701b perf(sessions): share one enumeration across archive retention sweeps (#91957)
Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
2026-06-15 21:11:50 +10:00
Mason Huang
4029fbd2b2 fix(status): avoid stale session context windows (#93220)
Summary:
- The PR filters stale session/live context-token values when rendering `/status`, threads existing per-agent/default context caps into status rendering, and adds regression tests for status message and summary output.
- PR surface: Source +107, Tests +155. Total +262 across 7 files.
- Reproducibility: yes. Source inspection shows current main forwards stale live and persisted context-token v ... atus`, and the PR comments include live gateway output validating the Kimi/DeepSeek mismatch after the fix.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(status): avoid stale session context windows

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

Prepared head SHA: 4a8e9299a3
Review: https://github.com/openclaw/openclaw/pull/93220#issuecomment-4705953238

Co-authored-by: masonxhuang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-15 11:08:48 +00:00
Sliverp
e7ee1c55b4 fix(qqbot): keep markdown table chunks valid (#92428)
* fix(qqbot): keep markdown table chunks valid

* fix(qqbot): keep markdown table chunks valid across message boundaries (#92428) (thanks @sliverp)

Co-authored-by: sliverp <870080352@qq.com>
2026-06-15 18:32:25 +08:00
Mason Huang
3ce3ed668d fix(status): correct pinned model clear hint (#93231)
Summary:
- This PR changes pinned-session `/status` guidance, model-selection docs, and status tests to recommend `/model default` instead of `/model <configured>` or `/reset` for clearing a session model pin.
- PR surface: Source 0, Tests 0, Docs +4. Total +4 across 7 files.
- Reproducibility: yes. from source inspection. Current main and v2026.6.6 emit the old `/reset` hint, while `/model default` clears persisted model overrides and `/reset` intentionally preserves user-selected overrides.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs: align model clear hint docs
- PR branch already contained follow-up commit before automerge: fix(status): correct pinned model clear hint

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

Prepared head SHA: 1181624daa
Review: https://github.com/openclaw/openclaw/pull/93231#issuecomment-4706327717

Co-authored-by: masonxhuang <masonxhuang@tencent.com>
Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-15 10:15:54 +00:00
Peter Steinberger
0314819f91 fix(agents): replace prose terminal classifiers (#93228)
* fix(agents): replace prose terminal classifiers

Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>

* fix(agents): preserve terminal failure lifecycles

* fix(agents): order parallel terminal summaries

* fix(agents): preserve structured post-tool silence

* fix(agents): preserve structured replay provenance

---------

Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
2026-06-15 02:53:14 -07:00
Alex Knight
eab22a911a fix(auto-reply): clear pending-final state before honoring post-send abort (#89115) (#93201)
In dispatchReplyFromConfig the user-message success branch ran
throwIfDispatchOperationAborted() *before* clearPendingFinalDeliveryAfterSuccess().
If stuck-session recovery aborted the run in the window between the final reply
shipping and the clear, the message was delivered but pendingFinalDelivery stayed
true forever — the get-reply redelivery short-circuit then silently blocked every
future inbound and the agent "went silent" (#89115).

Reorder so the durable pending-final bookkeeping is cleared first, then honor the
abort afterwards (preserving abort reporting). Also clear the stranded
pendingFinalDeliveryIntentId field — agent-command.ts already clears it but the
success helper did not.
2026-06-15 19:46:06 +10:00
Jason (Json)
773ffd87a1 fix(tui): keep parent stdin paused after exit (#93159)
Keep the setup TUI parent stdin paused after its inherited-stdio child exits so Docker and PTY setup parents terminate cleanly. Align pre/post setup terminal cleanup with the cleanup-then-exit contract and add lifecycle regression coverage.

Thanks @fuller-stack-dev.
2026-06-15 17:37:43 +08:00
Vincent Koc
3add8af427 docs(skills): post Discord announcements as user 2026-06-15 17:26:30 +08:00
Ben Badejo
3fc850fe86 fix(matrix): replace recovered command progress lines (#89920)
* fix(matrix): replace recovered command progress lines

* fix(matrix): replace recovered command progress lines

* fix(matrix): share command progress identity

* fix(channels): share command progress identity

* fix command progress draft replacement

* fix command progress ids without changing public line ids

* test(telegram): assert command progress preview update

* fix(telegram): keep progress preview test typed

---------

Co-authored-by: Benjamin Badejo <ben@benbadejo.com>
Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
2026-06-15 19:14:43 +10:00
zhang-guiping
ba1be23821 fix #69443: [Bug] Subagent RPC callback to WeChat session key routed to main session instead (#90231)
* fix agent session-key callback routing

* fix reset ack session-key delivery

* fix direct agent to session key routing

* fix lint in direct agent session routing

* fix: route subagent RPC callbacks to agent-shaped --to session key instead of main session (#90231) (thanks @zhangguiping-xydt)

---------

Co-authored-by: sliverp <870080352@qq.com>
2026-06-15 17:10:50 +08:00
Sash Zats
233b48daaa refactor: prune unused iOS code (#91996)
Prune unused iOS surfaces and regenerate the Xcode project. Add a scoped Periphery PR gate with hardened artifact handling and stale-status cleanup.

Co-authored-by: Sash Zats <sash@zats.io>
2026-06-15 02:07:15 -07:00
Ayaan Zaidi
501f63443f fix(openai): route spark through codex runtime 2026-06-15 14:17:15 +05:30
Ayaan Zaidi
dae2bcf31b style(openai): trim spark suppression comments 2026-06-15 14:17:15 +05:30
VACInc
aaa5ce6280 fix(openai): preserve catalog spark base urls 2026-06-15 14:17:15 +05:30
VACInc
6e8982f7a0 fix(openai): preserve suppression base urls 2026-06-15 14:17:15 +05:30
VACInc
8ea848acb0 docs(openai): clarify spark oauth support 2026-06-15 14:17:15 +05:30
VACInc
59e6452772 fix(openai): restore spark oauth routing 2026-06-15 14:17:15 +05:30
Vincent Koc
e573b751bf fix(cron): expose safe explicit delivery context 2026-06-15 16:45:58 +08:00
Peter Steinberger
d012d29e6f test(protocol): narrow literal union schemas 2026-06-15 04:26:00 -04:00
Jason O'Neal
44bf1c6d72 fix(voice-call): require realtime websocket path boundary
Tighten voice-call realtime WebSocket upgrade matching so configured stream paths match exactly or as slash-delimited children only.

Rejects same-prefix sibling paths that previously entered the realtime handler via a raw startsWith check. Preserves root stream-path child routing and adds bound VoiceCallWebhookServer regression coverage for valid child, sibling rejection, and root child behavior.

Verification:
- node scripts/run-vitest.mjs run extensions/voice-call/src/webhook.test.ts
- node scripts/run-vitest.mjs run extensions/voice-call/src/webhook.test.ts extensions/voice-call/src/webhook/realtime-handler.test.ts
- node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.extensions.json extensions/voice-call/src/webhook.ts extensions/voice-call/src/webhook.test.ts
- ./node_modules/.bin/oxfmt --check --threads=1 extensions/voice-call/src/webhook.ts extensions/voice-call/src/webhook.test.ts
- git diff --check
- GitHub PR checks passing
2026-06-15 18:21:22 +10:00
Peter Steinberger
7a7165ad22 fix(protocol): emit Swift enums for literal unions 2026-06-15 03:20:42 -04:00
Vincent Koc
928b5932a3 fix(agents): track normalized message target evidence 2026-06-15 15:10:20 +08:00
Jason (Json)
77a682c5de fix(agents): retry empty post-tool final turns (#93073)
Recover assistant turns that complete tool work without producing a visible final answer, while preserving intentional silent replies.

Use concrete tool-instance replay safety across embedded, Codex, and Copilot runtimes so unknown, mutating, async-started, and durable recall operations fail closed. Preserve genuine empty Codex final items without promoting commentary or tool-progress echoes.

Supersedes #90872. Thanks @fuller-stack-dev.

Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
2026-06-15 00:08:57 -07:00
Vincent Koc
efbefceb0e fix(cron): preserve explicit delivery and timeout semantics 2026-06-15 14:58:46 +08:00
Goutam Adwant
1c30bb8ce6 fix(telegram): preserve sticker media paths (#93130)
* fix(telegram): preserve sticker media paths

* fix(telegram): address PR validation failures

* fix(telegram): preserve sticker media context

* test(telegram): fix sticker proof checks

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-15 14:43:32 +08:00
Vincent Koc
94833b2c90 fix(release): support ClawHub-only runtime builds 2026-06-15 14:23:57 +08:00
Vincent Koc
1057e74438 fix(e2e): resolve macOS Parallels VM
(cherry picked from commit a231ab8acf)
2026-06-15 14:23:57 +08:00
Vincent Koc
55a6d8c57d fix(e2e): resume restored Parallels snapshots
(cherry picked from commit a7e0822a1a)
2026-06-15 14:23:57 +08:00
Vincent Koc
b8967fc877 fix(docker): seed prune store from lockfile
(cherry picked from commit 47ec5be9ef)
2026-06-15 14:23:57 +08:00
Vincent Koc
42759a1b79 fix(telegram): repair rich message typecheck 2026-06-15 14:23:57 +08:00
Vincent Koc
5b18b7560e fix(release): harden plugin package preflight 2026-06-15 14:23:57 +08:00
dongdong
bcb016a528 fix: accept mixed source/dist bundled roots (#93119)
* fix: accept mixed source/dist bundled roots fixes #87730

* fix(plugins): validate mixed bundled roots per plugin

* fix(plugins): preserve active source overlays

---------

Co-authored-by: Jasmine Zhang <jasminezhang@JasminedeMac-mini.local>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-15 14:00:48 +08:00
Ayaan Zaidi
b3f315461b fix(telegram): preserve rich markdown line breaks 2026-06-15 11:07:15 +05:30
Vincent Koc
5b460c4669 fix(status): preserve rich message line breaks 2026-06-15 11:07:15 +05:30
Dallin Romney
1f8c4d3958 simplify QA evidence profile and mappings/coverage shape (#93153)
* test(qa): simplify evidence coverage shape

* test(qa): collapse evidence scorecard metadata

* test(qa): document evidence schema version
2026-06-14 22:26:58 -07:00
mushuiyu_xydt
04875efd28 fix(memory-core): vary dream diary recall snippets (#91225)
Prevent repeated first-day Dream Diary narratives by prioritizing fresh recall snippets across the bounded short-term store and adding recent diary context to narrative generation. Keep diary reads best-effort and reject symlink/non-file inputs.

Fixes #83830.

Thanks @mushuiyu886.

Co-authored-by: 杨浩宇0668001029 <yang.haoyu@xydigit.com>
2026-06-14 22:02:06 -07:00
liuhao1024
7e0128ae65 fix(agents): preserve literal current session resolution (#93138)
* fix(agents): resolve "current" session alias locally without gateway round-trip

The system prompt tells agents to use sessionKey="current" to refer to
their own session.  Previously, resolveSessionReference sent the literal
string "current" to the gateway sessions.resolve action, which rejected
it with INVALID_REQUEST and logged a noisy error line on every tool call.
The wrapper fell back to requesterInternalKey and succeeded, so the tool
worked — but the gateway error was spurious.

Add "current" to the well-known client alias check in
resolveCurrentSessionClientAlias so it is resolved locally to the
requester's session key, matching how TUI/CLI/WebChat client labels are
handled.  This eliminates the unnecessary gateway round-trip and the
error log line.

Fixes #78424

* test: update session_status tests for local current-key resolution

* test: update session_status tests for local current-key resolution

* Revert "test: update session_status tests for local current-key resolution"

This reverts commit d9f6c8b5248921c99f43dc222667ffa429b34401.

* Revert "test: update session_status tests for local current-key resolution"

This reverts commit 40bf77d06711833c1beaeedf562b60a765a559d6.

* Revert "fix(agents): resolve "current" session alias locally without gateway round-trip"

This reverts commit d92bc9b91e0840ea5823cd44223c139e434c5ec4.

* fix(agents): preserve literal current session resolution

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-15 12:32:15 +08:00
liuhao1024
1db8ab3734 fix(feishu): pass card_msg_content_type to get full card content (fixes #78289) (#93134)
* fix(feishu): pass card_msg_content_type to get full card content

When reading Feishu interactive card messages via getMessageFeishu,
the API returns a degraded structure (title + 'upgrade client' prompt)
unless card_msg_content_type=user_card_content is passed in params.

Fixes #78289

* fix(feishu): request full card content for message reads

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-15 12:04:55 +08:00
Omar Shahine
cc954798f2 fix(imessage): honor disabled reply actions (#93137)
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
2026-06-15 11:53:35 +08:00
Mason Huang
4fc805320f fix(cron): require explicit message target proof (#92318)
Summary:
- Merged fix(cron): require explicit message target proof after ClawSweeper review.

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

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

Prepared head SHA: 2aff537f9f
Review: https://github.com/openclaw/openclaw/pull/92318#issuecomment-4704342205

Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-15 03:52:05 +00:00
Mason Huang
06431fd99b test: add temp directory helper guidance (#87298)
Summary:
- Merged test: add temp directory helper guidance after ClawSweeper review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(scripts): honor temp report failure mode
- PR branch already contained follow-up commit before automerge: fix(scripts): reduce temp report noise
- PR branch already contained follow-up commit before automerge: fix(scripts): cover test support temp reports
- PR branch already contained follow-up commit before automerge: fix(scripts): report temp use in test helpers
- PR branch already contained follow-up commit before automerge: fix(scripts): broaden temp report test surface
- PR branch already contained follow-up commit before automerge: fix(scripts): cover nested test temp reports

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

Prepared head SHA: 132f14a381
Review: https://github.com/openclaw/openclaw/pull/87298#issuecomment-4704338581

Co-authored-by: masonxhuang <masonxhuang@tencent.com>
Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-15 03:51:43 +00:00
Dallin Romney
3d38c9a633 test(qa): embed profile scorecard evidence (#93109)
* test(qa): embed profile scorecard evidence

* test(qa): fix profile runner return lint

* test(qa): satisfy suite command lint return
2026-06-14 20:51:38 -07:00
Ayaan Zaidi
663fabbe30 fix(telegram): render progress drafts as rich previews 2026-06-15 08:52:27 +05:30
Peter Lindsey
c847db550f fix(telegram): keep streamed tool-progress lines on separate lines
Telegram's rich-markdown renderer treats a lone "\n" as a soft break
(rendered as a space), so streamed tool-progress draft lines joined by a
single newline collapsed onto one line. Pass "\n\n" as the progress-draft
line separator for Telegram; it renders a blank line as a single break, so
each tool/thinking/commentary line gets its own line again. Other channels
keep the single-newline default, so Discord and the rest are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 08:52:27 +05:30
Galin Iliev
50c82b3020 fix(scripts): add database-first legacy store guard
Adds a required database-first legacy-store guard and regression coverage for legacy runtime state write patterns.

The guard is wired into architecture/preflight/changed checks, narrows the documented guard contract to the implemented filesystem-write scope, and tightens extension migration exemptions to explicit owner APIs. Also includes a small memory-core lint unblocker after current CI flagged an unnecessary non-null assertion.

Verification:
- pnpm check:database-first-legacy-stores
- pnpm lint:scripts
- node scripts/run-vitest.mjs test/scripts/check-database-first-legacy-stores.test.ts -- --reporter=verbose
- node scripts/run-oxlint.mjs extensions/memory-core/src/memory/manager-embedding-ops.ts
- git diff --check
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- GitHub CI green for PR head 34dde2c620

Closes #91628.
2026-06-14 20:08:06 -07:00
Vincent Koc
dc46a67e80 fix(memory-core): remove redundant provider key assertion 2026-06-15 11:05:07 +08:00
Marcus Castro
7d8b000bf7 fix(whatsapp): bound socket operations (#93094)
* fix(whatsapp): bound socket operations

* test(whatsapp): type monitor fixture config

* fix(whatsapp): align socket timeout semantics

* test(whatsapp): cover socket timeout edge cases

* test(whatsapp): shrink socket timeout coverage

* refactor(whatsapp): simplify socket timeout boundary

* fix(whatsapp): keep send api socket type structural
2026-06-15 00:04:11 -03:00
clawsweeper[bot]
ac1042b09b fix(voice-call): preserve live Twilio streams in stale reaper (#90812)
Summary:
- The PR updates the voice-call plugin to preserve live `speaking`/`listening` calls without `answeredAt`, backfill max-duration enforcement for live/restored call paths, and add regression tests.
- PR surface: Source +90, Tests +223. Total +313 across 9 files.
- Reproducibility: yes. source-level: current main and v2026.6.6 still reap aged non-terminal calls solely bec ... king` or `listening` without setting it. I did not run a live Twilio carrier call in this read-only review.

Automerge notes:
- Ran the ClawSweeper repair loop before final review.
- Included post-review commit in the final squash: fix(voice-call): preserve live Twilio streams in stale reaper
- Included post-review commit in the final squash: fix(clawsweeper): address review for automerge-openclaw-openclaw-9062…

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

Prepared head SHA: 5fee2ff7a1
Review: https://github.com/openclaw/openclaw/pull/90812#issuecomment-4637047870

Co-authored-by: Sahibzada Allahyar <sahibzada@fastino.ai>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-06-15 02:39:29 +00:00
Chunyue Wang
fd80e0dd6b fix(agents): do not misclassify client-disconnect abort as run timeout (#90936)
Summary:
- The PR adds an abort-signal-specific timeout classifier, switches two embedded attempt abort handlers to it, and adds focused failover tests.
- PR surface: Source +5, Tests +32. Total +37 across 3 files.
- Reproducibility: yes. from source inspection and a focused Node abort-reason check, but not from a live 180- ... ault AbortController abort reason through the broad timeout classifier used by the embedded abort handlers.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): do not misclassify client-disconnect abort as run timeout

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

Prepared head SHA: 2708b0a37d
Review: https://github.com/openclaw/openclaw/pull/90936#issuecomment-4638919394

Co-authored-by: openperf <16864032@qq.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-06-15 02:38:47 +00:00
mushuiyu_xydt
44e6caff54 fix(memory): accept local default model path migration (#92954)
* fix(memory): accept local default model path migration

Treat the official local default embedding model's hf URI and downloaded GGUF path identities as equivalent so upgraded local memory indexes do not pause solely on path-format changes.

* fix(memory): satisfy local identity lint

Avoid filtered array tail access in the local model filename helper while preserving the same compatibility behavior.

* fix(memory): preserve local embedding identity aliases

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-15 09:29:42 +08:00
Dallin Romney
d1eda0bd6f test(reply): preserve telegram dedupe fallback (#93107) 2026-06-14 18:25:40 -07:00
Dallin Romney
cae64ee360 fix(reply): avoid bundled channel load in dedupe (#93104) 2026-06-14 18:09:26 -07:00
Dallin Romney
e8db9c3bc0 test(qa): add qa run --profile and unified output summary/evidence (#91587)
* test(qa): add mapped qa run profiles

* test(qa): document mapped profile runner

* test(qa): validate run profiles from mapping

* test(qa): preserve root profile parsing

* test(qa): simplify taxonomy profile dispatch

* test(qa): align tool coverage CLI expectation

* test(qa): fix profile dispatch fixture type

* test(qa): share profile runner option types

* test(qa): split shared cli runner options

* test(qa): unify profile suite artifacts

* fix(qa): filter profile scenarios by provider lane

* test(qa): drop native scenario subreports

* fix(qa): keep native log refs repo-relative

* fix(cli): preserve qa run root profile parsing

* fix(qa): avoid qa profile flag collision

* fix(qa): reject profile flags without qa profile
2026-06-14 18:08:42 -07:00
Kevin Lin
e82d19fb06 feat(codex): add auto plugin approvals (#92625)
* feat(codex): add on-request plugin approvals

* feat(codex): rename plugin approval policy to auto

* fix(codex): update binding schema version callers
2026-06-14 18:00:38 -07:00
aliahnaf2013-max
870ec6dee2 fix(codex): close one-shot app server clients
Fix gateway-routed one-shot Codex app-server teardown so owned shared clients are retired after run cleanup. Verified with focused tests, Showboat proof, and green PR CI.
2026-06-14 17:55:31 -07:00
Dallin Romney
fef8394079 Convert QA scenarios to YAML files (#92915)
* refactor: load QA scenarios from YAML

* docs: update personal QA scenario docs

* test: keep QA scenarios YAML-only
2026-06-14 17:31:18 -07:00
Vincent Koc
1ca3d4f586 test(reply): default followup enqueue mock to accepted 2026-06-15 08:21:49 +08:00
Yuval Dinodia
02acb0bd74 fix(gateway): accept file-only input on /v1/responses (parity with image-only) (#93011)
* fix(gateway): accept file-only input on /v1/responses (parity with image-only)

* fix(gateway): preserve file-only model context

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-15 08:01:19 +08:00
zhang-guiping
a1f18ef46e fix #90333: [Bug]: Discord image build aborts at step 66 — openclaw-build-messaging-plugins.py exits 1 (#92869)
* fix(cli): recover versioned Discord plugin installs

* fix(cli): harden plugin install recovery

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-15 07:58:22 +08:00
openclaw-clownfish[bot]
9ba6ed1d5c fix(agents): preserve fresh usage after compaction (#93084)
* fix(compaction): preserve fresh usage after compaction

Co-authored-by: HollyChou <128659251+Hollychou924@users.noreply.github.com>

Co-authored-by: 吴杨帆 <39647285+leno23@users.noreply.github.com>

* fix(compaction): satisfy stale usage timestamp narrowing

* fix(clownfish): address review for ghcrawl-156678-autonomous-smoke (1)

Co-authored-by: HollyChou <128659251+Hollychou924@users.noreply.github.com>

Co-authored-by: de1ty <7804799+de1tydev@users.noreply.github.com>

Co-authored-by: 吴杨帆 <39647285+leno23@users.noreply.github.com>

Co-authored-by: Zhao Shiqi <109639815+425072024@users.noreply.github.com>

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: 吴杨帆 <39647285+leno23@users.noreply.github.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: Zhao Shiqi <109639815+425072024@users.noreply.github.com>
2026-06-15 07:33:45 +08:00
Josh Lehman
f1b8827d20 refactor: route bundled plugin session callers through seam (#89129)
Merged via squash.

Prepared head SHA: 7975bc06ac
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-06-14 12:26:53 -07:00
kumaxs
356385045f fix(opencode-go): warm context metadata from provider catalog (#92913)
Register OpenCode Go's provider-owned static catalog so lifecycle cache warmup supplies the correct context window to memory flush and compaction without persisting catalog rows in user config.

Fixes #92912.

Co-authored-by: kumaxs <45620232+kumaxs@users.noreply.github.com>
2026-06-14 12:08:49 -07:00
Sam
a189baa4b3 fix(agents): tolerate missing attribution baseUrl (#92991)
Avoid assuming every runtime model exposes a string `baseUrl` before provider attribution checks. Preserve OpenRouter and Cloudflare attribution behavior while allowing Bedrock session setup to reach provider routing.

Fixes #92974.

Co-authored-by: Sami Rusani <sr@samirusani>
2026-06-14 11:43:10 -07:00
zengLingbiao
07dfdd4bd0 fix(agents): prevent duplicate before-tool-call hooks (#93009)
Prevent duplicate `before_tool_call` execution when an already wrapped tool passes through schema normalization and coding-tool assembly. Preserve the normalized schema while replacing stale wrapper context with the current agent/session/run context.

Fixes #92973.

Co-authored-by: zengLingbiao <zeng.lingbiao@xydigit.com>
2026-06-14 11:25:43 -07:00
Shakker
96f786d4b1 fix: route oauth fallback env setup 2026-06-14 19:21:40 +01:00
Shakker
0100c27bc0 test: route oauth helper env setup 2026-06-14 19:19:04 +01:00
Shakker
a8ca9619a5 fix: route oauth lock env setup 2026-06-14 19:17:22 +01:00
Shakker
3a8d422000 test: scope tilde home env overrides 2026-06-14 19:15:19 +01:00
Shakker
fc3e00cc11 fix: route acp harness env restore 2026-06-14 19:13:12 +01:00
Shakker
a1b7f3570c test: centralize session file env restore 2026-06-14 19:11:31 +01:00
Shakker
b8ed2c3280 fix: restore acp task env scope 2026-06-14 19:06:25 +01:00
Shakker
8bc728307d test: route wsl env mutations 2026-06-14 19:03:47 +01:00
OfflynAI
81924cfd5e fix(ollama): preserve length-limited responses (#89160)
Preserve Ollama length termination and surface incomplete token-limited embedded-agent turns without discarding durable tool output.

Fixes #89051.

Co-authored-by: joelnishanth <140015627+joelnishanth@users.noreply.github.com>
2026-06-14 10:59:37 -07:00
openclaw-clownfish[bot]
6134c00657 fix(telegram): cool down transient sendChatAction failures (#93020)
* fix(telegram): cool down transient sendChatAction failures

Co-authored-by: Langning Zhang <145515129+Boulea7@users.noreply.github.com>

Co-authored-by: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com>

Co-authored-by: pick-cat <266665499+Pick-cat@users.noreply.github.com>

* fix(clownfish): address review for ghcrawl-156876-autonomous-smoke (1)

Co-authored-by: Langning Zhang <145515129+Boulea7@users.noreply.github.com>

Co-authored-by: Sumaia Zaman <45918347+sumaiazaman@users.noreply.github.com>

Co-authored-by: pick-cat <266665499+Pick-cat@users.noreply.github.com>

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: pick-cat <266665499+Pick-cat@users.noreply.github.com>
2026-06-15 01:07:10 +08:00
openclaw-clownfish[bot]
4c2fef4a3b fix(gateway): repair usage cost aggregation across agents (#93022)
* fix(gateway): aggregate usage cost across agents

* fix(clownfish): address review for ghcrawl-157040-autonomous-smoke (1)

Co-authored-by: luke-skywalker-open-claw <262978557+luke-skywalker-open-claw@users.noreply.github.com>

Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
2026-06-15 01:06:36 +08:00
Vincent Koc
b470316fc0 fix(state): harden sqlite path caching
Resolve explicit relative SQLite DB paths before caching handles and centralize durable SQLite connection pragmas so busy_timeout is applied before WAL/NFS negotiation.
2026-06-15 01:04:35 +08:00
Sally O'Malley
7e12a3326d feat(openrouter): surface Fusion panel config (#93005)
Signed-off-by: sallyom <somalley@redhat.com>
2026-06-14 12:44:49 -04:00
ZengWen-DT
a42bda5b37 fix(memory): clean stale reindex temp files (#92891)
* fix(memory): clean stale reindex temp files

* fix(memory): harden stale reindex cleanup

* fix(memory): serialize safe reindex cleanup

* fix(memory): satisfy reindex lock lint

---------

Co-authored-by: zengwen <zeng_wen@foxmail.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-15 00:40:43 +08:00
NVIDIAN
ecaebfc51b fix(agents): retry thinking-only errored turns (#92191)
Retry replay-safe reasoning-only provider errors before assistant failover while preserving classified fallback and terminal-output ownership. Adds deterministic Anthropic gateway fault-injection coverage and focused regression tests.\n\nCo-authored-by: ai-hpc <mail.speedy.hpc@hotmail.com>
2026-06-14 09:39:27 -07:00
Ciward
364461949d fix: refresh slash command routing config (#39617)
Use the active runtime snapshot for Discord and Slack native command routing and Discord autocomplete after config hot writes.

Fixes #39605

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-14 09:32:18 -07:00
Vincent Koc
10b0dea77a test(gateway): retry chat temp cleanup 2026-06-15 00:28:58 +08:00
Martin Kessler
aefd49909d fix(skills): keep managed prompt paths readable (#92894)
* fix(skills): keep managed prompt paths readable

* fix(skills): preserve only managed skill prompt paths

* chore: refresh proof gate

* fix(skills): refresh managed prompt snapshots

* fix(skills): preserve plugin prompt paths in state roots

* test(skills): use generic state-root path names

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

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: sallyom <somalley@redhat.com>
2026-06-14 12:19:08 -04:00
sandieman2
c67dc59b02 fix(reply): deliver final reply when queued follow-up claims session; scope dedupe to routed thread (#90943)
* fix(reply): deliver final reply when queued follow-up claims session; scope dedupe to routed thread

Two core bugs caused composed replies to be silently dropped (no delivery,
no error) when a second message arrived in the same thread mid-run:

1. dispatch-from-config: ensureDispatchReplyOperation only kept the
   dispatch-owned operation authoritative while it had no result. Once
   runReplyAgent completed the operation to drain queued follow-ups, a
   second same-thread inbound could claim the session and the first final
   reply would try to re-acquire the lane instead of finishing delivery,
   deadlocking behind the queued work. Keep the dispatch-owned operation
   authoritative through final delivery.

2. reply-payloads-dedupe: messaging-tool reply dedupe compared only the
   channel target, not the routed thread, so a send in one thread could
   suppress a later reply in a different thread. Thread the routed thread
   id through buildReplyPayloads + follow-up delivery and only fall back to
   channel-only matching for providers without a thread-aware suppression
   matcher when neither side carries thread evidence.

Adds regression tests; existing Telegram topic-suppression behavior is
preserved by gating the thread guard to providers lacking a plugin matcher.

* fix(reply): preserve threaded message delivery evidence

* fix(reply): dedupe final payloads by delivery route

* fix(slack): preserve native send thread evidence

* fix(reply): preserve explicit reply thread evidence

* fix(reply): align explicit reply route dedupe

* fix(reply): preserve delivery lane through final dispatch

* fix(mattermost): preserve threaded tool send routes

* chore(plugin-sdk): refresh API baseline

* fix(reply): align final delivery route dedupe

* fix(reply): gate followups on final delivery

* fix(reply): keep send receipts private

* fix(reply): infer implicit message provider

* fix(reply): align routed threading policy

* fix(reply): preserve queued delivery context

* fix(reply): hydrate queued system event routes

* fix(reply): hydrate queued execution routes

* fix(reply): scope final delivery barriers

* fix(slack): preserve DM target aliases

* fix(reply): mirror resolved source thread routes

* fix(mattermost): retain delayed delivery barrier

* fix(codex): separate message routing from tool policy

* fix(reply): consume normalized Slack DM targets once

* fix(slack): remove stale target alias

* style(reply): satisfy changed lint gates

* fix(mattermost): preserve explicit reply targets

* test: align Slack reply branch checks

* fix(reply): persist overflow summaries to admitted session

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-14 09:11:05 -07:00
Josh Lehman
e1744184b8 refactor: route command session reads through seam (#89122)
Merged via squash.

Prepared head SHA: 73218cc7cd
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-06-14 09:01:17 -07:00
Vincent Koc
2e240e772b test(gateway): expect trajectory session branch bundle 2026-06-14 23:34:08 +08:00
Vincent Koc
5836982506 test(gateway): allow optional trajectory metadata bundle 2026-06-14 23:24:55 +08:00
Vincent Koc
40093f5a93 fix(gateway): track effective watcher polling mode 2026-06-14 23:19:54 +08:00
zhang-guiping
d0851435e8 fix #86872: Subagent run reports success but fails to write output file (#92642)
* fix(codex): wait for native subagent completion

Codex native subagent lifecycle status is only a progress signal; the task row should not report success until the transcript or native completion result is available.

* fix(codex): preserve later native subagent failures

* test(codex): freeze authoritative subagent results

* fix(codex): preserve remote V1 completion fallback

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-14 23:18:57 +08:00
Vincent Koc
3826cda4d8 fix(gateway): run export helpers through cli entry 2026-06-14 23:08:19 +08:00
Vortex Openclaw
771881d189 fix(elevenlabs): use current TTS model ids (#92904)
* fix(elevenlabs): use current TTS model ids

* fix(elevenlabs): preserve served legacy model choices

---------

Co-authored-by: Ariel Bravy <ariel@vortexradar.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-14 23:01:27 +08:00
ZengWen-DT
6db496b04b fix(tui): keep spinner active when toggling tools (#92909)
* fix(tui): keep spinner active when toggling tools

* fix(tui): preserve finishing status when toggling tools

---------

Co-authored-by: zengwen <zeng_wen@foxmail.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-14 22:59:29 +08:00
iloveleon19
4892bbc10f fix(mattermost): merge progress preview lines by identity (#91331)
* fix(mattermost): merge progress preview lines by identity

* fix(mattermost): preserve progress across assistant boundaries

* fix(mattermost): compose reasoning with progress previews

* fix(channels): reset reasoning progress at block boundaries

* fix(channels): align tool progress line identities

* fix(channels): keep tool call identity mapping injective

---------

Co-authored-by: leon <leon@gmail.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-14 22:58:54 +08:00
Yuval Dinodia
7f6df80537 fix(google): route Gemini CLI OAuth through the env proxy (#46184) (#92815) 2026-06-14 22:52:18 +08:00
이민재
1a2e418500 fix: use passive periodic sqlite wal checkpoints
Use PASSIVE for periodic SQLite WAL checkpoints while keeping explicit checkpoint() and close() on TRUNCATE by default.

Preserve the old interval export as a compatibility alias, add the neutral interval export, and update the task storage docs contract.

Fixes #81715.
2026-06-14 22:46:15 +08:00
liuhao1024
a823cc3b1c docs(browser-control): document OPENCLAW_EAGER_BROWSER_CONTROL_SERVER requirement (#92845)
The standalone loopback HTTP API only starts when
OPENCLAW_EAGER_BROWSER_CONTROL_SERVER=1 is set in the gateway
service environment. Without it, browser control works via CLI and
agent tools but nothing listens on the loopback control port.

Fixes #92841
2026-06-14 22:43:26 +08:00
Colin Johnson
fd855c831f feat(webui): add session workspace rail (#92856)
* feat(webui): add session workspace rail

* fix(webui): address session workspace review

* fix(webui): secure session workspace previews

* fix(webui): handle nested session workspace paths

* fix(webui): update session file protocol models

* fix(webui): clear session rail lint
2026-06-14 22:39:51 +08:00
Vincent Koc
ccf5976d06 fix(gateway): async trajectory export approvals 2026-06-14 22:25:47 +08:00
abel-zer0
b5999bc6a0 fix(agents): drop incomplete reasoning replay turns (#88656)
Drop assistant replay turns that ended at the token limit with only incomplete hidden reasoning while preserving visible text, tool calls, empty turns, and unknown content shapes. Apply the same classification to embedded replay and public transport transforms, with focused regression, live OpenAI/Anthropic provider proof, docs, autoreview, Testbox, and green CI.

Co-authored-by: clawstation <abel@stationzero.ai>
2026-06-14 07:02:03 -07:00
Vincent Koc
fc6d448138 fix(ci): skip session accessor guard for older targets 2026-06-14 21:35:37 +08:00
Vincent Koc
2e745ba225 fix(webchat): route trajectory slash export before agent dispatch 2026-06-14 21:22:40 +08:00
Josh Lehman
ef47dd610c refactor: add session accessor seam with gateway consumer (#90463)
Merged via squash.

Prepared head SHA: 58aa59eaf8
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-06-14 06:18:47 -07:00
mmyzwl
fbc3fa3876 fix(agents): recover genericized Anthropic thinking errors (#92916)
Recover invalid Anthropic thinking replays when provider details survive genericization in SDK, failover, cause-chain, or terminal stream error fields.

The recovery matcher now uses cycle-safe named error carriers, avoids scanning assistant content and tool arguments, and retains one retry per provider call. Focused regressions cover each carrier, cyclic causes, terminal errors, and false-positive payload text.

Addresses the recovery path in #92201. The separate root cause that creates or persists invalid signatures remains open for investigation.

Co-authored-by: wlzeng0668001202 <ceng.wenlong@xydigit.com>
2026-06-14 06:08:24 -07:00
Vincent Koc
d28691da97 fix(release): preserve child release check refs 2026-06-14 20:55:05 +08:00
Ayaan Zaidi
65e6d9c98c fix(auto-reply): strip delivery hints from leading metadata 2026-06-14 18:18:20 +05:30
Ayaan Zaidi
d498b1cce4 fix(plugin-sdk): expose delivery hints without utility imports 2026-06-14 18:18:20 +05:30
Ayaan Zaidi
210877a73e fix(auto-reply): share message-tool delivery hints 2026-06-14 18:18:20 +05:30
Ayaan Zaidi
d5b1d4529f test(auto-reply): trim duplicate progress assertion 2026-06-14 18:18:20 +05:30
Forge
36f6008842 test(auto-reply): assert allowed suppressed progress gating 2026-06-14 18:18:20 +05:30
Forge
cf88b4c024 fix(auto-reply): align message-tool progress gating 2026-06-14 18:18:20 +05:30
ragesaq
a15427d605 fix(auto-reply): deliver channel message-tool final replies
Clarify that interim assistant text remains visible under message_tool_only delivery while the final answer must use the message tool, and forward progress for channel message-tool turns once the message tool has delivered the final reply.

Co-authored-by: Forge <forge@psiclawops.dev>

Co-authored-by: Chisel <chisel@psiclawops.dev>
2026-06-14 18:18:20 +05:30
David
23d74dad12 fix(lmstudio): honor thinking off for binary reasoning models (#92002)
Scope disabled-thinking payload repair to LM Studio's lightweight provider stream hook. Preserve official OpenAI and Anthropic tool-calling paths.

Co-authored-by: David <32288+nxmxbbd@users.noreply.github.com>
2026-06-14 05:41:49 -07:00
Vincent Koc
ae8da992ce test(gateway): unblock trajectory export live release gate 2026-06-14 20:41:34 +08:00
Stellar鱼
4644e0c102 fix(memory-wiki): tolerate artifacts without agent ids
Fixes #92207.

Normalize public memory artifacts at the memory host boundary so providers that omit agentIds produce an empty list instead of throwing during artifact cloning, sorting, or memory-wiki bridge import. The bridge now renders those artifacts with unknown agents while downstream consumers still receive stable array-shaped metadata.

Verification:
- node scripts/run-vitest.mjs src/plugins/memory-state.test.ts extensions/memory-wiki/src/bridge.test.ts --maxWorkers=1
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- Crabbox run_2a30de5d0a00 / cbx_3684cb0b7ea5: OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed
- GitHub PR checks clean on 19678ed60f
2026-06-14 20:19:24 +08:00
JC
fff193402e fix(gateway): build row metadata for single session lists
Refs #92057.

Build the request-scoped row metadata context for every non-empty sessions.list result, including limit=1, so single-row lists use the shared subagent metadata read index instead of direct per-row registry snapshot lookups. This keeps the existing single-row store child-session candidate optimization intact while removing the single-row metadata-cache gap.

Verification:
- node scripts/run-vitest.mjs src/gateway/session-utils.single-row-cache.test.ts --maxWorkers=1
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- Crabbox run_f89b56ffea83 / cbx_f1b1f5013225: OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed
- GitHub PR checks clean on 1ba6619f2e
2026-06-14 19:50:30 +08:00
danbao
c78f9376d9 fix(gateway): degrade config watcher to polling
Fixes #92851.

When native filesystem watching exhausts its retry budget, the gateway config reloader now falls back to polling instead of disabling hot reload for the rest of the process. The watcher state tracks the effective Chokidar polling mode, including CHOKIDAR_USEPOLLING overrides, so forced polling avoids a redundant native phase and forced native mode reports an accurate native-mode disable.

Verification:
- node scripts/run-vitest.mjs src/gateway/config-reload.test.ts --maxWorkers=1
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- Testbox-through-Crabbox tbx_01kv2xvbqkv4dmvvvsswzm75hz: OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed
- GitHub PR checks clean on c9762c5159
2026-06-14 19:29:29 +08:00
Peter Steinberger
d4a74b4993 fix(subagents): handle configured bare model provenance 2026-06-14 04:29:22 -07:00
Jasmine Zhang
aa04aef629 fix(subagents): preserve config-selected child model overrides 2026-06-14 04:29:22 -07:00
Vincent Koc
0716ebc1e5 test(openai): extend live STT fixture timeout 2026-06-14 19:21:25 +08:00
Chunyue Wang
cda040b4e5 fix(agents): clamp subagent spawn thinking overrides
Fixes #92412.

Subagent spawns that request an unsupported explicit thinking level now clamp through the existing provider/model thinking fallback instead of hard-failing after the orchestrator has already received an accepted ack. The exception is limited to trusted subagent spawn runs by requiring both the subagent lane and a subagent-shaped session key, so interactive and non-subagent explicit `--thinking` validation still fails loudly.

Verification:
- `node scripts/run-vitest.mjs src/agents/agent-command.live-model-switch.test.ts --maxWorkers=1`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- Testbox-through-Crabbox `tbx_01kv2wt0nqavsmnvzzzy2antrc`: `OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed`
- GitHub PR checks clean on `c71186863337d9dfb9a18e5349ebef634a7d5ccd`
2026-06-14 19:11:16 +08:00
Vincent Koc
00479b12d1 test(gateway): authorize trajectory export live command 2026-06-14 18:57:39 +08:00
Peter Steinberger
2a6eeceb40 fix(openai): recover invalid reasoning signatures (#92941) 2026-06-14 03:51:16 -07:00
Vincent Koc
30f5e6f639 test(installer): stabilize npm prefix probe test 2026-06-14 18:48:11 +08:00
Vincent Koc
dae9345407 test(gateway): isolate trajectory export live seed turn 2026-06-14 18:31:48 +08:00
Thomas Krohnfuß
a12942518e fix(openai): omit gpt-5.5 tool reasoning effort (#90574)
Fix GPT-5.5 Chat Completions tool requests by omitting the incompatible reasoning effort only on verified OpenAI and Azure routes. Preserve no-tool requests and nonblank custom OpenAI-compatible providers; add official regional endpoint metadata plus OpenAI and Anthropic live regression proof.

Co-authored-by: Thomas Krohnfuß <thomas.krohnfuss@stud.th-luebeck.de>
2026-06-14 03:28:54 -07:00
ghitafilali
b2c5e790b4 fix: restart gateway after isolated cron setup timeout
Restart the gateway after isolated cron setup timeouts and harden stale cron-task finalization around restart/reload boundaries.

Verification: focused cron/tasks/CLI regression suites, gateway filesystem/session regression suite, test typecheck, core lint shards, git diff --check, autoreview, Blacksmith Testbox changed gate tbx_01kv2srgbex71w9ce5rwv2wtr4, and clean GitHub PR checks on 13d06b5d6f.
2026-06-14 18:26:27 +08:00
Vincent Koc
37e3e895b0 test(release): unblock beta validation checks 2026-06-14 18:02:38 +08:00
Vincent Koc
2f34d06b42 fix(release): harden beta validation gates
(cherry picked from commit 91eeda0d708c2d8dac7c09c259b7cf390193f83f)
2026-06-14 17:33:48 +08:00
liuhao1024
db5e415888 fix(feishu): re-resolve route when dynamic agent binding already exists in runtime config (fixes #42837) (#92814)
* fix(feishu): re-resolve route when dynamic agent binding already exists in runtime config

When dynamicAgentCreation is enabled and a binding was previously written
to the config file (e.g. from a prior message), the in-memory cfg may be
stale and not contain the binding. Previously, maybeCreateDynamicAgent
returned { created: false, updatedCfg: cfg } with the stale cfg, and
bot.ts only re-resolved the route when created === true. This caused
subsequent messages to still route to agent:main.

Fix: check runtime.config.current() for the binding when it is missing
from the in-memory cfg. When found, return the runtime's current config
so the caller can re-resolve the route with up-to-date bindings.

Fixes #42837

* fix(feishu): serialize dynamic agent config updates

* fix(feishu): route with refreshed runtime config

* fix(feishu): use current dynamic-agent policy

* fix(feishu): reauthorize refreshed dynamic routes

* fix(feishu): authorize dynamic agent mutations

* fix(feishu): complete account-scoped dynamic routing

* fix(feishu): revalidate current direct routes

* fix(feishu): isolate named-account dynamic agents

* fix(feishu): bound named dynamic agent ids

* docs(feishu): explain legacy dynamic agent cap

* test(feishu): fix dynamic routing check types

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-14 17:28:54 +08:00
openclaw-clownfish[bot]
b9e8e6d66e fix(sessions): restore reset archive fallback reads
Fall back to valid reset transcript archives when active async session transcripts are missing, while keeping active transcript priority and choosing the newest valid archive across roots.

Validation:

- node scripts/run-vitest.mjs src/gateway/session-utils.fs.test.ts src/gateway/sessions-history-http.test.ts src/gateway/sessions-history-http.revocation.test.ts src/gateway/session-history-state.test.ts src/gateway/server.chat.gateway-server-chat-b.test.ts src/gateway/managed-image-attachments.test.ts src/agents/tools/embedded-gateway-stub.test.ts src/tui/embedded-backend.test.ts

- node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.src.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-src-pr92879.tsbuildinfo

- git diff --check origin/main...HEAD && git diff --check

- autoreview --mode branch --base origin/main: clean

Direct-landed from #92879 because the source branch has maintainer edits disabled and the landed diff needed maintainer repair before merge.

Co-authored-by: Masato Hoshino <246810661+masatohoshino@users.noreply.github.com>

Co-authored-by: Hu Yitao <39733381+CadanHu@users.noreply.github.com>

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-14 17:24:17 +08:00
Peter Steinberger
a57b4c513f fix(openai): guard post-hook tool payloads (#92928)
Guard OpenAI post-hook tool inspection and code-mode filtering against unreadable accessors and asynchronous payload replacements. Preserve valid official `exec` and `wait` function tools across Responses and Chat Completions paths.

Supersedes #89703.

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-14 02:17:25 -07:00
openclaw-clownfish[bot]
333a93ce44 fix(cli): avoid false downgrade prompt for latest tag
Keep unresolved latest package targets moving while preserving downgrade confirmation for unresolved non-latest dist-tags.

Validation:

- node scripts/run-vitest.mjs src/cli/update-cli.test.ts

- node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.src.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-src-pr92911.tsbuildinfo

- git diff --check origin/main...HEAD && git diff --check

Direct-landed from #92911 because the source branch has maintainer edits disabled and ClawSweeper requested changelog removal plus behavior proof.

Co-authored-by: Andy Wu <31586206+Andy312432@users.noreply.github.com>

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-14 17:16:22 +08:00
Bek
46a5a5ee5f fix(media): route OAuth image defaults through Codex (#92824)
Route implicit OpenAI image understanding through the Codex app-server for eligible OpenAI OAuth profiles. Preserve scoped and persisted credential ownership plus the rotating-token refresh lifecycle for isolated clients.

Fixes #87168

Thanks @bek91.
2026-06-14 17:11:18 +08:00
Vincent Koc
af091174db test(gateway): allow trajectory export instruction latency 2026-06-14 17:05:24 +08:00
Dallin Romney
1affe4fcdf Fold Telegram RTT sampling into live QA evidence (#92550)
* refactor(qa): fold telegram rtt into live evidence

* test: default package telegram rtt samples

* refactor(qa-lab): fold telegram rtt into live evidence

* fix(qa-lab): keep package telegram rtt optional for focused runs

* fix(qa-lab): avoid stale rtt evidence on failed samples

* fix(qa-lab): pass telegram live env into credential leasing

* fix(qa-lab): update telegram canary remediation artifacts

* docs(qa): remove stale telegram observed artifact guidance

* fix(qa-lab): clarify telegram empty-reply remediation

* fix(qa-lab): honor telegram rtt timeout

* ci(qa): drop stale telegram capture env

* refactor: align telegram evidence coverage fields

* fix: ignore stale telegram observed artifacts

* fix: preserve telegram rtt coverage mapping

* fix: omit unused telegram rtt catch binding

* docs: document telegram rtt check selector
2026-06-14 17:02:33 +08:00
Peter Steinberger
439a9e97fd fix(openai): quarantine unreadable tool schemas (#92921)
Snapshot unreadable OpenAI tool descriptors and schemas before payload construction, preserve healthy siblings, and reconcile hard tool choices with the surviving function inventory.

Adds live-tested Responses and Chat Completions coverage, including allowed_tools, while keeping Anthropic regressions green.

Related: #89413, #89013, #89016, #89378, #89543, #90200, #90283, #90286, #90397

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-14 01:45:38 -07:00
Vincent Koc
1c86769cb8 test(gateway): keep trajectory export live proof correlated 2026-06-14 16:38:58 +08:00
Vincent Koc
b1caba5906 test(qa): align tool coverage CLI expectation 2026-06-14 16:11:00 +08:00
Vincent Koc
d2c0d3ac9b test(agents): skip anthropic billing drift in live tool checks 2026-06-14 15:56:29 +08:00
openclaw-clownfish[bot]
e99ab385cf fix(memory): preserve reindex rollback recovery (#92881)
* fix(memory): preserve reindex rollback recovery

Co-authored-by: Shiwen Han <46259514+TSHOGX@users.noreply.github.com>

* fix(clownfish): address review for gitcrawl-5644-autonomous-smoke (1)

Co-authored-by: Shiwen Han <46259514+TSHOGX@users.noreply.github.com>

* test: update memory reindex test routing expectation

* chore(memory): remove release changelog entry

* fix(memory): complete reindex retry recovery

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Shiwen Han <46259514+TSHOGX@users.noreply.github.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-14 15:31:51 +08:00
Peter Steinberger
19130e0dc2 fix(providers): quarantine unreadable Anthropic payload tools (#92908)
Quarantine unreadable and invalid Anthropic-family tool schemas before OpenAI-compatible serialization, keep tool choices aligned with surviving tools, and preserve provider metadata.

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-14 00:27:48 -07:00
openclaw-clownfish[bot]
8d08b90489 fix(memory-wiki): stop flagging raw source pages as malformed (#92876)
* fix(memory-wiki): stop flagging raw source pages as malformed

* fix(clownfish): address review for gitcrawl-11828-autonomous-smoke (1)

* fix(memory-wiki): skip freshness lint for raw source pages

* fix(memory-wiki): keep raw source ids linted

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-14 15:18:41 +08:00
Dallin Romney
a3e9dfee0e Simplify QA scorecard mapping shape (#92558)
* test(qa): simplify scorecard mapping shape

* test(qa): use typed scorecard evidence refs

* test(qa): map scorecard categories by coverage id

* feat: align qa coverage with taxonomy features

* refactor: keep qa coverage ids canonical

* refactor: minimize qa coverage id churn

* test: align qa coverage id assertions

* test: update qa evidence coverage expectations

* refactor qa taxonomy coverage ids

* style qa taxonomy coverage ids

* test qa coverage lint fix

* test qa coverage type fix
2026-06-14 00:16:33 -07:00
Vincent Koc
cf4000b47b fix(gateway): preserve slash command media replies 2026-06-14 14:54:18 +08:00
Vincent Koc
85ebbec117 docs(changelog): refresh 2026.6.8 notes
(cherry picked from commit 0b5cb00980c68a39b8fb1d77f6c04e9733bcbb09)
2026-06-14 14:54:18 +08:00
Vincent Koc
f85ef0d114 fix(gateway): deliver command block replies in webchat
(cherry picked from commit 33390ee88f5c8325efc60d1793c532a9489a5a72)
2026-06-14 14:54:18 +08:00
brokemac79
d1299658ac fix(active-memory): preserve verbose recall summaries (#90739)
* fix(active-memory): preserve verbose recall summaries

* fix(active-memory): require recall evidence for recovery

* fix(active-memory): recognize capped recall results

* fix(active-memory): preserve grounded recall state

* refactor(active-memory): limit recovery to completed recalls

* fix(active-memory): ground terminal recall recovery

* fix(active-memory): limit unavailable recovery to completed replies

* fix(active-memory): harden recall evidence recovery

* fix(active-memory): preserve timeout recovery contract

* fix(active-memory): preserve capped failure evidence

* fix(active-memory): reject content-only recall failures

* fix(active-memory): ground completed recall summaries

* fix(active-memory): separate hook and recall timeouts

* fix(active-memory): classify custom tool failures

* fix(active-memory): preserve harness tool evidence

* fix(active-memory): reject explicit empty results

* fix(active-memory): wait for fallback recall evidence

* fix(codex): report dynamic tool results

* fix(active-memory): separate preflight recall deadline

* fix(active-memory): normalize recall tool names

* fix(agents): classify unavailable approvals

* docs(active-memory): clarify hook timeout phases

* test(active-memory): stabilize timeout abort proof

* fix(agents): preserve successful cancellation outcomes

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-13 23:38:58 -07:00
Peter Steinberger
a02813164d fix(anthropic): quarantine invalid direct tool schemas (#92896)
Quarantine unreadable and structurally invalid direct/custom Anthropic tool schemas in both canonical request builders while preserving healthy siblings, forced-choice semantics, OAuth name mapping, and official OpenAI behavior.

Supersedes #89418, #89221, #90228, #89622, #89229, and #90278.

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-13 23:28:36 -07:00
Jefsky Wong
462c076a24 fix(anthropic): merge consecutive assistant replay turns (#87346)
Merge adjacent Anthropic assistant turns before dangling tool-use validation so signed tool calls remain immediately paired with their tool results. Preserve contributor credit. Fixes #87329.
2026-06-13 22:34:38 -07:00
Peter Steinberger
d68de3f77d docs(crabbox): warm Testbox in parallel 2026-06-14 01:20:32 -04:00
liuhao1024
2e20dd8dbf fix(anthropic): omit stale thinking from disabled requests (#92373)
Preserve signed thinking for active Anthropic tool-result continuation while omitting native thinking from completed history when the new request disables or omits thinking. Applies the same replay rule to the legacy SDK provider and managed Anthropic transport. Fixes #92360.
2026-06-13 22:17:02 -07:00
elfka toruviel
66880a5d73 fix(agents): preserve valid reasoning replay metadata (#90682)
Preserve validated provider reasoning ciphertext, signatures, and replay identifiers through transcript redaction without exempting malformed, nested, or credential-shaped values.

Fixes #90093.

Co-authored-by: Elfka Toruviel <aeb31988340aa87b@toruviel.online>
2026-06-13 21:47:38 -07:00
Song Luo
231b5a14d5 fix(cli): clarify --tz help text for offset-less --at values
Clarifies cron edit --at help after maintainer rebase and preserves the Gateway-host timezone wording for cron --tz help.

Validation:
- git diff --check
- node scripts/run-vitest.mjs src/cli/cron-cli.test.ts
- local Codex autoreview clean, no actionable findings

Co-authored-by: rrrrrredy <rrrrrredy@users.noreply.github.com>
2026-06-14 12:34:10 +08:00
openclaw-clownfish[bot]
c5db07eddc fix(lobster): surface workflow path errors
Surface missing bare Lobster workflow file paths instead of silently falling through to inline pipeline parsing.

The runner now treats plain workflow file inputs as file paths, keeps inline commands with file-like arguments as pipelines, and preserves existing workflow file paths that contain spaces. Regression coverage covers missing bare workflow paths, inline false positives, and spaced workflow filenames.

Fixes #68101.

Based on and credits #68106 by @vvitovec. This replacement branch carries the focused fix forward because #68106 is dirty against current main and could not be repaired on the fork branch with available bot permissions.

Validation:
- node scripts/run-vitest.mjs extensions/lobster/src/lobster-runner.test.ts
- autoreview clean: no accepted/actionable findings after the spaced-path fix
- GitHub checks: 127 pass, 0 fail, 0 pending

Co-authored-by: Viktor Vítovec <230458341+vvitovec@users.noreply.github.com>
2026-06-14 12:18:33 +08:00
openclaw-clownfish[bot]
808f677ab4 fix(feishu): target typing reaction on inbound message
Target Feishu Typing reactions at the inbound message id while preserving reply and thread routing to the topic root.

This keeps the fallback to replyToMessageId for flows without a separate inbound target, and adds regression coverage for topic/replyInThread behavior and synthetic Feishu turn sources.

Based on and credits #67783 by @huiwen01. This replacement branch carries the same user-visible fix forward because #67783 is dirty against main and earlier automation could not update the fork branch with available permissions. This intentionally does not reuse or expand #73958; root_id routing remains separate.

Validation:
- pnpm check:changed
- pnpm -s vitest run extensions/feishu/src/bot.test.ts extensions/feishu/src/reply-dispatcher.test.ts extensions/feishu/src/monitor.reaction.test.ts
- autoreview clean: no accepted/actionable findings
- GitHub checks: 127 pass, 0 fail, 0 pending

Co-authored-by: huiwen01 <89329207+huiwen01@users.noreply.github.com>
2026-06-14 12:11:49 +08:00
Jason (Json)
7259cb5c77 fix(update): continue after package doctor warnings (#91586)
* fix(update): continue after package doctor warnings

* fix(update): type advisory step rendering

* fix(update): preserve advisory doctor step state

* fix(update): share advisory doctor state

* fix(update): keep timed-out doctor failures blocking

* fix(update): require explicit doctor advisory result

* fix(update): reject malformed doctor advisory results

* fix(update): bound doctor advisory diagnostics

* fix(update): keep doctor advisory restart-neutral

* fix(update): protect doctor advisory IPC

* fix(update): scope doctor advisories to converging updater

* fix(update): scope doctor advisories to deferred repairs

* fix(update): secure doctor advisory IPC

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-13 21:03:57 -07:00
openclaw-clownfish[bot]
889bc52ba5 fix(ui): repair iOS Safari chat viewport handling
Repairs https://github.com/openclaw/openclaw/pull/63644.

Review proof: local structured autoreview on branch review/pr-92855 against origin/main exited clean with no accepted/actionable findings.

Co-authored-by: Xi Qi <1311124+macdao@users.noreply.github.com>
2026-06-14 11:36:42 +08:00
openclaw-clownfish[bot]
c8cf57a1a5 fix(hooks): reject slug-generator error payloads
Repairs https://github.com/openclaw/openclaw/pull/64181.

Co-authored-by: Cypher <28184436+Cypherm@users.noreply.github.com>
2026-06-14 11:31:01 +08:00
openclaw-clownfish[bot]
9a0bae06b7 fix(acp): accept MCP date protocolVersion in ACP server
Repairs https://github.com/openclaw/openclaw/pull/56176.
Fixes https://github.com/openclaw/openclaw/issues/56102.

Co-authored-by: Saurabh Mishra <2924124+bugkill3r@users.noreply.github.com>
2026-06-14 11:30:48 +08:00
Eduardo Piva
7f49f875de Add diagnostics OTEL capability contract tests (#92045) 2026-06-14 11:26:49 +08:00
openclaw-clownfish[bot]
bd10e1998b fix(tailscale): preserve parse errors for malformed JSON
Accept the fail-closed behavior for malformed `tailscale funnel status --json`: noisy valid JSON should parse, but malformed status output should not silently become “route absent”.

Source PR: https://github.com/openclaw/openclaw/pull/63321

Co-authored-by: Francisco Maestre Torreblanca <2027043+franciscomaestre@users.noreply.github.com>
2026-06-14 11:22:05 +08:00
zhang-guiping
650c5cac33 fix(qqbot): surface failed media sends (#92823)
* fix(qqbot): surface media send failures

* test(qqbot): cover text send failures

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-14 11:13:56 +08:00
Jason (Json)
965fa05df3 feat: add tool search directory mode
Add an experimental directory mode that keeps large authorized tool schemas deferred while exposing bounded discovery, exact deferred hydration, and normal OpenClaw policy/hook execution. Client tools remain directly visible; ambiguous hidden names fail closed.
2026-06-13 20:08:39 -07:00
openclaw-clownfish[bot]
9f32bea397 fix(feishu): await HTTP server shutdown during monitor cleanup
Source PR: https://github.com/openclaw/openclaw/pull/48588

Co-authored-by: alex sagit <267811734+alex-xuweilong@users.noreply.github.com>
2026-06-14 10:56:15 +08:00
zengLingbiao
7fb0d45b48 fix(agents): add usage guidance to sessions_spawn tool description (fixes #91814) (#91824)
* fix(agents): add usage guidance to sessions_spawn tool description (fixes #91814)

* fix(agents): tighten sessions spawn guidance

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-14 10:49:26 +08:00
Jason (Json)
8ae1adfdcc ci: gate stable releases on Windows companion assets (#92555)
* ci: gate stable releases on Windows companion assets

* fix(release): reject malformed Windows checksum manifests

* fix(release): make Windows recovery fail closed

* fix(release): tighten Windows asset identity checks

* fix(release): validate prepared candidate tarballs

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-13 19:33:33 -07:00
LowCode191
4001be54e4 fix(agents): catch malformed image blocks in sanitizeContentBlocksImages (#92792)
* fix(agents): catch malformed image blocks in sanitizeContentBlocksImages

* fix(agents): sanitize malformed-only image results

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-14 10:24:48 +08:00
Jayesh Betala
e8b35a8280 fix(status): render sub-1000 token counts as plain integers (#89736)
* fix(status): render sub-1000 token counts as plain integers

formatKTokens always divided by 1000 and appended "k", so token counts
below 1000 rendered as misleading fractional k in `openclaw status`
output (e.g. 999 rounded up across the boundary to "1.0k", 420 -> "0.4k",
a 300-token cache write -> "write 0.3k").

Guard value < 1000 to render the plain rounded integer, matching the
canonical formatTokenCount convention (src/utils/usage-format.ts). The
>=1000 "k" behavior is unchanged. Adds focused regression tests for the
0/420/999/1000/12000 boundary and small-session/small-cache status lines.

Fixes #89735

* fix(status): reuse canonical token formatter

* refactor(status): extract lightweight token formatter

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-14 10:24:42 +08:00
liuhao1024
077d3f1366 fix(stale): exempt ClawSweeper actionable labels from stale lifecycle (#92801)
Add clawsweeper:queueable-fix, clawsweeper:source-repro, and
clawsweeper:fix-shape-clear to exempt-issue-labels in all 4 stale
workflow steps and the backfill-closures script's issueExemptLabels
set.

Previously, issues classified by ClawSweeper as actionable fix
candidates could still be marked stale and auto-closed, creating
a conflict between the two automation systems (e.g. #78640,
#81078, #81122 had both 'stale' and 'clawsweeper:queueable-fix').

Fixes #89564
2026-06-14 10:24:20 +08:00
zhang-guiping
8afbc98018 fix(whatsapp): require durable auth before login success (#92095) 2026-06-14 10:24:14 +08:00
openclaw-clownfish[bot]
f58f8c86b9 fix(discord): raise thread title timeout and tokens
Source PR: https://github.com/openclaw/openclaw/pull/64734

Co-authored-by: Hana Chang <741302+hanamizuki@users.noreply.github.com>
2026-06-14 10:22:02 +08:00
clawsweeper[bot]
24e729fc4e feat(browser): extend --labels overlay to full-page and element captures (#92834)
Summary:
- The replacement PR extends Browser plugin labeled screenshots to honor Playwright full-page/ref/element scope, returns annotation bounding boxes, and updates docs, tests, and skill guidance.
- PR surface: Source +415, Tests +550, Docs +24. Total +989 across 12 files.
- Reproducibility: yes. Current main source shows the labeled Playwright helper ignores fullPage/ref/element and omits annotations, and the source PR supplies live before/after commands for the Browser plugin path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs(browser): correct raw-CDP labels caveat in automation skill
- PR branch already contained follow-up commit before automerge: fix(browser): preserve labelsSkipped semantics for off-viewport refs
- PR branch already contained follow-up commit before automerge: docs(browser): scope labels docs by driver
- PR branch already contained follow-up commit before automerge: docs(browser): fix labels annotation indent and document scope fix
- PR branch already contained follow-up commit before automerge: docs(browser): indent annotations box schema under --labels bullet
- PR branch already contained follow-up commit before automerge: docs(browser): indent labels annotation schema

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

Prepared head SHA: 70aca6c506
Review: https://github.com/openclaw/openclaw/pull/92834#issuecomment-4700431344

Co-authored-by: FMLS <kfliuyang@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-14 02:21:23 +00:00
Peter Steinberger
aef670cf0c fix(copilot): strip replayed thinking blocks
Remove replayed thinking and redacted-thinking blocks from GitHub Copilot Claude history and final Anthropic payloads while preserving visible content, tool turns, and non-empty assistant structure.

Fixes #81520
Supersedes #87060 and #81534

Co-authored-by: Gio Della-Libera <giodl73@gmail.com>
2026-06-13 19:14:16 -07:00
clawsweeper[bot]
399f5bc993 fix: refresh model context metadata safely
Cap configured session context overrides by the selected model's known context window, refresh provider/model metadata consistently, and preserve the fixed Anthropic 1M context contract.

Fixes #39857

Co-authored-by: Kros Dai <7087+xdanger@users.noreply.github.com>
2026-06-13 18:48:43 -07:00
openclaw-clownfish[bot]
34678d8dfa fix(telegram): preserve command callbacks while prefixing generic callback data (#92825)
Fixes #54909.

Repair #54962 by preserving raw slash-command callbacks while routing generic callback data to agents as `callback_data: <value>`.

Validation:
- pnpm check:changed
- pnpm -s vitest run extensions/telegram/src/bot.create-telegram-bot.test.ts
- Codex /review
- Real behavior proof
- GitHub PR checks

Source PR credit: continues @hnshah's work from #54962 and preserves @timt80's report credit from #54909.

Co-authored-by: Hiten Shah <3155200+hnshah@users.noreply.github.com>
2026-06-14 09:47:16 +08:00
Vincent Koc
c60b424124 test(release): accept trajectory command session final
(cherry picked from commit 9458ffa7e8)
2026-06-14 09:42:13 +08:00
Vincent Koc
340c2456bb test(release): harden trajectory export live check
(cherry picked from commit b2d4cb7f86)
2026-06-14 09:25:18 +08:00
openclaw-clownfish[bot]
971542b7f6 UI: localize Logs tab labels (#92820)
Repair #61080 by routing Logs tab user-facing labels through Control UI locale keys while keeping export filename suffixes stable.

Validation:
- pnpm ui:i18n:check
- pnpm check:changed
- ui/src/ui/views/logs.test.ts
- Codex /review
- GitHub PR checks

Source PR credit: continues @rubensfox20's work from https://github.com/openclaw/openclaw/pull/61080.

Co-authored-by: rubensfox20 <111531429+rubensfox20@users.noreply.github.com>
2026-06-14 09:23:00 +08:00
scotthuang
b3dc274034 fix(gateway): preserve active runs during plugin finalization (#92746)
* fix(gateway): preserve active run during plugin finalization

* fix(ui): skip session.message history reload while gateway reports active run

* fix(ui): remove unused eslint-disable directive

* fix(ui): preserve active runs through finalization

---------

Co-authored-by: scotthuang <scotthuang@tencent.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-14 08:58:27 +08:00
liuhao1024
1acca038b1 fix(gateway): use resolveNonNegativeNumber for totalTokens to display 0 instead of ? (fixes #43009) (#92795)
* fix(gateway): use resolveNonNegativeNumber for totalTokens to display 0 instead of ?

resolvePositiveNumber requires value > 0, filtering out the valid
totalTokens = 0 case (new session, no usage yet). This caused the TUI
to display 'tokens ?/200k' instead of 'tokens 0/200k (0%)'.

Use resolveNonNegativeNumber (>= 0) for the final totalTokens value
used in session display. The needsTranscriptTotalTokens check at line
2041 still correctly uses resolvePositiveNumber to decide whether to
fetch transcript data.

Fixes #43009

* fix(gateway): preserve fresh zero-token sessions

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-14 08:56:49 +08:00
liuhao1024
fd4f5b3f59 fix(macos): defer isOverflowing mutation to break SwiftUI render loop (fixes #43480) (#92778)
* fix(macos): defer isOverflowing mutation to break SwiftUI render loop

measuredHeight() mutated model.isOverflowing synchronously during a SwiftUI
view update cycle. The onChange(of: attributed) handler triggered
updateWindowFrame → targetFrame → measuredHeight, which set isOverflowing,
invalidating the view and re-triggering onChange — an infinite render loop
causing 100% CPU pinwheel.

Fix: defer the isOverflowing mutation via DispatchQueue.main.async with an
equality guard to prevent redundant updates. The frame calculation itself
remains synchronous so the window size is correct immediately.

Fixes #43480

* fix(macos): preserve latest overflow measurement

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-14 08:56:25 +08:00
Vincent Koc
ac3a98e55d test(gateway): capture trajectory export command events
(cherry picked from commit b656a51046)
2026-06-14 08:53:54 +08:00
liuhao1024
57e8c50d19 fix(telegram): skip IPv4 fallback when user explicitly configures non-ipv4first dnsResultOrder (fixes #41671) (#92806)
* fix(telegram): skip IPv4 fallback when user configures non-ipv4first dnsResultOrder

When the user explicitly configures channels.telegram.network.dnsResultOrder
to a non-ipv4first value (e.g. verbatim), the sticky IPv4 fallback dispatcher
should not be armed. Forcing autoSelectFamily=false + dnsResultOrder=ipv4first
overrides the user's explicit IPv6-friendly config, causing media downloads to
fail on hosts where IPv4 is broken but IPv6 works.

Fixes #41671

* fix(telegram): respect explicit DNS fallback policy

---------

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-14 08:51:26 +08:00
liuhao1024
274b7b1d9f fix(doctor): avoid false-positive legacy cron store warning when store was already migrated (fixes #92683) (#92690)
* fix(doctor): avoid false-positive legacy cron store warning when store was already migrated

When rawJobs.length > 0 and other issues exist (notifyCount, dreamingStaleCount)
but legacyStoreDetected is false (file already removed after migration), the doctor
unconditionally printed 'Legacy cron job storage detected at ...' — misleading users
into thinking the migration was incomplete.

Fix: conditionally use 'Cron store issues detected' heading when no legacy store file
exists, reserving 'Legacy cron job storage detected' for actual legacy store presence.

Fixes #92683

* test(doctor): add test for false-positive legacy cron store warning (#92683)
2026-06-14 08:49:07 +08:00
Vincent Koc
efca4b7e64 fix(memory): keep skipped recall diagnostics opt-in
Follow-up to #92745 after maintainer autoreview found that the skipped recall event widened the shipped MemoryHostEvent union and changed limited legacy reads.

Keep readMemoryHostEvents() source-compatible by filtering diagnostic records before applying limits, and expose skipped recall diagnostics through the opt-in MemoryHostEventRecord/readMemoryHostEventRecords path.

Original skipped-recall behavior landed in #92745 by @mushuiyu886.
2026-06-14 08:47:03 +08:00
Jason (Json)
65b460f234 fix(nodes): surface pending reapproval diagnostics (#92547)
* fix(nodes): surface pending reapproval diagnostics

* fix(nodes): harden reapproval diagnostics

* fix(nodes): scope pending diagnostics

* fix(nodes): request pairing diagnostics in cli

* fix(nodes): reuse stored auth for diagnostics

* fix(nodes): preserve selected diagnostics credentials

* fix(nodes): prefer approved diagnostics auth

* fix(nodes): narrow diagnostics fallbacks

* fix(nodes): recover from stale diagnostics auth

* fix(gateway): preserve connect error narrowing

* fix(nodes): isolate privileged diagnostics auth

* fix(nodes): constrain privileged diagnostics auth

* fix(nodes): close diagnostics review gaps

* fix(nodes): guard reapproval cleanup races

* fix(nodes): defer stale pairing cleanup

* fix(nodes): preserve reapproval on hello failure

* test(nodes): await post-handshake reapproval cleanup

* test(nodes): avoid unbound websocket send capture

* fix(nodes): allow local auth-none diagnostics

* fix(nodes): preserve overlapping reapproval

* fix(nodes): preserve pending node metadata

* fix(nodes): keep connection age with status

* fix(nodes): preserve reapproval during reconnect races

* fix(nodes): serialize reapproval cleanup

* fix(nodes): bound reapproval reconnect races

* test(nodes): satisfy cleanup claim lint

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-13 17:40:18 -07:00
liuhao1024
4c3c0ff5f9 fix(telegram): acknowledge callbacks before sequentialize
Fixes #42156.

Answer Telegram callback queries before per-chat/topic sequentialize can queue the handler behind an active turn, and carry the in-flight answer promise on the grammY context so the normal handler reuses it instead of double-answering.

Proof:
- node scripts/run-vitest.mjs extensions/telegram/src/bot.create-telegram-bot.test.ts -- -t "answers callback queries before same-chat sequentialize delays handlers|sequentializes updates by chat and thread|routes callback_query payloads as messages"
- node scripts/run-vitest.mjs extensions/telegram/src/bot.test.ts extensions/telegram/src/bot-handlers.runtime.test.ts
- node_modules/.bin/oxfmt --check extensions/telegram/src/bot-core.ts extensions/telegram/src/bot-handlers.runtime.ts extensions/telegram/src/callback-query-answer-state.ts extensions/telegram/src/bot.create-telegram-bot.test.ts
- git diff --check origin/main...HEAD
- .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
- Azure Crabbox cbx_cba20c462ad5 / silver-barnacle: OPENCLAW_TESTBOX=1 node scripts/crabbox-wrapper.mjs run --provider azure --class Standard_D4ads_v6 --idle-timeout 90m --ttl 240m --timing-json -- env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed

Proof gap: live Telegram Desktop/burner-account proof was not run because openclaw-telegram-user-crabbox-proof is not installed in this shell.
2026-06-14 08:37:24 +08:00
Vincent Koc
9a27af9507 test(gateway): wait for trajectory export guidance
(cherry picked from commit 4675423788)
2026-06-14 08:26:47 +08:00
Peter Steinberger
1d9b9ef48f fix: reject unvalidated voice media streams
Reject voice media stream start frames when no acceptance validator is configured, preventing fail-open STT/TTS session creation. Verified locally, with autoreview, in a remote Linux dev box, and by green CI.
2026-06-13 17:26:19 -07:00
Ashish Patel
8f62ec6177 fix(status): avoid cumulative usage for context percent (#92604)
* fix(status): avoid cumulative usage for context percent

* fix(status): preserve legacy context totals
2026-06-14 08:07:08 +08:00
swide
b72634f56d fix(gateway): forward image-only input on /v1/responses (parity with chat completions) (#92488)
* fix(gateway): accept image-only input on /v1/responses

The OpenResponses endpoint rejected requests whose `input` contained only
an `input_image` (no `input_text`) with `400 Missing user message in
input.`, even though the image was parsed and collected into `images`.
The guard only checked `prompt.message` and ignored `images`, unlike the
equivalent /v1/chat/completions guard which uses
`!prompt.message && images.length === 0`.

Align the OpenResponses guard with Chat Completions so image-only turns
are forwarded to the agent. Empty input (no text and no image) still
returns 400.

Adds regression tests: image-only base64 input -> 200 with image reaching
the agent, and empty content -> 400.

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

* fix(gateway): pass image-only /v1/responses turns to the agent

The one-line guard alone was insufficient: even after letting image-only
input past the `Missing user message` check, the downstream agent command
(`prepareAgentCommandExecution`) throws `Message (--message) is required`
for an empty message, so image-only `/v1/responses` returned 500.

Mirror the /v1/chat/completions prompt builder: substitute the shared
IMAGE_ONLY_USER_MESSAGE placeholder for the active image-only user turn so
the turn is not dropped and the real image is still attached via `images`.
Promote the placeholder constant to the shared gateway agent-prompt module
so both endpoints stay in sync, and revert the responses guard back to the
original `!prompt.message` check (responses images are not scoped to the
active turn, so the placeholder is the correct, single source of truth).

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

* chore: retrigger CI (flaky startup-core test timeout, unrelated to change)

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

---------

Co-authored-by: songwendong <songwendong@shuidi-inc.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 08:06:20 +08:00
mushuiyu_xydt
99e7dad0e4 fix(memory): surface skipped short-term recall hits (#92745)
Record diagnostic events when memory_search returns durable memory hits that are intentionally excluded from short-term promotion, so users can distinguish eligibility decisions from recall tracking failures.
2026-06-14 08:05:52 +08:00
Vincent Koc
d626e99c31 test(gateway): preserve live Codex api-key auth
(cherry picked from commit 4aa50732f6)
2026-06-14 08:00:22 +08:00
openclaw-clownfish[bot]
c2754150c9 fix(heartbeat): route outbound mirror to isolated session key (#92807)
* fix(heartbeat): route outbound mirror to isolated session key

Co-authored-by: Merkava <263752781+agent-merkava@users.noreply.github.com>

* fix(clownfish): address review for ghcrawl-143801-autonomous-smoke (1)

Co-authored-by: Merkava <263752781+agent-merkava@users.noreply.github.com>

* fix(clownfish): address review for ghcrawl-143801-autonomous-smoke (1)

Co-authored-by: Merkava <263752781+agent-merkava@users.noreply.github.com>

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Merkava <263752781+agent-merkava@users.noreply.github.com>
2026-06-14 07:51:29 +08:00
Chunyue Wang
5b21384ab6 fix(cron): isolate transient auth cooldowns
Keep cron-local transient auth failures from polluting shared cooldowns while preserving real auth/billing/rate-limit propagation. Verified with focused auth/cron tests, type proof, autoreview, and clean CI.
2026-06-14 07:45:20 +08:00
Zhao Shiqi
edd76238fe fix(daemon): keep duplicate Windows gateway tasks visible
Normalize Windows schtasks default gateway names without hiding similarly prefixed duplicate tasks. Verified with focused daemon tests, type proof, autoreview, and clean CI.
2026-06-14 07:45:16 +08:00
Darren Xu
d6b3950734 fix(tavily): keep web search contract executable
Keep the Tavily public artifact lightweight while lazily executing through the provider runtime. Verified with focused Tavily/provider artifact tests and clean CI.
2026-06-14 07:43:17 +08:00
Jayesh Betala
61145dc252 fix(ports): avoid stale cleanup for non-gateway SSH listeners
Limit SSH tunnel classification to actual queried-port forwards and keep SSH-like non-gateway listeners out of stale gateway cleanup. Verified with focused port/restart tests and clean CI.
2026-06-14 07:43:12 +08:00
Anurag Bheemappa Gnanamurthy
382db15e33 fix(configure): mask gateway token prompts
Mask gateway token/password prompts while preserving blank-token generation. Verified with focused configure wizard tests and clean CI.
2026-06-14 07:43:07 +08:00
Colin Johnson
1a8747620e fix: reflow composer beside workspace rail
Reflow the Control UI chat composer into the main chat column when the Workspace Files rail is expanded, so the composer stays beside the rail instead of extending underneath it.

Prepared head SHA: 7108d88cda
Co-authored-by: Colin Johnson <211764741+Solvely-Colin@users.noreply.github.com>
Co-authored-by: shakkernerd <165377636+shakkernerd@users.noreply.github.com>
Reviewed-by: @shakkernerd
2026-06-14 00:40:03 +01:00
Rohit
e55cebf4c2 fix(canvas): validate CLI numeric options
Validate Canvas CLI numeric arguments before node invocation.

- Reject malformed `--invoke-timeout` values through the shared Canvas invoke path before resolving a node.
- Keep snapshot `--quality` bounded to the existing Canvas tool schema range of 0..1.
- Add focused CLI regressions for timeout validation, quality bounds, and accepted boundary values.

Verification:
- `node scripts/run-vitest.mjs extensions/canvas/src/cli.test.ts --maxWorkers=1`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main ...`
- AWS Crabbox `cbx_b7838c58daba`, run `run_bbb58ca536f4`: `check:changed`
- Exact PR head `a970ce594ea6e7284ace519352fc258b1b81cb80`: GitHub checks green
2026-06-14 07:39:17 +08:00
Vincent Koc
2d4a9eb405 test(matrix): avoid racy inbound dedupe TTL
(cherry picked from commit 8ae3662b1c)
2026-06-14 07:35:14 +08:00
Vincent Koc
47759c3506 fix(qa): accept rich Telegram canary presence
(cherry picked from commit e86eb7567a)
2026-06-14 07:20:16 +08:00
Rohit
3429e33feb fix(matrix): validate CLI numeric option ranges
Validates Matrix CLI numeric option ranges before invoking setup or verification side effects.

`--initial-sync-limit` must now be non-negative, and `--timeout-ms` must now be positive.

Original PR by @rohitjavvadi.

Verification:
- `node scripts/run-vitest.mjs extensions/matrix/src/cli.test.ts --maxWorkers=1`
- autoreview clean
- Crabbox AWS `cbx_5c32f138ab3a` / `swift-lobster`, run `run_6e133b8b82e7`: `check:changed` passed
- exact PR head CI green: `d75f118299029b0516311646276cd2d6582379c5`
2026-06-14 07:16:39 +08:00
Vincent Koc
894f521aa5 test(macos): avoid real approvals migration in tests 2026-06-14 07:08:26 +08:00
Vincent Koc
97c5e6c235 test(macos): isolate exec approvals env 2026-06-14 07:04:53 +08:00
Stellar鱼
9974641d1e fix(agents): preserve compatible CLI session runtime pins
Preserves provider-compatible CLI runtime session pins across reply execution, follow-up execution, dispatch visibility, preflight compaction, and memory flush.

This keeps sessions pinned to compatible CLI runtimes such as `claude-cli` from leaking into embedded OpenClaw maintenance paths while still rejecting cross-provider runtime pins.

Original PR by @yu-xin-c; includes maintainer follow-up for the sibling memory paths.

Verification:
- `node scripts/run-vitest.mjs src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/agent-runner-memory.test.ts src/agents/model-runtime-aliases.test.ts --maxWorkers=1`
- autoreview clean
- Crabbox AWS `cbx_44400b494e97` / `coral-prawn`, run `run_69dd43475e39`: `check:changed` passed
- exact PR head CI green: `303b2f794f6c01fcf21b62b27c536b5f6eceb421`
2026-06-14 06:54:51 +08:00
Vincent Koc
924f4c1964 fix(qa): read rich Telegram replies in live checks
(cherry picked from commit 9c8b880353)
2026-06-14 06:35:59 +08:00
dependabot[bot]
2f57352eaa chore(deps): bump macOS Swift dependencies
Updates the macOS Swift package resolution for patch releases of Peekaboo, Sparkle, and swift-log.

Verification:
- `swift package describe --type json`
- `swift build --target OpenClawIPC`
- `swift build --target OpenClawDiscovery`
- upstream tag/revision checks for Peekaboo 3.4.1, Sparkle 2.9.3, and swift-log 1.13.2
- autoreview clean
- exact PR head CI green for macOS, dependency, and security checks
2026-06-14 06:26:41 +08:00
Vincent Koc
c11fcbcb6a fix(release): repair beta validation fixtures 2026-06-14 06:19:23 +08:00
lizeyu-xydt
5b6810211c fix(docker): remove stale nested openclaw package
Remove the stale nested openclaw package, its .bin shim, and the pnpm virtual-store copy from the runtime Docker image before final runtime assets are copied.

Run the package dist import-closure check after the cleanup so the check validates the final runtime-assets tree that the image ships.

Compatibility note: private Docker paths under /app/node_modules/openclaw and /app/node_modules/.bin/openclaw are removed; downstream images should use the documented /usr/local/bin/openclaw launcher or /app/openclaw.mjs.

Fixes #92551.
Thanks @lzyyzznl for the fix and @fxstein for the report.
2026-06-14 06:17:55 +08:00
Vincent Koc
e4313bac97 fix(ui): localize workspace file rail labels 2026-06-14 06:01:30 +08:00
Vincent Koc
6ebb303ef0 docs(changelog): refresh 2026.6.8 notes 2026-06-14 05:43:48 +08:00
Yuval Dinodia
ae68006a8f fix(sessions): enforce channel send policy for account-scoped DMs
Derive the channel from canonical account-scoped DM session keys when resolving session.sendPolicy, so channel-scoped allow/deny rules apply to per-account-channel-peer sessions.

Keep derivation limited to canonical channel peer key shapes and add malformed-key regressions so incomplete or non-channel keys do not accidentally match channel rules.

Compatibility note: existing channel-scoped send-policy rules can now block account-scoped DM sends that were previously allowed by this bug.

Thanks @yetval for the fix.
2026-06-14 05:38:42 +08:00
Peter Steinberger
735f59af73 feat(providers): add GLM-5.2 support (#92796)
* feat(providers): add GLM-5.2 support

* ci(live): add GLM-5.2 provider shard
2026-06-13 14:33:28 -07:00
mushuiyu_xydt
47112fc423 fix(memory): split header-too-large embedding batches
Route OpenAI/OpenAI-compatible request_headers_too_large embedding failures into the existing memory-core batch splitter instead of aborting bulk memory indexing.

Tighten the classifier to require header-too-large wording rather than a bare 431 status token, so unrelated provider errors do not fan out into recursive requests.

Fixes #92465.
Thanks @mushuiyu886 for the fix and @BrettHamlin for the report and proof.
2026-06-14 05:20:25 +08:00
Andy Ye
8549a203d4 Honor WhatsApp configured ACP bindings (#92513)
Merged via squash.

Prepared head SHA: 665080f482
Co-authored-by: TurboTheTurtle <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr
2026-06-13 18:09:22 -03:00
liuhao1024
d912909230 docs(nodes): add node config example
Add a Nodes overview `openclaw.json` example for node pairing, command allow/deny policy, node exec routing, and per-agent node pinning.

Also clarifies exact `denyCommands` matching and links readers to the config reference for pairing and command-policy field details.

Fixes #92662.
Thanks @liuhao1024 for the fix and @ZengWen-DT for the parallel docs wording on exact node command policy.
2026-06-14 04:58:06 +08:00
Sally O'Malley
e6ffcf7362 docs: clarify before_install hook scope (#92766)
Signed-off-by: sallyom <somalley@redhat.com>
2026-06-13 16:54:04 -04:00
Ariel Smoliar
8047350445 docs(config): correct agent defaults concurrency comments
Correct the exported agent defaults type comments for `maxConcurrent` and `subagents.maxConcurrent` so they match the runtime defaults of 4 and 8.

No runtime behavior changes.
Thanks @ArielSmoliar for the fix.
2026-06-14 04:44:03 +08:00
Terrance Chen
15e4fbf593 fix(markdown-core): treat infinity chunk limit as unbounded
Fix render-aware markdown chunking so `Number.POSITIVE_INFINITY` is treated as an explicit unbounded chunk limit instead of falling back to `1`.

This preserves full Signal media captions and disabled Signal text chunking while keeping invalid non-finite limits on the existing fallback path.

Fixes #92734.
Thanks @yhterrance for the report and fix.
2026-06-14 04:29:12 +08:00
KRATOS
4e4ea1c16b fix(diagnostics): keep recovery scheduling out of the stuck-session warning backoff (#92752)
Summary:
- The branch changes diagnostic stuck/long-running warning backoff so recovery-eligible classifications are still returned during throttled warning ticks and updates the diagnostic tests.
- PR surface: Source +17, Tests +48. Total +65 across 2 files.
- Reproducibility: yes. Current main source shows logSessionAttention can return undefined during stuck or lon ... g backoff before the heartbeat reaches requestStuckSessionRecovery; I did not run a live QQ gateway replay.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(diagnostics): keep recovery scheduling out of the stuck-session w…

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

Prepared head SHA: f61ec3a33f
Review: https://github.com/openclaw/openclaw/pull/92752#issuecomment-4699298908

Co-authored-by: Gnanam <gnanasekaran.sekareee@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-06-13 20:05:33 +00:00
Narahari Raghava
b2da129e51 fix(tui): show resolved model ref in confirmation
Uses the canonical model ref returned by `sessions.patch` for the TUI `/model` confirmation so alias inputs report the model that was actually applied. The fallback still shows the raw input when a backend does not return `resolved`, and the display path uses `modelKey` so nested model ids keep the provider prefix without double-prefixing self-prefixed ids.

Proof: local focused TUI Vitest/format/lint; autoreview clean; Crabbox AWS focused TUI test run `run_7d7cc5b040e8`; exact-head GitHub CI green on `6db4acfb08f9d477ee1bdab429bd7189b78ffc92`.
2026-06-14 03:45:59 +08:00
Yzx
5b21a0337b fix(state): avoid sqlite wal on nfs state volumes
Detects NFS-backed SQLite database paths in the shared WAL helper and uses rollback journaling for those paths while preserving WAL/checkpoint maintenance on local filesystems. The NFS path now verifies SQLite's effective journal mode before disabling WAL maintenance, and core/memory/proxy-capture callers pass database path context into the centralized helper.

Fixes #90491.

Proof: local focused Vitest/format/lint; autoreview clean after fixing the journal-mode verification finding; Crabbox AWS focused test run `run_2ea7014350da`; Crabbox AWS changed gate `run_c828bbfe7d23`; exact-head GitHub CI green on `59674305ecd863d4815eec6098ccd3daab79ca4f`.
2026-06-14 03:32:13 +08:00
Shakker
dbf24fe35a fix: start workspace files collapsed 2026-06-13 20:19:34 +01:00
sallyom
d03932af18 feat: make workspace files panel collapsible
Signed-off-by: sallyom <somalley@redhat.com>
2026-06-13 20:19:34 +01:00
Zee Zheng
13a079b3f8 fix(telegram): expose thread create CLI remap
Exposes Telegram's thread-create CLI remap through the exported Telegram channel action adapter, preserving the existing plugin-owned mapping to topic-create before gateway dispatch.\n\nFixes #81581.\n\nProof: local focused format/lint/Vitest and dry-run; autoreview clean; Crabbox AWS run_07b98c939fce focused tests; Crabbox AWS run_1b7b35ce1de1 check:changed; exact-head GitHub CI green on 16f6afbdd7.
2026-06-14 02:37:31 +08:00
Colin Johnson
e58310b000 fix(ios): force stale foreground gateway reconnects (#92552) 2026-06-13 20:33:39 +02:00
狼哥
a1814586c6 fix(ui): preserve dashboard session parent lineage
Preserves the selected Control UI session as the parent when creating dashboard child sessions even if the session list is stale or filtered, while avoiding the synthetic unknown session as a parent.\n\nFixes #90623.\n\nProof: local focused format/lint/Vitest/browser test; autoreview clean; Crabbox AWS run_a2bfdcd2315a UI proof; Crabbox AWS run_ce60fdc546ff check:changed; exact-head GitHub CI green on 03d1c6f646.
2026-06-14 02:14:28 +08:00
Matt H
ca2410ab07 fix(parallel): send User-Agent on free MCP requests
Adds the OpenClaw Parallel User-Agent to free Parallel Search MCP requests so the zero-config web_search path is identifiable at the HTTP layer, matching the paid REST transport.\n\nProof: local focused format/lint/Vitest; live anonymous Parallel MCP handshake; autoreview clean; Crabbox AWS run_bf41ce86e862 focused regression; Crabbox AWS run_ee9b8954b081 check:changed; exact-head GitHub CI green on b7e45e3bfc.
2026-06-14 01:52:59 +08:00
ooiuuii
d20fdf3b38 fix(gateway): mark active main sessions before restart shutdown aborts (#91357)
* Mark active main sessions during restart shutdown

* Type restart marker mock in close tests

* fix(gateway): preserve active run ownership across restart

* fix(gateway): preserve active runs across restart

* fix(gateway): close restart recovery edge cases

* fix(cron): preserve lifecycle ownership across restart

* fix(gateway): release rejected run contexts

* fix(gateway): preserve restart lifecycle ownership

* fix(cron): retain overlapping run ownership

* fix(agents): preserve restart terminal precedence

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-13 10:49:17 -07:00
Josh Avant
689ebc815b feat: support /btw in CLI-backed sessions (#92669)
* feat: support CLI btw side questions

* test: fix CLI prepare test fixture types

* fix: lazy load local btw runner
2026-06-13 19:36:53 +02:00
bymle
22069bcc56 fix(google): strip provider prefix from Vertex model path
Summary:
- Strip the redundant `google/` provider prefix before embedding Google Vertex model ids under `/publishers/google/models/`.
- Keep bare Vertex model ids unchanged.
- Add regression coverage for the provider-qualified Vertex path.

Verification:
- `node_modules/.bin/oxfmt --check --threads=1 extensions/google/transport-stream.ts extensions/google/transport-stream.test.ts`
- `node scripts/run-oxlint.mjs extensions/google/transport-stream.ts extensions/google/transport-stream.test.ts`
- `node scripts/run-vitest.mjs extensions/google/transport-stream.test.ts --maxWorkers=1 -t 'strips redundant google provider prefixes from Google Vertex model paths'`
- Autoreview clean
- AWS Crabbox `run_649b209478d2` focused Node 24 regression proof
- AWS Crabbox `run_e193db2707ad` remote `check:changed`
- Exact-head CI green for `23aca6f46f596e220df37d939317b433f7044ec6`
- Contributor live Google Vertex proof recorded in the PR body
2026-06-14 01:13:43 +08:00
NianJiu
b01a54de6f fix(ui): restore sidebar session picker interactivity above desktop workbench (#92705)
* fix(ui): restore sidebar session picker interactivity above desktop workbench

The collapsed sidebar session picker was covered by the chat content
area when the workspace rail was visible at wider viewports. Two
issues caused this:

1. .sidebar-session-select--collapsed .chat-session-picker used
   var(--z-dropdown) which was never defined, creating an invalid
   z-index declaration (falls back to auto).

2. .shell-nav and .content--chat are grid siblings with equal
   z-index (auto), and .content--chat (later DOM) paints above
   .shell-nav, covering the session picker that extends from the
   nav column into the content column.

Fix: add position:relative + z-index:10 to .shell-nav so it stacks
above .content--chat; change overflow from hidden to visible so
the session picker extends beyond the nav rail; replace undefined
var(--z-dropdown) with z-index:100.

* fix(ui): keep sidebar picker z-index tokenized

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-14 01:13:29 +08:00
Ayaan Zaidi
45e36a241a fix(telegram): pass rich text prompts to cli backends 2026-06-13 21:45:22 +05:30
Ayaan Zaidi
5cb6f8aa9f fix(telegram): show rich text prompt for final replies 2026-06-13 21:45:22 +05:30
Ayaan Zaidi
b9ad8649d0 fix(telegram): allow rich tables in group prompts 2026-06-13 21:45:22 +05:30
Ayaan Zaidi
4e8a527542 test(telegram): align message flow fixture with rich drafts 2026-06-13 21:45:22 +05:30
Ayaan Zaidi
0eb92fa79c fix(telegram): clean rich message CI gates 2026-06-13 21:45:22 +05:30
Ayaan Zaidi
f1e303404c feat(telegram): nudge agents toward rich text 2026-06-13 21:45:22 +05:30
Ayaan Zaidi
80d2b40fac fix(telegram): keep rich text media-free 2026-06-13 21:45:22 +05:30
Ayaan Zaidi
a3bc0097c8 fix(telegram): migrate retired native draft config 2026-06-13 21:45:22 +05:30
Ayaan Zaidi
93318050e1 test(telegram): cover rich list and table limits 2026-06-13 21:45:22 +05:30
Ayaan Zaidi
18fbcef496 fix(chunking): preserve surrogate pairs 2026-06-13 21:45:22 +05:30
Ayaan Zaidi
e8b142feb1 refactor(telegram): remove native draft previews 2026-06-13 21:45:22 +05:30
Ayaan Zaidi
547cc0f109 feat(telegram): send text as rich messages 2026-06-13 21:45:22 +05:30
zhouhe-xydt
bb71f46251 fix(ui): preserve reset soft command args
Fixes #91316

Summary:
- Preserve `/reset soft ...` arguments when Control UI dispatches the local reset command.
- Reuse parsed slash-command semantics for reset confirmation detection.
- Keep non-soft reset tails on the destructive confirmation path across whitespace and colon separators.

Verification:
- `node_modules/.bin/oxfmt --check --threads=1 ui/src/ui/app-chat.ts ui/src/ui/app-chat.test.ts`
- `node scripts/run-oxlint.mjs ui/src/ui/app-chat.ts ui/src/ui/app-chat.test.ts`
- `node scripts/run-vitest.mjs ui/src/ui/app-chat.test.ts --maxWorkers=1 -t 'reset soft|reset softish|typed /reset command dispatch'`
- Autoreview clean
- AWS Crabbox `run_fbaf31b3fff8` focused Node 24 regression proof
- AWS Crabbox `run_eb3af5b92e42` remote `check:changed`
- Exact-head CI green for `5dee6f488fd393cb2127fe152f0d3fd53ccc13d2`
2026-06-13 23:55:59 +08:00
Vincent Koc
a6aa84f2d0 test(plugins): avoid brittle provider ref error text 2026-06-13 23:30:38 +08:00
Andy Ye
3b94949437 fix(agents): deliver generated media completions in webchat
Fixes #91003

Add explicit generated-media directives to completion handoff prompts and treat real attachment payloads as visible session-only delivery evidence for dashboard/webchat completions. Hardened maintainer follow-up keeps malformed attachment arrays from masking failed delivery and keeps generated MEDIA directive values single-line sanitized.

Proof: focused local format/lint/Vitest, clean final autoreview, Crabbox AWS focused proof run_32499eb46b33, Crabbox AWS check:changed run_af46879ffbd1, and exact-head GitHub CI green for f8e6f4a04e.
2026-06-13 23:21:08 +08:00
Vincent Koc
45056a463a fix(test): extend watchdog for gateway core shard 2026-06-13 23:01:11 +08:00
ZengWen-DT
c773d8cd8e fix(cron): de-duplicate main-session heartbeat events
Fixes #44922

Preserve heartbeat-owned cron reminders as a single model input during heartbeat runs while keeping normal-turn fallback delivery when a heartbeat is skipped.

Proof: focused local Vitest/oxlint/format, clean autoreview, Crabbox AWS run_67abc286250a, Crabbox AWS check:changed run_bddebf014d58, and exact-head GitHub CI green for 341e807d7a.
2026-06-13 22:49:48 +08:00
Vincent Koc
eb1b640854 test(config): contain shell env fallback in config write tests 2026-06-13 22:22:01 +08:00
Andy Ye
ddacb7ba39 fix(memory): keep memory_search in transient qmd mode (#92639)
Summary:
- Merged fix(memory): keep memory_search in transient qmd mode after ClawSweeper review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(memory): close transient search managers
- PR branch already contained follow-up commit before automerge: fix(memory): preserve default search managers
- PR branch already contained follow-up commit before automerge: fix(memory): preserve qmd cli boot freshness

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

Prepared head SHA: 64fe82c24c
Review: https://github.com/openclaw/openclaw/pull/92639#issuecomment-4698763950

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-06-13 14:14:54 +00:00
Song Zhenlin
762d8d8e64 fix(feishu): clear client cache on test SDK swap
Clear cached Feishu clients when the test runtime replaces the SDK, preventing stale clients from leaking across test setup. Adds regression coverage for the SDK swap path. Fixes #83911.
2026-06-13 22:01:15 +08:00
Anson_H
205ab8d4bd perf(terminal): reuse ANSI truncation scanner
Reuse one module-level ANSI/OSC scanner during visible-width truncation and reset scanner state between calls. Keeps styled, plain, and OSC-8 truncation behavior covered by regression tests.
2026-06-13 21:54:53 +08:00
Ayaan Zaidi
7994880864 fix(usage): suppress unknown total-only cost 2026-06-13 19:16:02 +05:30
Ayaan Zaidi
afe75b3387 fix(usage): warn on broken footer templates 2026-06-13 19:16:02 +05:30
Ayaan Zaidi
84cbaf1832 fix(usage): preserve partial footer counts 2026-06-13 19:16:02 +05:30
Ayaan Zaidi
5892dc8522 docs(usage): avoid unsupported duration template path 2026-06-13 19:16:02 +05:30
Ayaan Zaidi
a55accb4b6 fix(usage): reject empty footer templates 2026-06-13 19:16:02 +05:30
Ayaan Zaidi
cdd71103c9 test(usage): align full footer contract 2026-06-13 19:16:02 +05:30
Ayaan Zaidi
7328caba82 fix(usage): simplify default full footer 2026-06-13 19:16:02 +05:30
Peter Lindsey
3ec16bbad3 feat(usage): merge user footer templates over the default + ship full scale palette + docs
- messages.usageTemplate now layers OVER the built-in default (objects
  merge by key, arrays/scalars replace), like other openclaw config
  objects, so a user template only needs the delta it adds/changes.
- Default ships the full scale palette (braille/block/shade/moon/level/
  weather/plants/moons6); users add more by name.
- Document the template format end to end (the "default" sentinel, merge
  behavior, the contract paths, verb table, piece forms, a worked example)
  in docs/concepts/usage-tracking.md — previously unauthorable from docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:16:02 +05:30
Peter Lindsey
cc831f8684 feat(usage): built-in "default" footer template (hidden)
Set messages.usageTemplate to the sentinel "default" to render a
good-looking built-in /usage full footer without supplying a template.
Intentionally undocumented in the config schema/help for now; a path or
inline object still overrides, and unset keeps the built-in line.

The default lives in source (default-template.ts) rather than a shipped
JSON so it stays in lockstep with the renderer. It keeps the 📚
context-window bar; it does not render limits/reset windows (the merged
PluginHookReplyUsageState carries no limits data).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:16:02 +05:30
Jayesh Betala
89cc175b2e fix(disk-space): promote rounded GiB boundary
Round MiB before selecting the display unit so low-disk warnings do not render boundary values as 1024 MiB. Adds regression coverage for the GiB boundary. Fixes #90245.
2026-06-13 21:45:51 +08:00
Ayaan Zaidi
3c02c239b4 test(openai): type storeless responses replay cases 2026-06-13 19:13:38 +05:30
Ayaan Zaidi
7359206b76 refactor(openai): simplify storeless replay gating 2026-06-13 19:13:38 +05:30
snowzlm
37d6fd2e81 test(OpenAI Responses): cover storeless replay compatibility 2026-06-13 19:13:38 +05:30
snowzlm
8ecf55b36a fix(OpenAI Responses): gate replay when store is stripped 2026-06-13 19:13:38 +05:30
Song Zhenlin
2e8a2d617d fix(browser): remove dead requireRef navigation import
Remove the unused requireRef import and void anchor from Browser navigation command registration while keeping navigate/resize registration covered by regression tests. Fixes #83878.
2026-06-13 21:39:51 +08:00
Vincent Koc
27e24ca683 fix(test): extend watchdog for slow vitest shards 2026-06-13 21:37:57 +08:00
huangjianxiong
68e234f9e2 fix(cli): preserve usage-error exits for lazy reparses
Reparse nested lazy commands from the Commander root so unknown options keep the original argv and exit non-zero. Adds nested lazy-command coverage for the root rawArgs path. Fixes #92069.
2026-06-13 21:33:30 +08:00
clawsweeper[bot]
5854e0c8f6 fix: split image setup and request timeout semantics (#92673)
Summary:
- The PR separates image media-understanding setup and provider request timeout handling, adds focused timeout regression tests, and updates gateway/Codex docs for the existing image timeout setting.
- PR surface: Source +39, Tests +67, Docs +8. Total +114 across 5 files.
- Reproducibility: yes. Source inspection shows current main subtracts setup elapsed time from the provider request timeout, and the PR adds a slow-setup regression test that exercises the failure path.

Automerge notes:
- PR branch already contained follow-up commit before automerge: docs: clarify image timeout phase semantics
- PR branch already contained follow-up commit before automerge: fix: bound image setup timeout separately
- PR branch already contained follow-up commit before automerge: Revert "fix: bound image setup timeout separately"
- PR branch already contained follow-up commit before automerge: fix: split image setup and request timeout semantics

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

Prepared head SHA: 001dee3fb0
Review: https://github.com/openclaw/openclaw/pull/92673#issuecomment-4698582136

Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-13 13:29:45 +00:00
Mason Huang
eaeedbf1f9 fix(docs): finalize i18n postprocess before skip (#92668)
Summary:
- Merged fix(docs): finalize i18n postprocess before skip after ClawSweeper review.

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

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

Prepared head SHA: ad79445835
Review: https://github.com/openclaw/openclaw/pull/92668#issuecomment-4698629026

Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
2026-06-13 13:17:03 +00:00
Rishi Tamrakar
dc493bc9a2 fix(slack): emit message_sent on outbound replies (#89943)
Emit terminal Slack message_sent and message:sent hooks across normal, streaming, preview, fallback, slash, failure, and TTS reply paths with canonical session/target correlation and one outcome per logical payload.

Fixes #89942

Co-authored-by: Rishi Tamrakar <rishi.ktamrakar@gmail.com>
2026-06-13 06:10:51 -07:00
zhang-guiping
78c66742ab fix(agents): expose session identity in runtime prompts
Expose session key and stable session id in Runtime prompt metadata for embedded, CLI-backed, and command-generated agent prompts so agents do not infer session identity. Fixes #92453.
2026-06-13 21:03:38 +08:00
2429 changed files with 212099 additions and 39480 deletions

View File

@@ -24,7 +24,7 @@ Use when:
- Prefer small fixes at the right ownership boundary; no refactor unless it clearly improves the bug class.
- When an accepted finding shows a bug class or repeated pattern, inspect the current PR scope for sibling instances before fixing.
- Fix the scoped bug class at once when practical; stop at touched surfaces, owner boundaries, and clear follow-up territory.
- Keep going until structured review returns no accepted/actionable findings.
- Keep going until structured review returns no accepted/actionable findings only while the work remains inside the original task scope.
- If a review-triggered fix changes code, rerun focused tests and rerun the structured review helper.
- For security-audit suppression changes, verify accepted findings remain auditable: suppressed findings stay in structured output, active output keeps an unsuppressible suppression notice, and aggregate findings cannot hide unrelated active risk.
- Never switch or override the requested review engine/model. If the review hits model capacity, retry the same command a few times with the same engine/model.
@@ -43,6 +43,42 @@ Use when:
- If Gitcrawl reports a portable manifest mismatch, source/runtime DB health error, or stale portable-store checkout, run `gitcrawl doctor --json` and inspect `source_db_health`, `runtime_db_health`, and `portable_store_status` before falling back to live GitHub.
- Do not push just to review. Push only when the user requested push/ship/PR update.
## Scope Governor
Autoreview is a closeout gate, not permission to rewrite the task.
Before the first review, freeze a scope baseline: original request or issue, target branch, intended behavior, owner boundary, changed files, and non-test LOC. For inherited or already-bloated branches, use the intended PR diff as the baseline rather than accepting all existing branch drift.
Before patching a finding, classify it:
- **In-scope blocker**: the finding is introduced by the current diff, affects the same owner boundary, and can be fixed without changing the task's contract.
- **Follow-up**: the finding is real but belongs to an adjacent bug class, sibling surface, cleanup, or broader hardening track.
- **Stop-and-escalate**: the finding requires a new protocol/config/storage/public API contract, a different owner boundary, a release-process change, or a design choice outside the original request.
Stop patching and report the scope break instead of continuing when:
- a narrow PR turns into an architecture change, protocol change, migration, or release-process change;
- the diff grows past 2x the original files or non-test LOC without explicit approval to expand scope;
- two review-triggered patch cycles have not converged; pause and reclassify every remaining finding before another edit;
- the best fix is "define the canonical contract first" rather than another local inference layer;
- fixing the accepted finding would make the PR no longer describe the same behavior, issue, or owner boundary.
After the two-cycle pause, continue only when every remaining accepted finding is still an in-scope blocker. Otherwise preserve the useful analysis, identify the smallest safe landed subset if one exists, and open or request a follow-up for the larger fix. Do not keep committing speculative fixes just to satisfy the reviewer.
Do not stack or push review-triggered fix commits while scope classification or focused proof is unresolved. Keep exploratory edits local until the cycle is proven in scope; if scope breaks, remove them from the landing lane instead of preserving them as branch history.
Critical exceptions must be explicit: active data loss, crash, broken install/upgrade, release blocker, or concrete security exposure. If the exception is not one of those, it is not critical enough to blow up scope.
## Release Branches And Release Process
On release, beta, stable, hotfix, signing, notarization, appcast, package-publish, or release-check work, use freeze discipline even when the branch name is not release-like:
- Fix only release blockers, failed release infrastructure, exact backports, install/upgrade breakage, data loss, crashes, or concrete security exposure.
- Treat non-blocking autoreview findings as follow-ups for `main`, not reasons to broaden the release branch.
- Do not introduce new product behavior, config surface, protocol shape, migration, plugin ownership, docs narrative, or process policy unless it directly unblocks the release.
- Keep proof tied to the release target: exact branch/ref, failing check or shipped-risk reason, smallest command/proof, and whether the fix must also forward-port to `main`.
- If review discovers a real but non-critical design problem during release closeout, stop with a follow-up issue/PR plan; do not use the release branch as the refactor lane.
## Pick Target
Dirty local work:

View File

@@ -440,8 +440,36 @@ def load_datasets(args: argparse.Namespace) -> str:
return "\n\n".join(chunks)
def review_scope_policy() -> str:
return textwrap.dedent(
"""
Review scope discipline:
- This helper is a closeout gate. Do not turn a narrow patch into a broad
redesign request.
- Report a finding only when this diff introduces or exposes a concrete
defect that must be fixed before this target can land.
- If the best fix requires a new protocol, config, storage, public API,
release process, migration, owner-boundary move, or canonical contract,
say that directly in the finding and keep the finding tied to the
smallest changed line that proves the current patch is not landable.
- Do not ask for sibling-surface hardening, cleanup, refactors, or
follow-up architecture work unless the current diff is incorrect
without that work.
- Prefer the smallest correct pre-merge fix. A broader ideal design is
not an actionable finding unless the current patch cannot safely land.
- If this is release-branch or release-process work, apply freeze
discipline. Report only release blockers, exact backport regressions,
install/upgrade breakage, crashes, data loss, concrete security
exposure, or release-infrastructure failures. Non-blocking design,
cleanup, and hardening concerns belong on main as follow-ups.
"""
).strip()
def build_prompt(repo: Path, target: str, target_ref: str | None, bundle: str, extra_prompt: str, datasets: str) -> str:
target_line = f"{target} {target_ref}" if target_ref else target
branch = current_branch(repo)
scope_policy = review_scope_policy()
return textwrap.dedent(
f"""
You are a senior code reviewer. Review the provided git change bundle only.
@@ -463,8 +491,11 @@ def build_prompt(repo: Path, target: str, target_ref: str | None, bundle: str, e
- If there are no actionable findings, return an empty findings array and mark the patch correct.
Review target: {target_line}
Current branch: {branch}
Repository: {repo}
{scope_policy}
{extra_prompt}
{datasets}

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import argparse
import os
import runpy
import shutil
import stat
import subprocess
@@ -145,8 +146,23 @@ def create_fixture_repo(repo: Path, fixture: str) -> None:
write_fixture_file(repo, MALICIOUS_CHANGED if fixture == "malicious" else BENIGN_CHANGED)
def validate_prompt_policy(repo: Path, autoreview: Path) -> None:
namespace = runpy.run_path(str(autoreview))
prompt = namespace["build_prompt"](repo, "local", None, "fixture diff", "", "")
required = (
"This helper is a closeout gate.",
"Do not turn a narrow patch into a broad",
"If this is release-branch or release-process work",
"Non-blocking design,",
)
missing = [needle for needle in required if needle not in prompt]
if missing:
raise RuntimeError(f"autoreview prompt missing scope policy: {missing}")
def run_reviews(repo: Path, script_dir: Path, fixture: str, engines: list[str]) -> None:
autoreview = script_dir / "autoreview"
validate_prompt_policy(repo, autoreview)
for engine in engines:
print(f"== {engine} ==", flush=True)
command = [

View File

@@ -54,6 +54,13 @@ pnpm crabbox:run -- --help | sed -n '1,120p'
- For broad OpenClaw maintainer `pnpm` gates, prefer the repo wrapper with
`--provider blacksmith-testbox` or the repo Testbox helpers when the standing
Testbox policy applies.
- Cold Testbox acquisition and hydration often take tens of seconds. When broad
remote proof is likely, immediately start
`node scripts/crabbox-wrapper.mjs warmup --provider blacksmith-testbox --keep --timing-json`
in a background command session while inspecting, editing, and running
focused local tests. Poll later, reuse the returned `tbx_...` with
`--provider blacksmith-testbox --id <tbx_id>`, and stop it before handoff.
Do not warm speculatively when remote proof is unlikely.
- Always report the actual provider and id. `cbx_...` means AWS Crabbox;
`tbx_...` means Blacksmith Testbox through Crabbox. If the output only says
`blacksmith testbox list`, use `blacksmith testbox list --all` before

View File

@@ -0,0 +1,51 @@
---
name: discord-user-post
description: Post an approved message as the logged-in Discord user through the Discord desktop app. Use for release announcements or other direct user-authored Discord posts; not for OpenClaw channel sends, bots, webhooks, relays, agent sessions, or archive search.
---
# Discord User Post
Use `$computer-use` to operate `/Applications/Discord.app` in the user's
existing logged-in session. This workflow represents the user directly.
## Prepare
1. Draft the complete final message outside Discord.
2. Confirm the intended server and channel with the user when either is
ambiguous.
3. Open Discord and navigate to the exact destination without entering the
message.
4. Verify the visible server name, channel header, and logged-in account.
Do not infer the target from unrelated Discord content. Stop if Discord is not
logged in, the account is wrong, or the exact destination cannot be verified.
## Confirm and Post
Posting is representational communication. Follow the `$computer-use`
confirmation policy even when the user previously asked for an announcement:
1. Show the user the exact final body and verified destination.
2. Request action-time confirmation before typing into Discord.
3. After confirmation, enter the approved body unchanged.
4. Visually inspect the composed message and destination again.
5. Send once.
If the body or destination changes after confirmation, request confirmation
again before sending.
## Verify
- Confirm the message appears once, from the user's account, in the intended
channel.
- Report the server, channel, and visible send result.
- Do not edit, delete, react, or send a follow-up without the corresponding
user instruction and confirmation.
## Guardrails
- Never use `openclaw message`, an OpenClaw agent, a Discord bot, webhook, relay,
or token for this workflow.
- Never expose private Discord content or account details in public output.
- Never send a draft, partial message, duplicate, or unreviewed attachment.
- For Discord archive/history/search, use `$discrawl` instead.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Discord User Post"
short_description: "Post approved messages through the logged-in Discord app"
default_prompt: "Post this approved message as me through the logged-in Discord desktop app."

View File

@@ -284,7 +284,7 @@ gh search issues --repo openclaw/openclaw --match title,body --limit 50 \
- If bot review conversations exist on your PR, address them and resolve them yourself once fixed.
- Leave a review conversation unresolved only when reviewer or maintainer judgment is still needed.
- Before landing any PR with non-trivial code changes, run `$autoreview` until no accepted/actionable findings remain, unless equivalent manual review already covered it, the change is trivial/docs-only, or the user opts out.
- When landing or merging any PR, follow the global `/landpr` process.
- When an agent is landing or merging a PR targeting `main`, use only the repo-native `scripts/pr` wrapper: run `scripts/pr review-init <PR>`, follow its emitted checkout/guard guidance, initialize and complete review artifacts with `scripts/pr review-artifacts-init <PR>`, validate them with `scripts/pr review-validate-artifacts <PR>`, then run `scripts/pr prepare-run <PR>` and `scripts/pr merge-run <PR>`.
- Use `scripts/committer "<msg>" <file...>` for scoped commits instead of manual `git add` and `git commit`.
- Keep commit messages concise and action-oriented.
- Group related changes; avoid bundling unrelated refactors.

View File

@@ -13,7 +13,7 @@ Use this skill for `qa-lab` / `qa-channel` work. Repo-local QA only.
- `docs/help/testing.md`
- `docs/channels/qa-channel.md`
- `qa/README.md`
- `qa/scenarios/index.md`
- `qa/scenarios/index.yaml`
- `extensions/qa-lab/src/suite.ts`
- `extensions/qa-lab/src/character-eval.ts`
@@ -198,7 +198,9 @@ pnpm openclaw qa character-eval \
- Judges default to `openai/gpt-5.4,thinking=xhigh,fast` and `anthropic/claude-opus-4-6,thinking=high`.
- Report includes judge ranking, run stats, durations, and full transcripts; do not include raw judge replies. Duration is benchmark context, not a grading signal.
- Candidate and judge concurrency default to 16. Use `--concurrency <n>` and `--judge-concurrency <n>` to override when local gateways or provider limits need a gentler lane.
- Scenario source should stay markdown-driven under `qa/scenarios/`.
- Scenario source is YAML-only under `qa/scenarios/`: use `index.yaml` and
per-scenario `*.yaml` files with top-level `title`, `scenario`, and optional
`flow`. Never add fenced `qa-scenario` / `qa-flow` Markdown files.
- For isolated character/persona evals, write the persona into `SOUL.md` and blank `IDENTITY.md` in the scenario flow. Use `SOUL.md + IDENTITY.md` only when intentionally testing how the normal OpenClaw identity combines with the character.
- Keep prompts natural and task-shaped. The candidate model should receive character setup through `SOUL.md`, then normal user turns such as chat, workspace help, and small file tasks; do not ask "how would you react?" or tell the model it is in an eval.
- Prefer at least one real task, such as creating or editing a tiny workspace artifact, so the transcript captures character under normal tool use instead of pure roleplay.
@@ -234,7 +236,8 @@ pnpm openclaw qa manual \
## Repo facts
- Seed scenarios live in `qa/`.
- Seed scenarios live in `qa/scenarios/index.yaml` and
`qa/scenarios/<theme>/*.yaml`.
- Main live runner: `extensions/qa-lab/src/suite.ts`
- QA lab server: `extensions/qa-lab/src/lab-server.ts`
- Child gateway harness: `extensions/qa-lab/src/gateway-child.ts`
@@ -262,8 +265,9 @@ pnpm openclaw qa manual \
## When adding scenarios
- Add or update scenario markdown under `qa/scenarios/`
- Keep kickoff expectations in `qa/scenarios/index.md` aligned
- Add or update scenario YAML under `qa/scenarios/`; do not add `.md` scenario
files or fenced YAML blocks.
- Keep kickoff expectations in `qa/scenarios/index.yaml` aligned
- Add executable coverage in `extensions/qa-lab/src/suite.ts`
- Prefer end-to-end assertions over mock-only checks
- Save outputs under `.artifacts/qa-e2e/`

View File

@@ -6,7 +6,8 @@ description: "Draft or post OpenClaw beta/stable Discord release announcements f
# OpenClaw Release Announcement
Use with `release-openclaw-maintainer` after a beta or stable release is live.
Use with `openclaw-discord` when actually posting to Discord.
Use with `$discord-user-post` when actually posting to Discord as the logged-in
user.
## Evidence First
@@ -80,6 +81,7 @@ Fresh installs still point to `https://openclaw.ai`.
## Posting
When asked to post, use the configured Discord workflow from
`openclaw-discord` or the approved OpenClaw relay. Never print tokens.
For public channels, inspect the final body before sending.
When asked to post, use `$discord-user-post` to operate the logged-in Discord
desktop app as the user. Resolve and visibly verify the exact server/channel,
inspect the final body, and request action-time confirmation before entering or
sending it. Never use OpenClaw channel sends, bots, webhooks, relays, or tokens.

View File

@@ -16,6 +16,10 @@ Use this with `$release-openclaw-maintainer` and `$openclaw-testing` when a rele
- Watch one parent run plus compact child summaries. Avoid broad `gh run view` polling loops; REST quota is easy to burn.
- Fetch logs only for failed or currently-blocking jobs. If quota is low, stop polling and wait for reset.
- Treat live-provider flakes separately from code failures: prove key validity, provider HTTP status, retry evidence, and exact failing lane before editing code.
- A model-list response proves authentication, not billing or inference
entitlement. Mandatory live providers must pass a real completion probe
before release dispatch. Fix the credential first; do not add an alternate
auth path merely to bypass a failed release credential.
- Full Release Validation parent monitors fail fast: once a required child job
fails, the parent cancels the remaining child matrix and prints the failed
job summary. Inspect that first red job instead of waiting for unrelated
@@ -36,6 +40,8 @@ git rev-parse HEAD
preflight. Inject those exact targeted keys first, then run the verifier; use
ambient env only when it was already intentionally injected for this release.
The script prints only provider status and HTTP class, never tokens.
The Anthropic check performs a tiny message completion so exhausted or
non-billable credentials fail before the expensive release matrix.
## Dispatch
@@ -65,6 +71,13 @@ gh workflow run openclaw-performance.yml \
Prefer the trusted workflow on `main`, target the exact release SHA:
- Keep trusted-workflow checks compatible with frozen release targets. If
`main` adds a target-owned guard script or package command after the release
branch cut, make the trusted workflow skip only when that target surface is
absent. Heal the trusted workflow before rerunning validation; do not port an
unrelated runtime refactor or mutate the release candidate just to satisfy a
newer `main`-only check.
```bash
gh workflow run full-release-validation.yml \
--repo openclaw/openclaw \
@@ -106,7 +119,10 @@ Stop watchers before ending the turn or switching strategy.
--jq '.jobs[] | select(.conclusion=="failure" or .conclusion=="timed_out" or .conclusion=="cancelled") | [.databaseId,.name,.conclusion,.url] | @tsv'
```
3. Fetch one failed job log. If rate-limited, note reset time and avoid more REST calls.
4. For secret-looking failures, validate the provider endpoint from the same secret source before editing code.
4. For secret-looking failures, validate a real completion from the same secret source before editing code. A successful model-list request is insufficient.
Claude CLI subscription credentials are a separate native auth path; prove
them in a clean-home CLI probe, never as a substitute for a required
Anthropic API-key lane.
5. For live-cache failures, inspect whether it is missing/invalid key, empty text, provider refusal, timeout, or baseline miss. Do not weaken release gates without clear provider evidence.
6. Fix narrowly, run local/changed proof, commit, push, rerun the smallest matching group.

View File

@@ -1,17 +1,22 @@
#!/usr/bin/env node
/**
* Release preflight helper that verifies required provider API keys can reach
* their model-list endpoints without printing secret values.
* Release preflight helper that verifies required provider API keys without
* printing secret values. Anthropic must complete a prompt because model-list
* access does not prove billing or inference entitlement.
*/
import process from "node:process";
const args = new Map();
for (let index = 2; index < process.argv.length; index += 1) {
const arg = process.argv[index];
if (!arg.startsWith("--")) continue;
if (!arg.startsWith("--")) {
continue;
}
const [key, inlineValue] = arg.slice(2).split("=", 2);
const value = inlineValue ?? process.argv[index + 1];
if (inlineValue === undefined) index += 1;
if (inlineValue === undefined) {
index += 1;
}
args.set(key, value);
}
@@ -28,7 +33,9 @@ const timeoutMs = Number(args.get("timeout-ms") ?? 10_000);
function envFirst(names) {
for (const name of names) {
const value = process.env[name]?.trim();
if (value) return { name, value };
if (value) {
return { name, value };
}
}
return undefined;
}
@@ -44,13 +51,19 @@ async function checkProvider(id, config) {
try {
const headers = config.headers(secret.value);
const response = await fetch(config.url, {
body: config.body,
headers,
method: config.method,
signal: controller.signal,
});
const responseBody = config.validateResponse
? await response.json().catch(() => undefined)
: undefined;
const ok = response.ok && (!config.validateResponse || config.validateResponse(responseBody));
return {
id,
ok: response.ok,
status: response.ok ? "ok" : `http_${response.status}`,
ok,
status: response.ok ? (ok ? "ok" : "invalid_response") : `http_${response.status}`,
env: secret.name,
};
} catch (error) {
@@ -73,11 +86,21 @@ const providers = {
},
anthropic: {
env: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_TOKEN"],
url: "https://api.anthropic.com/v1/models",
url: "https://api.anthropic.com/v1/messages",
method: "POST",
body: JSON.stringify({
max_tokens: 8,
messages: [{ role: "user", content: "Reply with OK." }],
model: "claude-haiku-4-5",
}),
headers: (token) => ({
"anthropic-version": "2023-06-01",
"content-type": "application/json",
"x-api-key": token,
}),
validateResponse: (body) =>
Array.isArray(body?.content) &&
body.content.some((part) => typeof part?.text === "string" && part.text.trim()),
},
fireworks: {
env: ["FIREWORKS_API_KEY"],
@@ -108,7 +131,9 @@ let failed = false;
for (const result of results) {
const requiredLabel = required.has(result.id) ? "required" : "optional";
console.log(`${result.id}: ${result.status} env=${result.env} ${requiredLabel}`);
if (required.has(result.id) && !result.ok) failed = true;
if (required.has(result.id) && !result.ok) {
failed = true;
}
}
if (failed) {

View File

@@ -150,9 +150,21 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
- Stable Windows Hub release closeout requires the signed
`OpenClawCompanion-Setup-x64.exe`, `OpenClawCompanion-Setup-arm64.exe`, and
`OpenClawCompanion-SHA256SUMS.txt` assets on the canonical
`openclaw/openclaw` GitHub Release. Use the public `Windows Node Release`
workflow after the matching `openclaw/openclaw-windows-node` release exists;
it verifies Authenticode signatures on Windows before uploading assets.
`openclaw/openclaw` GitHub Release. Pass the exact signed
`openclaw/openclaw-windows-node` release tag as `windows_node_tag` to
`OpenClaw Release Publish`, together with the candidate-approved
`windows_node_installer_digests` map; it prevalidates the published source
release and required installers against that map before any publish child,
dispatches the public `Windows Node Release` workflow while the OpenClaw
release is still a draft, carries those pinned source asset digests
unchanged, verifies the expected OpenClaw Foundation Authenticode signer on
Windows, re-downloads and checksum-verifies the promoted asset contract, and
blocks publication until the canonical asset contract is present. Use direct
`Windows Node Release` dispatch only for recovery, always with an exact tag,
never `latest`, and the explicit `expected_installer_digests` JSON map from
the approved source release. Recovery rejects unexpected
`OpenClawCompanion-*` target asset names, then replaces the expected contract
assets with the pinned source bytes.
- Website Windows Hub download links should target exact canonical
`openclaw/openclaw/releases/download/vYYYY.M.PATCH/...` assets for the current
stable release, or `releases/latest/download/...` only after verifying the
@@ -309,6 +321,7 @@ Upgrade with the beta channel.
Before tagging or publishing, run:
```bash
pnpm release:fast-pretag-check
pnpm check:architecture
pnpm build
pnpm ui:build
@@ -317,6 +330,21 @@ pnpm release:check
pnpm test:install:smoke
```
- Treat `pnpm release:fast-pretag-check` as a hard packaging gate. Every
publishable plugin must have a non-empty package-root `README.md`, build its
package-local runtime, and pass the npm and ClawHub release metadata checks
before a tag or publish workflow can start. Do not defer README, entrypoint,
or packed-artifact failures to postpublish verification.
- Before tagging, require green CI for the exact release-candidate SHA, not an
earlier branch SHA. Heal every related red CI, release-check, packaging, or
root-Dockerfile lane on the release branch, forward-port the fix to `main`,
and rerun the affected exact-SHA gates. Never waive a red Docker lane because
npm preflight passed.
- Root Dockerfile proof is mandatory before every beta and stable tag. Run the
release `install-smoke` group or equivalent root Dockerfile build for the
exact candidate SHA and require it to pass. The tag-triggered Docker Release
workflow is post-tag publishing, not the first valid proof that the root
Dockerfile can build.
- Before tagging, diff publishable plugin package manifests against the last
reachable stable/beta release tag. For every newly publishable package
(`openclaw.release.publishToNpm: true` or `publishToClawHub: true`) whose
@@ -524,6 +552,16 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- `preflight_only=true` on the npm workflow is also the right way to validate an
existing tag after publish; it should keep running the build checks even when
the npm version is already published.
- npm registry metadata is eventually consistent immediately after trusted
publishing. Keep postpublish `npm view` checks on bounded `--prefer-online`
retries, and carry that verified tarball/integrity metadata into later proof
steps instead of reading the registry again. If the OpenClaw npm child
succeeded but the parent publish workflow failed on an immediate exact-version
`E404`, verify the exact version with a cache-bypassed registry read, run the
standalone postpublish verifier and the full beta verifier with the original
successful child run IDs, then finalize the draft, dependency evidence asset,
and release proof manually. Never rerun the publish workflow for that
already-published version.
- npm validation-only preflight may still be dispatched from ordinary branches
when testing workflow changes before merge. Release checks and real publish
use only `main` or `release/YYYY.M.PATCH`.
@@ -632,9 +670,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
off, live OpenAI off, and regression failure off. Let it run in parallel
with preflight and validation work.
10. Run the fast local beta preflight from the release branch before any npm
preflight or publish. Keep expensive Docker, Parallels, and published-package
install/update lanes for after the beta is live unless the operator asks to
run them before beta publication.
preflight or publish. Require exact-SHA CI and root Dockerfile install-smoke
to be green before tagging. Keep the remaining expensive Docker, Parallels,
and published-package install/update lanes for after the beta is live unless
the operator asks to run them before beta publication.
11. For beta releases, skip mac app build/sign/notarize unless beta scope or a
release blocker specifically requires it. For stable releases, include the
mac app, signing, notarization, and appcast path.
@@ -675,20 +714,29 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
where npm did not publish the beta version, delete/recreate the same beta
tag and any accidental draft/incomplete prerelease at the fixed commit
instead of skipping a prerelease number.
22. Start `.github/workflows/openclaw-npm-release.yml` from the same branch with
22. Start `.github/workflows/openclaw-release-publish.yml` from the same branch with
the same tag for the real publish, choose `npm_dist_tag` (`beta` default,
`latest` only when you intentionally want direct stable publish), keep it
the same as the preflight run, and pass the successful npm
`preflight_run_id`.
`preflight_run_id` plus the successful `full_release_validation_run_id`.
For stable publish, also pass the exact non-prerelease
`openclaw/openclaw-windows-node` tag as `windows_node_tag` and its
candidate-approved installer digest map as `windows_node_installer_digests`.
23. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
24. Wait for the real publish workflow to run postpublish verification,
create or update the GitHub release as a draft, upload dependency evidence,
promote and verify the required Windows Hub assets for stable releases,
append release verification proof, and only then undraft/publish it. If a
waited plugin publish fails after OpenClaw npm succeeds, the workflow keeps
the release draft with OpenClaw npm evidence and exits red; do not undraft
until the plugin publish gap is repaired. The standalone verifier command
remains the recovery probe:
waited plugin publish or Windows Hub promotion fails after OpenClaw npm
succeeds, the workflow keeps the release draft with OpenClaw npm evidence
and exits red; do not undraft until the gap is repaired. The standalone
verifier command remains the first recovery probe:
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>`.
For a failed postpublish parent after successful publish children, also run
`pnpm release:verify-beta -- <published-version> ... --skip-github-release`
with the original child run IDs and an evidence output path before manually
recreating the workflow's draft, dependency evidence asset, proof section,
and publish step.
25. Run the post-published beta verification roster. First scan current `main`
for critical fixes that landed after the release branch cut; backport only
important low-risk fixes before starting expensive lanes, or increment to

View File

@@ -1,61 +0,0 @@
name: openclaw-codeql-process-exec-boundary-critical-security
disable-default-queries: true
queries:
- uses: security-extended
query-filters:
- include:
precision:
- high
- very-high
tags contain: security
security-severity: /([7-9]|10)\.(\d)+/
paths:
- src/process
- src/tui/tui-local-shell.ts
- src/tui/tui.ts
- src/plugin-sdk/windows-spawn.ts
- packages/agent-core/src/harness/env
- packages/memory-host-sdk/src/host
- extensions/acpx/src
- extensions/bonjour/src/advertiser.ts
- extensions/browser/src/browser/chrome-mcp.ts
- extensions/browser/src/browser/chrome.executables.ts
- extensions/browser/src/browser/chrome.ts
- extensions/codex/src/app-server/sandbox-exec-server
- extensions/codex/src/app-server/transport-stdio.ts
- extensions/codex/src/node-cli-sessions.ts
- extensions/codex-supervisor/src/json-rpc-client.ts
- extensions/file-transfer/src
- extensions/google-meet/src
- extensions/imessage/src
- extensions/memory-core/src/memory/qmd-manager.ts
- extensions/memory-wiki/src/obsidian.ts
- extensions/microsoft-foundry/cli.ts
- extensions/ollama/src/wsl2-crash-loop-check.ts
- extensions/qa-lab/src
- extensions/signal/src/daemon.ts
- extensions/tts-local-cli/speech-provider.ts
- extensions/voice-call/src
- scripts
paths-ignore:
- "**/node_modules"
- "**/coverage"
- "**/*.generated.ts"
- "**/*.bundle.js"
- "**/*-runtime.js"
- "**/*.test.ts"
- "**/*.test.tsx"
- "**/*.spec.ts"
- "**/*.spec.tsx"
- "**/*.e2e.test.ts"
- "**/*.e2e.test.tsx"
- "**/*test-support*"
- "**/*test-helper*"
- "**/*mock*"
- "**/*fixture*"
- "**/*bench*"

View File

@@ -61,7 +61,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
@@ -188,7 +188,7 @@ jobs:
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=10s 30s git \
timeout --signal=TERM --kill-after=10s 120s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"

View File

@@ -76,7 +76,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
@@ -106,7 +106,7 @@ jobs:
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=10s 30s git \
timeout --signal=TERM --kill-after=10s 120s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"

View File

@@ -61,7 +61,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
@@ -91,7 +91,7 @@ jobs:
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=10s 30s git \
timeout --signal=TERM --kill-after=10s 120s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"

View File

@@ -90,7 +90,7 @@ jobs:
local ref="$1"
local fetch_status
for attempt in 1 2 3; do
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
timeout --signal=TERM --kill-after=10s 120s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=2 origin \
"+${ref}:refs/remotes/origin/checkout" && return 0
@@ -351,7 +351,7 @@ jobs:
local ref="$1"
local fetch_status
for attempt in 1 2 3; do
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
timeout --signal=TERM --kill-after=10s 120s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${ref}:refs/remotes/origin/checkout" && return 0
@@ -499,7 +499,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -564,7 +564,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -810,7 +810,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -899,7 +899,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -979,7 +979,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1056,7 +1056,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1131,7 +1131,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1258,7 +1258,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1288,6 +1288,7 @@ jobs:
env:
OPENCLAW_LOCAL_CHECK: "0"
TASK: ${{ matrix.task }}
PR_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }}
shell: bash
run: |
set -euo pipefail
@@ -1297,6 +1298,10 @@ jobs:
pnpm tool-display:check
pnpm check:host-env-policy:swift
pnpm dup:check:coverage
if [ -n "$PR_BASE_SHA" ]; then
git fetch --no-tags --depth=1 origin "+${PR_BASE_SHA}:refs/remotes/origin/pr-base"
node scripts/report-test-temp-creations.mjs --base refs/remotes/origin/pr-base --head HEAD --no-merge-base
fi
pnpm deps:patches:check
pnpm lint:webhook:no-low-level-body-read
pnpm lint:auth:no-pairing-store-group
@@ -1358,6 +1363,10 @@ jobs:
- check_name: check-additional-boundaries-bcd
group: boundaries
boundary_shard: 2/4,3/4,4/4
- check_name: check-session-accessor-boundary
group: session-accessor-boundary
- check_name: check-session-transcript-reader-boundary
group: session-transcript-reader-boundary
- check_name: check-additional-extension-channels
group: extension-channels
- check_name: check-additional-extension-bundled
@@ -1390,7 +1399,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1504,6 +1513,24 @@ jobs:
boundaries)
node scripts/run-additional-boundary-checks.mjs
;;
session-accessor-boundary)
if [ ! -f scripts/check-session-accessor-boundary.mjs ]; then
echo "[skip] session accessor boundary check is not present in this checkout"
elif ! node -e 'const pkg = require("./package.json"); process.exit(pkg.scripts?.["lint:tmp:session-accessor-boundary"] ? 0 : 1);'; then
echo "[skip] session accessor boundary script is not present in package.json"
else
run_check "lint:tmp:session-accessor-boundary" pnpm run lint:tmp:session-accessor-boundary
fi
;;
session-transcript-reader-boundary)
if [ ! -f scripts/check-session-transcript-reader-boundary.mjs ]; then
echo "[skip] session transcript reader boundary check is not present in this checkout"
elif ! node -e 'const pkg = require("./package.json"); process.exit(pkg.scripts?.["lint:tmp:session-transcript-reader-boundary"] ? 0 : 1);'; then
echo "[skip] session transcript reader boundary script is not present in package.json"
else
run_check "lint:tmp:session-transcript-reader-boundary" pnpm run lint:tmp:session-transcript-reader-boundary
fi
;;
extension-channels)
run_check "lint:extensions:channels" pnpm run lint:extensions:channels
;;
@@ -1557,7 +1584,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
@@ -1603,7 +1630,7 @@ jobs:
git -C "$workdir" config gc.auto 0
git -C "$workdir" remote add origin "https://github.com/openclaw/clawhub.git"
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+refs/heads/main:refs/remotes/origin/checkout" || return 1
@@ -1650,7 +1677,7 @@ jobs:
fetch_checkout_ref() {
local fetch_status
for attempt in 1 2 3; do
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
timeout --signal=TERM --kill-after=10s 120s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" && return 0
@@ -2056,7 +2083,7 @@ jobs:
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
timeout --signal=TERM --kill-after=10s 120s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1

View File

@@ -17,28 +17,7 @@ on:
- ".github/actions/**"
- ".github/codeql/**"
- ".github/workflows/**"
- "extensions/acpx/src/**"
- "extensions/bonjour/src/advertiser.ts"
- "extensions/browser/src/browser/chrome-mcp.ts"
- "extensions/browser/src/browser/chrome.executables.ts"
- "extensions/browser/src/browser/chrome.ts"
- "extensions/codex/src/app-server/sandbox-exec-server/**"
- "extensions/codex/src/app-server/transport-stdio.ts"
- "extensions/codex/src/node-cli-sessions.ts"
- "extensions/codex-supervisor/src/json-rpc-client.ts"
- "extensions/file-transfer/src/**"
- "extensions/google-meet/src/**"
- "extensions/imessage/src/**"
- "extensions/memory-core/src/memory/qmd-manager.ts"
- "extensions/memory-wiki/src/obsidian.ts"
- "extensions/microsoft-foundry/cli.ts"
- "extensions/ollama/src/wsl2-crash-loop-check.ts"
- "extensions/qa-lab/src/**"
- "extensions/signal/src/daemon.ts"
- "extensions/tts-local-cli/speech-provider.ts"
- "extensions/voice-call/src/**"
- "packages/**"
- "scripts/**"
- "src/**"
push:
branches:
@@ -47,28 +26,7 @@ on:
- ".github/actions/**"
- ".github/codeql/**"
- ".github/workflows/**"
- "extensions/acpx/src/**"
- "extensions/bonjour/src/advertiser.ts"
- "extensions/browser/src/browser/chrome-mcp.ts"
- "extensions/browser/src/browser/chrome.executables.ts"
- "extensions/browser/src/browser/chrome.ts"
- "extensions/codex/src/app-server/sandbox-exec-server/**"
- "extensions/codex/src/app-server/transport-stdio.ts"
- "extensions/codex/src/node-cli-sessions.ts"
- "extensions/codex-supervisor/src/json-rpc-client.ts"
- "extensions/file-transfer/src/**"
- "extensions/google-meet/src/**"
- "extensions/imessage/src/**"
- "extensions/memory-core/src/memory/qmd-manager.ts"
- "extensions/memory-wiki/src/obsidian.ts"
- "extensions/microsoft-foundry/cli.ts"
- "extensions/ollama/src/wsl2-crash-loop-check.ts"
- "extensions/qa-lab/src/**"
- "extensions/signal/src/daemon.ts"
- "extensions/tts-local-cli/speech-provider.ts"
- "extensions/voice-call/src/**"
- "packages/**"
- "scripts/**"
- "src/**"
schedule:
- cron: "0 6 * * *"
@@ -115,11 +73,6 @@ jobs:
runs_on: blacksmith-4vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-mcp-process-tool-boundary-critical-security.yml
- language: javascript-typescript
category: process-exec-boundary
runs_on: blacksmith-4vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-process-exec-boundary-critical-security.yml
- language: javascript-typescript
category: plugin-trust-boundary
runs_on: blacksmith-4vcpu-ubuntu-2404

View File

@@ -275,7 +275,7 @@ jobs:
local workflow="$1"
shift
local before_json dispatch_output run_id status conclusion url poll_count
local dispatch_output run_id status conclusion url poll_count
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
@@ -298,8 +298,6 @@ jobs:
printf '%s\n' "$output" >&2
return "$status"
}
before_json="$(gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh_with_retry workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@")"
printf '%s\n' "$dispatch_output"
run_id="$(
@@ -309,20 +307,7 @@ jobs:
)"
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
fi
if [[ -z "${run_id:-}" ]]; then
echo "Could not find dispatched run for ${workflow}." >&2
echo "::error::gh workflow run ${workflow} did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs." >&2
exit 1
fi
@@ -423,7 +408,7 @@ jobs:
local workflow="$1"
shift
local before_json dispatch_output run_id status conclusion url poll_count
local dispatch_output run_id status conclusion url poll_count
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
@@ -446,8 +431,6 @@ jobs:
printf '%s\n' "$output" >&2
return "$status"
}
before_json="$(gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh_with_retry workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@")"
printf '%s\n' "$dispatch_output"
run_id="$(
@@ -457,20 +440,7 @@ jobs:
)"
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
fi
if [[ -z "${run_id:-}" ]]; then
echo "Could not find dispatched run for ${workflow}." >&2
echo "::error::gh workflow run ${workflow} did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs." >&2
exit 1
fi
@@ -581,7 +551,7 @@ jobs:
local workflow="$1"
shift
local before_json dispatch_output run_id status conclusion url poll_count run_json
local dispatch_output run_id status conclusion url poll_count run_json
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
@@ -604,8 +574,6 @@ jobs:
printf '%s\n' "$output" >&2
return "$status"
}
before_json="$(gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
dispatch_output="$(gh_with_retry workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@")"
printf '%s\n' "$dispatch_output"
run_id="$(
@@ -615,20 +583,7 @@ jobs:
)"
if [[ -z "$run_id" ]]; then
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh_with_retry run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
fi
if [[ -z "${run_id:-}" ]]; then
echo "Could not find dispatched run for ${workflow}." >&2
echo "::error::gh workflow run ${workflow} did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs." >&2
exit 1
fi
@@ -783,7 +738,7 @@ jobs:
fi
args=(
-f ref="$TARGET_SHA"
-f ref="$TARGET_REF"
-f expected_sha="$TARGET_SHA"
-f provider="$PROVIDER"
-f mode="$MODE"
@@ -928,8 +883,6 @@ jobs:
return "$status"
}
before_json="$(gh_with_retry run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
args=(-f package_spec="${PACKAGE_SPEC:-openclaw@beta}" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
if [[ -z "${PACKAGE_SPEC// }" ]]; then
if [[ "$PREPARE_PACKAGE_RESULT" != "success" || -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then
@@ -946,22 +899,16 @@ jobs:
args+=(-f scenario="$SCENARIO")
fi
gh_with_retry workflow run npm-telegram-beta-e2e.yml --ref "$CHILD_WORKFLOW_REF" "${args[@]}"
run_id=""
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh_with_retry run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
dispatch_output="$(gh_with_retry workflow run npm-telegram-beta-e2e.yml --ref "$CHILD_WORKFLOW_REF" "${args[@]}")"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
if [[ -z "$run_id" ]]; then
echo "Could not find dispatched run for npm-telegram-beta-e2e.yml." >&2
echo "::error::gh workflow run npm-telegram-beta-e2e.yml did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs." >&2
exit 1
fi
@@ -1073,31 +1020,23 @@ jobs:
echo "- Release impact: advisory"
} >> "$GITHUB_STEP_SUMMARY"
before_json="$(gh_with_retry run list --workflow openclaw-performance.yml --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
gh_with_retry workflow run openclaw-performance.yml \
dispatch_output="$(gh_with_retry workflow run openclaw-performance.yml \
--ref "$CHILD_WORKFLOW_REF" \
-f target_ref="$TARGET_SHA" \
-f profile=release \
-f repeat=3 \
-f deep_profile=false \
-f live_openai_candidate=false \
-f fail_on_regression=false
run_id=""
for _ in $(seq 1 60); do
run_id="$(
BEFORE_IDS="$before_json" gh_with_retry run list --workflow openclaw-performance.yml --event workflow_dispatch --limit 50 --json databaseId,createdAt \
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
-f fail_on_regression=false)"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
if [[ -z "$run_id" ]]; then
echo "::warning::Could not find dispatched run for openclaw-performance.yml."
echo "::warning::gh workflow run openclaw-performance.yml did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs."
exit 0
fi

View File

@@ -476,19 +476,21 @@ jobs:
- name: Run Rocky Linux installer smoke
run: |
timeout --kill-after=30s 20m docker run --rm \
--platform linux/amd64 \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/scripts/install.sh:/tmp/install.sh:ro" \
rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \
rockylinux:9@sha256:d644d203142cd5b54ad2a83a203e1dee68af2229f8fe32f52a30c6e1d3c3a9e0 \
bash -lc 'dnf install -y -q ca-certificates tar gzip xz findutils which sudo >/dev/null && bash /tmp/install.sh --install-method npm --version latest --no-onboard --no-prompt --verify && openclaw --version'
- name: Run Rocky Linux CLI installer smoke
run: |
timeout --kill-after=30s 20m docker run --rm \
--platform linux/amd64 \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/scripts/install-cli.sh:/tmp/install-cli.sh:ro" \
rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \
rockylinux:9@sha256:d644d203142cd5b54ad2a83a203e1dee68af2229f8fe32f52a30c6e1d3c3a9e0 \
bash -lc 'dnf install -y -q ca-certificates tar gzip xz findutils which sudo >/dev/null && bash /tmp/install-cli.sh --prefix /tmp/openclaw-cli --version latest --no-onboard && /tmp/openclaw-cli/bin/openclaw --version'
bun_global_install_smoke:

View File

@@ -0,0 +1,447 @@
name: iOS Periphery Dead Code Comment
on:
workflow_run: # zizmor: ignore[dangerous-triggers] trusted PR commenter; job gates repository, source event, workflow name, live open PR, and exact current head before reading artifacts or writing comments
workflows: ["iOS Periphery Dead Code"]
types: [completed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
actions: read
contents: read
issues: write
pull-requests: read
jobs:
comment:
name: Comment on PR
runs-on: ubuntu-24.04
if: >
github.repository == 'openclaw/openclaw' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.name == 'iOS Periphery Dead Code'
steps:
- name: Upsert Periphery PR comment
uses: actions/github-script@v9
with:
script: |
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const childProcess = require("node:child_process");
const marker = "<!-- openclaw-ios-periphery-dead-code -->";
const run = context.payload.workflow_run;
const pr = run.pull_requests?.[0];
if (!pr) {
core.info("No pull request attached to workflow_run.");
return;
}
const { owner, repo } = context.repo;
const repository = `${owner}/${repo}`;
if (run.repository?.full_name !== repository) {
core.info(`Skipping workflow_run from ${run.repository?.full_name ?? "unknown repository"}.`);
return;
}
if (run.event !== "pull_request") {
core.info(`Skipping workflow_run for ${run.event ?? "unknown"} event.`);
return;
}
if (run.name !== "iOS Periphery Dead Code") {
core.info(`Skipping unexpected workflow ${run.name ?? "unknown"}.`);
return;
}
const livePull = await github.rest.pulls.get({
owner,
repo,
pull_number: pr.number,
});
if (livePull.data.state !== "open") {
core.info(`Skipping closed PR #${pr.number}.`);
return;
}
if (livePull.data.base?.repo?.full_name !== repository) {
core.info(`Skipping PR #${pr.number} targeting ${livePull.data.base?.repo?.full_name ?? "unknown repository"}.`);
return;
}
if (livePull.data.head?.sha !== run.head_sha) {
core.info(`Skipping stale run ${run.id}; PR #${pr.number} is now at ${livePull.data.head?.sha}.`);
return;
}
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
owner,
repo,
run_id: run.id,
filter: "latest",
per_page: 100,
});
const scopeJob = jobs.find((job) => job.name === "Detect iOS scan scope");
const scanJob = jobs.find((job) => job.name === "Scan iOS dead code");
const scanSkipped =
scopeJob?.conclusion === "success" && scanJob?.conclusion === "skipped";
if (scanSkipped) {
core.info(`Skipping intentionally omitted Periphery scan for PR #${pr.number}.`);
}
const artifacts = scanSkipped
? []
: await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
owner,
repo,
run_id: run.id,
per_page: 100,
});
const readReport = async () => {
if (scanSkipped) {
return;
}
const artifactName = `ios-periphery-dead-code-${run.id}-${run.run_attempt}`;
const artifact = artifacts.find((item) => item.name === artifactName);
if (!artifact) {
core.warning(`No ${artifactName} artifact found.`);
return;
}
if (artifact.expired) {
core.warning(`${artifactName} artifact expired.`);
return;
}
const maxArchiveBytes = 1024 * 1024;
const archiveSize = Number(artifact.size_in_bytes);
if (!Number.isSafeInteger(archiveSize) || archiveSize < 0 || archiveSize > maxArchiveBytes) {
core.warning(`Skipping ${artifactName}; compressed artifact size ${artifact.size_in_bytes ?? "unknown"} exceeds the ${maxArchiveBytes} byte limit.`);
return;
}
const archive = await github.rest.actions.downloadArtifact({
owner,
repo,
artifact_id: artifact.id,
archive_format: "zip",
});
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ios-periphery-"));
const archivePath = path.join(dir, "artifact.zip");
const archiveBuffer = Buffer.from(archive.data);
fs.writeFileSync(archivePath, archiveBuffer);
const allowedArtifactFiles = new Set([
"periphery.json",
"periphery.status",
"periphery.stderr.log",
"periphery.stdout.json",
"should-fail.txt",
]);
const maxEntries = allowedArtifactFiles.size;
const maxEntryBytes = 2 * 1024 * 1024;
const maxTotalBytes = 4 * 1024 * 1024;
const readUInt16 = (offset) => archiveBuffer.readUInt16LE(offset);
const readUInt32 = (offset) => archiveBuffer.readUInt32LE(offset);
const findEndOfCentralDirectoryOffset = () => {
const minimumOffset = Math.max(0, archiveBuffer.length - 0xffff - 22);
for (let offset = archiveBuffer.length - 22; offset >= minimumOffset; offset -= 1) {
if (readUInt32(offset) === 0x06054b50) {
return offset;
}
}
return -1;
};
const endOfCentralDirectoryOffset = findEndOfCentralDirectoryOffset();
if (endOfCentralDirectoryOffset < 0) {
core.warning(`Skipping ${artifactName}; ZIP end-of-central-directory record was not found.`);
return;
}
const entryCount = readUInt16(endOfCentralDirectoryOffset + 10);
const centralDirectorySize = readUInt32(endOfCentralDirectoryOffset + 12);
const centralDirectoryOffset = readUInt32(endOfCentralDirectoryOffset + 16);
if (entryCount < 1 || entryCount > maxEntries) {
core.warning(`Skipping ${artifactName}; artifact has ${entryCount} entries.`);
return;
}
if (
centralDirectoryOffset + centralDirectorySize > archiveBuffer.length ||
readUInt32(centralDirectoryOffset) !== 0x02014b50
) {
core.warning(`Skipping ${artifactName}; invalid ZIP central directory.`);
return;
}
const entries = new Map();
let totalUncompressedSize = 0;
let offset = centralDirectoryOffset;
for (let index = 0; index < entryCount; index += 1) {
if (offset + 46 > archiveBuffer.length || readUInt32(offset) !== 0x02014b50) {
core.warning(`Skipping ${artifactName}; invalid central directory entry.`);
return;
}
const compressionMethod = readUInt16(offset + 10);
const generalPurposeBitFlag = readUInt16(offset + 8);
const compressedSize = readUInt32(offset + 20);
const uncompressedSize = readUInt32(offset + 24);
const fileNameLength = readUInt16(offset + 28);
const extraLength = readUInt16(offset + 30);
const commentLength = readUInt16(offset + 32);
const externalAttributes = readUInt32(offset + 38);
const nameStart = offset + 46;
const nameEnd = nameStart + fileNameLength;
const nextOffset = nameEnd + extraLength + commentLength;
if (nextOffset > archiveBuffer.length) {
core.warning(`Skipping ${artifactName}; central directory entry exceeds archive bounds.`);
return;
}
const name = archiveBuffer.toString("utf8", nameStart, nameEnd);
const mode = externalAttributes >>> 16;
const fileType = mode & 0o170000;
const isRegularFile = fileType === 0 || fileType === 0o100000;
const invalidName =
!allowedArtifactFiles.has(name) ||
name.includes("/") ||
name.includes("\\") ||
name.includes("..") ||
path.isAbsolute(name);
if (invalidName) {
core.warning(`Skipping ${artifactName}; unexpected artifact entry ${name}.`);
return;
}
if (!isRegularFile || name.endsWith("/")) {
core.warning(`Skipping ${artifactName}; ${name} is not a regular file.`);
return;
}
if (entries.has(name)) {
core.warning(`Skipping ${artifactName}; duplicate artifact entry ${name}.`);
return;
}
if (![0, 8].includes(compressionMethod)) {
core.warning(`Skipping ${artifactName}; ${name} uses unsupported ZIP compression method ${compressionMethod}.`);
return;
}
if ((generalPurposeBitFlag & 0x1) !== 0) {
core.warning(`Skipping ${artifactName}; ${name} is encrypted.`);
return;
}
if (compressedSize > maxEntryBytes || uncompressedSize > maxEntryBytes) {
core.warning(`Skipping ${artifactName}; ${name} exceeds the per-file size limit.`);
return;
}
totalUncompressedSize += uncompressedSize;
if (totalUncompressedSize > maxTotalBytes) {
core.warning(`Skipping ${artifactName}; artifact exceeds the aggregate size limit.`);
return;
}
entries.set(name, { uncompressedSize });
offset = nextOffset;
}
const files = new Map();
for (const [name, entry] of entries) {
const contents = childProcess.execFileSync("unzip", ["-p", archivePath, name], {
encoding: "utf8",
maxBuffer: Math.max(1, entry.uncompressedSize + 1024),
timeout: 5000,
});
if (Buffer.byteLength(contents, "utf8") > maxEntryBytes) {
core.warning(`Skipping ${artifactName}; ${name} exceeded the per-file size limit while reading.`);
return;
}
files.set(name, contents);
}
const read = (name) => {
return files.get(name) ?? "";
};
const status = Number(read("periphery.status").trim() || "1");
let findings = null;
for (const name of ["periphery.json", "periphery.stdout.json"]) {
try {
const parsed = JSON.parse(read(name));
const validFindings =
Array.isArray(parsed) &&
parsed.every(
(finding) =>
finding !== null &&
typeof finding === "object" &&
!Array.isArray(finding),
);
if (validFindings) {
findings = parsed;
break;
}
} catch {}
}
return { findings, status };
};
const report = await readReport();
const status = report?.status ?? 1;
const findings = report?.findings ?? null;
const sanitizeCell = (value) => {
const normalized = String(value ?? "")
.replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ")
.replace(/[\u200b-\u200f\u202a-\u202e\u2060\u2066-\u2069\ufeff]/gu, "")
.replace(/\s+/gu, " ")
.trim();
const maxEncodedLength = 180;
let escaped = "";
for (const character of normalized) {
const encoded =
character === "`"
? "'"
: character === "|"
? "\\|"
: character;
if (escaped.length + encoded.length > maxEncodedLength) {
break;
}
escaped += encoded;
}
return `\`${escaped || "-"}\``;
};
const rows = (findings ?? []).map((finding) => {
const location = String(finding.location ?? "");
const [file, line] = location.split(":");
return {
file: file ? `apps/ios/${file}` : "",
line: line || "",
kind: String(finding.kind ?? ""),
name: String(finding.name ?? ""),
};
});
let mode = "failure";
let body = `${marker}\n`;
if (scanSkipped) {
mode = "skipped";
body += [
"### iOS Periphery",
"",
"Periphery scan skipped because the pull request is a draft or no longer touches iOS scan scope.",
].join("\n");
} else if (findings === null) {
body += [
"### iOS Periphery",
"",
"Periphery did not complete or its report could not be safely read. Check the workflow run for details.",
].join("\n");
} else if (rows.length === 0 && status === 0) {
mode = "success";
body += [
"### iOS Periphery",
"",
"No dead Swift code found.",
].join("\n");
} else if (rows.length > 0) {
const shown = rows.slice(0, 50);
body += [
"### iOS Periphery",
"",
`Found ${rows.length} dead Swift code ${rows.length === 1 ? "symbol" : "symbols"}. Remove the code or add a narrow Periphery exemption with a comment explaining why it must stay.`,
"",
"| File | Line | Kind | Name |",
"| --- | ---: | --- | --- |",
...shown.map((row) => `| ${sanitizeCell(row.file)} | ${sanitizeCell(row.line)} | ${sanitizeCell(row.kind)} | ${sanitizeCell(row.name)} |`),
rows.length > shown.length ? "" : null,
rows.length > shown.length ? `Showing first ${shown.length}; full JSON is in the workflow artifact.` : null,
].filter(Boolean).join("\n");
} else {
body += [
"### iOS Periphery",
"",
"Periphery exited with a non-zero status before producing findings. Check the workflow artifact for stdout/stderr.",
].join("\n");
}
body += "\n";
const maxCommentChars = 60_000;
if (body.length > maxCommentChars) {
body = [
marker,
"### iOS Periphery",
"",
`Found ${rows.length} dead Swift code ${rows.length === 1 ? "symbol" : "symbols"}. The rendered report exceeded the safe comment limit; use the workflow artifact for details.`,
"",
].join("\n");
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: livePull.data.number,
per_page: 100,
});
const existing = comments.find(
(comment) =>
comment.user?.login === "github-actions[bot]" &&
comment.body?.includes(marker),
);
if (!existing && ["skipped", "success"].includes(mode)) {
core.info(`No existing Periphery comment and scan ${mode}; skipping comment.`);
return;
}
const currentPull = await github.rest.pulls.get({
owner,
repo,
pull_number: pr.number,
});
if (
currentPull.data.state !== "open" ||
currentPull.data.base?.repo?.full_name !== repository ||
currentPull.data.head?.sha !== run.head_sha
) {
core.info(`Skipping stale run ${run.id}; PR #${pr.number} changed before comment update.`);
return;
}
const workflowRuns = await github.paginate(github.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id: run.workflow_id,
event: "pull_request",
head_sha: run.head_sha,
per_page: 100,
});
const supersedingRun = workflowRuns.find(
(candidate) =>
(candidate.id === run.id ||
candidate.pull_requests?.some(
(candidatePull) => candidatePull.number === pr.number,
)) &&
(candidate.run_number > run.run_number ||
(candidate.run_number === run.run_number &&
candidate.run_attempt > run.run_attempt)),
);
if (supersedingRun) {
core.info(`Skipping superseded run ${run.id} attempt ${run.run_attempt}; run ${supersedingRun.id} attempt ${supersedingRun.run_attempt} is newer.`);
return;
}
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
return;
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: livePull.data.number,
body,
});

229
.github/workflows/ios-periphery.yml vendored Normal file
View File

@@ -0,0 +1,229 @@
name: iOS Periphery Dead Code
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
workflow_dispatch:
concurrency:
group: ios-periphery-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
contents: read
pull-requests: read
jobs:
scope:
name: Detect iOS scan scope
runs-on: ubuntu-24.04
outputs:
should-scan: ${{ steps.scope.outputs.should-scan }}
steps:
- name: Detect changed paths
id: scope
uses: actions/github-script@v9
with:
script: |
if (context.eventName === "workflow_dispatch") {
core.setOutput("should-scan", "true");
return;
}
if (context.payload.pull_request?.draft) {
core.setOutput("should-scan", "false");
return;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
per_page: 100,
});
const isScanPath = (filename) =>
typeof filename === "string" && (
filename.startsWith("apps/ios/") ||
filename === ".github/workflows/ios-periphery.yml" ||
filename === ".github/workflows/ios-periphery-comment.yml" ||
filename === "config/swiftformat" ||
filename === "config/swiftlint.yml"
);
const shouldScan = files.some(
({ filename, previous_filename: previousFilename }) =>
isScanPath(filename) || isScanPath(previousFilename)
);
core.setOutput("should-scan", String(shouldScan));
scan:
name: Scan iOS dead code
needs: scope
if: ${{ needs.scope.outputs.should-scan == 'true' }}
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Verify Xcode
run: |
set -euo pipefail
for xcode_app in /Applications/Xcode_26.5.app /Applications/Xcode-26.5.0.app; do
if [ -d "$xcode_app/Contents/Developer" ]; then
sudo xcode-select -s "$xcode_app/Contents/Developer"
break
fi
done
xcodebuild -version
xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')"
if [[ "$xcode_version" != 26.* ]]; then
echo "error: expected Xcode 26.x, got $xcode_version" >&2
exit 1
fi
swift --version
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Install iOS Swift tooling
run: brew install xcodegen swiftformat swiftlint periphery
- name: Generate iOS project
run: |
set -euo pipefail
./scripts/ios-configure-signing.sh
./scripts/ios-write-version-xcconfig.sh
cd apps/ios
xcodegen generate
- name: Run Periphery
run: |
set -euo pipefail
output_dir="$RUNNER_TEMP/ios-periphery"
mkdir -p "$output_dir"
cd apps/ios
set +e
periphery scan \
--config .periphery.yml \
--strict \
--format json \
--write-results "$output_dir/periphery.json" \
>"$output_dir/periphery.stdout.json" \
2>"$output_dir/periphery.stderr.log"
periphery_status="$?"
set -e
printf '%s\n' "$periphery_status" >"$output_dir/periphery.status"
if [ ! -s "$output_dir/periphery.json" ]; then
cp "$output_dir/periphery.stdout.json" "$output_dir/periphery.json"
fi
- name: Build Periphery report
run: |
set -euo pipefail
node <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const outputDir = path.join(process.env.RUNNER_TEMP, "ios-periphery");
const read = (name) => {
const file = path.join(outputDir, name);
return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
};
const status = Number(read("periphery.status").trim() || "1");
let findings = null;
for (const name of ["periphery.json", "periphery.stdout.json"]) {
try {
const parsed = JSON.parse(read(name));
if (Array.isArray(parsed)) {
findings = parsed;
break;
}
} catch {}
}
const escapeCommandData = (value) =>
String(value ?? "")
.replaceAll("%", "%25")
.replaceAll("\r", "%0D")
.replaceAll("\n", "%0A");
const escapeCommandProperty = (value) =>
escapeCommandData(value)
.replaceAll(":", "%3A")
.replaceAll(",", "%2C");
const rows = (findings ?? []).map((finding) => {
const location = String(finding.location ?? "");
const [file, line] = location.split(":");
const repoFile = file ? `apps/ios/${file}` : "";
return {
file: repoFile,
line: line || "",
kind: String(finding.kind ?? ""),
name: String(finding.name ?? ""),
};
});
for (const row of rows) {
if (!row.file) continue;
const line = row.line ? `,line=${escapeCommandProperty(row.line)}` : "";
const title = `${row.kind || "Unused code"} ${row.name}`.trim();
console.log(`::error file=${escapeCommandProperty(row.file)}${line},title=Dead Swift code::${escapeCommandData(title)}`);
}
let shouldFail = "1";
let summary = "";
if (findings === null) {
summary = [
"### iOS Periphery",
"",
"Periphery did not complete. Check the workflow artifact for stdout/stderr.",
].join("\n");
} else if (rows.length === 0 && status === 0) {
shouldFail = "0";
summary = [
"### iOS Periphery",
"",
"No dead Swift code found.",
].join("\n");
} else if (rows.length > 0) {
summary = [
"### iOS Periphery",
"",
`Found ${rows.length} dead Swift code ${rows.length === 1 ? "symbol" : "symbols"}. See the PR comment or workflow artifact for details.`,
].join("\n");
} else {
summary = [
"### iOS Periphery",
"",
"Periphery exited with a non-zero status before producing findings. Check the workflow artifact for stdout/stderr.",
].join("\n");
}
fs.writeFileSync(path.join(outputDir, "should-fail.txt"), `${shouldFail}\n`);
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summary.trim()}\n`);
NODE
- name: Upload Periphery report
if: always()
uses: actions/upload-artifact@v7
with:
name: ios-periphery-dead-code-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ios-periphery
if-no-files-found: warn
retention-days: 14
- name: Fail on dead code
run: |
set -euo pipefail
test "$(cat "$RUNNER_TEMP/ios-periphery/should-fail.txt")" = "0"

View File

@@ -379,7 +379,6 @@ jobs:
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR }}
CRABBOX_COORDINATOR_TOKEN: ${{ secrets.CRABBOX_COORDINATOR_TOKEN }}
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }}

View File

@@ -220,7 +220,6 @@ jobs:
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
INPUT_SCENARIO: ${{ inputs.scenario }}
PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }}
run: |

View File

@@ -420,6 +420,7 @@ jobs:
add_suite live-cache
add_profile_suite native-live-src-agents "stable full"
add_profile_suite native-live-src-agents-zai-coding "stable full"
add_profile_suite native-live-src-gateway-core "beta minimum stable full"
add_profile_suite native-live-src-gateway-profiles-anthropic "stable full"
add_profile_suite native-live-src-gateway-profiles-anthropic-smoke "stable"
@@ -1956,6 +1957,12 @@ jobs:
timeout_minutes: 60
profile_env_only: false
profiles: stable full
- suite_id: native-live-src-agents-zai-coding
label: Native live Z.AI Coding Plan
command: ZAI_CODING_LIVE_TEST=1 node .release-harness/scripts/test-live-shard.mjs native-live-src-agents-zai-coding
timeout_minutes: 15
profile_env_only: false
profiles: stable full
- suite_id: native-live-src-gateway-core
label: Native live gateway core
command: OPENCLAW_LIVE_CODEX_HARNESS=1 OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-core
@@ -2215,7 +2222,11 @@ jobs:
case "${{ matrix.suite_id }}" in
live-cli-backend-docker)
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
if [[ -n "${OPENCLAW_CLAUDE_CREDENTIALS_JSON:-}" || -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=subscription" >> "$GITHUB_ENV"
else
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
fi
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"
@@ -2440,7 +2451,11 @@ jobs:
case "${{ matrix.suite_id }}" in
live-cli-backend-docker)
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
if [[ -n "${OPENCLAW_CLAUDE_CREDENTIALS_JSON:-}" || -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=subscription" >> "$GITHUB_ENV"
else
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
fi
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"

View File

@@ -56,6 +56,7 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
OCM_VERSION: v0.2.15
OCM_LINUX_X64_SHA256: b849b8de5d77e97e0df9319703254ae95e29d7f26a7552ea79bf173ff110ea0a
KOVA_REPOSITORY: openclaw/Kova
PERFORMANCE_MODEL_ID: gpt-5.5
@@ -187,11 +188,20 @@ jobs:
set -euo pipefail
KOVA_SRC="${RUNNER_TEMP}/kova-src"
echo "KOVA_SRC=$KOVA_SRC" >> "$GITHUB_ENV"
mkdir -p "$HOME/.local/bin" "$(dirname "$KOVA_SRC")"
curl -fsSL https://raw.githubusercontent.com/shakkernerd/ocm/main/install.sh \
| bash -s -- --version "$OCM_VERSION" --prefix "$HOME/.local" --force
git clone --filter=blob:none "https://github.com/${KOVA_REPOSITORY}.git" "$KOVA_SRC"
git -C "$KOVA_SRC" checkout "$KOVA_REF"
mkdir -p "$HOME/.local/bin" "$(dirname "$KOVA_SRC")" "${RUNNER_TEMP}/ocm-install"
ocm_archive="${RUNNER_TEMP}/ocm-${OCM_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 --retry-connrefused \
-o "$ocm_archive" \
"https://github.com/shakkernerd/ocm/releases/download/${OCM_VERSION}/ocm-x86_64-unknown-linux-gnu.tar.gz"
echo "${OCM_LINUX_X64_SHA256} ${ocm_archive}" | sha256sum -c -
tar -xzf "$ocm_archive" -C "${RUNNER_TEMP}/ocm-install"
install -m 0755 "${RUNNER_TEMP}/ocm-install/ocm" "$HOME/.local/bin/ocm"
git init -b main "$KOVA_SRC"
git -C "$KOVA_SRC" remote add origin "https://github.com/${KOVA_REPOSITORY}.git"
git -C "$KOVA_SRC" fetch --filter=blob:none --depth 1 origin "$KOVA_REF"
git -C "$KOVA_SRC" checkout --detach FETCH_HEAD
cat > "$HOME/.local/bin/kova" <<EOF
#!/usr/bin/env bash
export KOVA_HOME="${KOVA_HOME}"

View File

@@ -1181,7 +1181,7 @@ jobs:
runtime_tool_coverage_release_checks:
name: Enforce QA Lab runtime tool coverage
needs: [resolve_target, qa_lab_runtime_parity_release_checks]
if: always() && contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
@@ -1204,13 +1204,35 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
install-bun: "true"
- name: Download runtime parity status
uses: actions/download-artifact@v8
with:
name: release-check-status-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/
- name: Verify runtime parity producer status
id: verify_runtime_parity_status
shell: bash
run: |
set -euo pipefail
status_path=".artifacts/release-check-status/qa_lab_runtime_parity_release_checks.env"
status="$(sed -n 's/^status=//p' "$status_path" | tail -n 1)"
if [[ "$status" != "success" ]]; then
echo "Runtime parity producer status is ${status:-missing}; skipping coverage artifact consumer."
echo "ready=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "ready=true" >> "$GITHUB_OUTPUT"
- name: Download runtime parity artifacts
if: steps.verify_runtime_parity_status.outputs.ready == 'true'
uses: actions/download-artifact@v8
with:
name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
- name: Enforce standard runtime tool coverage
if: steps.verify_runtime_parity_status.outputs.ready == 'true'
run: |
set -euo pipefail
pnpm openclaw qa coverage \
@@ -1412,7 +1434,6 @@ jobs:
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
run: |
set -euo pipefail

View File

@@ -15,6 +15,14 @@ on:
description: Successful Full Release Validation run id for this tag/SHA, required when publish_openclaw_npm=true
required: false
type: string
windows_node_tag:
description: Exact openclaw-windows-node release tag, required for stable OpenClaw publish
required: false
type: string
windows_node_installer_digests:
description: Candidate-approved compact JSON map of Windows installer names to pinned sha256 digests
required: false
type: string
npm_telegram_run_id:
description: Optional successful NPM Telegram Beta E2E run id to include in final release evidence
required: false
@@ -81,12 +89,15 @@ jobs:
outputs:
sha: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
preflight_artifact_name: ${{ steps.preflight_artifact.outputs.name }}
windows_node_installer_digests: ${{ steps.windows_source.outputs.installer_digests }}
steps:
- name: Validate inputs
env:
RELEASE_TAG: ${{ inputs.tag }}
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
WINDOWS_NODE_INSTALLER_DIGESTS: ${{ inputs.windows_node_installer_digests }}
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
PLUGINS: ${{ inputs.plugins }}
@@ -115,6 +126,22 @@ jobs:
echo "publish_openclaw_npm=true requires full_release_validation_run_id." >&2
exit 1
fi
stable_release=true
if [[ "${RELEASE_TAG}" == *"-alpha."* || "${RELEASE_TAG}" == *"-beta."* ]]; then
stable_release=false
fi
if [[ -n "${WINDOWS_NODE_TAG}" && ! "${WINDOWS_NODE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]]; then
echo "windows_node_tag must be an explicit openclaw-windows-node release tag, not latest: ${WINDOWS_NODE_TAG}" >&2
exit 1
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${stable_release}" == "true" && -z "${WINDOWS_NODE_TAG}" ]]; then
echo "Stable OpenClaw publish requires an explicit windows_node_tag." >&2
exit 1
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${stable_release}" == "true" && -z "${WINDOWS_NODE_INSTALLER_DIGESTS}" ]]; then
echo "Stable OpenClaw publish requires candidate-approved windows_node_installer_digests." >&2
exit 1
fi
tideclaw_alpha_publish=false
if [[ "${RELEASE_TAG}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" == "alpha" && "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
tideclaw_alpha_publish=true
@@ -143,6 +170,73 @@ jobs:
;;
esac
- name: Validate stable Windows source release
id: windows_source
if: ${{ inputs.publish_openclaw_npm }}
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
APPROVED_INSTALLER_DIGESTS: ${{ inputs.windows_node_installer_digests }}
run: |
set -euo pipefail
if [[ "${RELEASE_TAG}" == *"-alpha."* || "${RELEASE_TAG}" == *"-beta."* ]]; then
exit 0
fi
source_json="$(gh release view "${WINDOWS_NODE_TAG}" \
--repo openclaw/openclaw-windows-node \
--json tagName,isDraft,isPrerelease,assets,url)"
if [[ "$(printf '%s' "${source_json}" | jq -r '.tagName')" != "${WINDOWS_NODE_TAG}" ]]; then
echo "Windows source release tag does not match ${WINDOWS_NODE_TAG}." >&2
exit 1
fi
if [[ "$(printf '%s' "${source_json}" | jq -r '.isDraft')" == "true" ]]; then
echo "Stable OpenClaw publish requires a published Windows source release." >&2
exit 1
fi
if [[ "$(printf '%s' "${source_json}" | jq -r '.isPrerelease')" == "true" ]]; then
echo "Stable OpenClaw publish requires a non-prerelease Windows source release." >&2
exit 1
fi
required_assets=(
"OpenClawCompanion-Setup-x64.exe"
"OpenClawCompanion-Setup-arm64.exe"
)
required_assets_json="$(printf '%s\n' "${required_assets[@]}" | jq -R . | jq -sc .)"
if ! approved_installer_digests="$(printf '%s' "${APPROVED_INSTALLER_DIGESTS}" | jq -ce --argjson names "${required_assets_json}" '
if type == "object" and
(keys | sort) == ($names | sort) and
all(.[]; type == "string" and test("^sha256:[a-f0-9]{64}$"))
then .
else error("invalid candidate-approved Windows installer digest map")
end
')"; then
echo "windows_node_installer_digests must contain exactly the candidate-approved current installer asset contract." >&2
exit 1
fi
for asset_name in "${required_assets[@]}"; do
asset_matches="$(printf '%s' "${source_json}" | jq -c --arg name "${asset_name}" '[.assets[]? | select(.name == $name)]')"
asset_match_count="$(printf '%s' "${asset_matches}" | jq 'length')"
if [[ "${asset_match_count}" != "1" ]]; then
echo "Windows source release ${WINDOWS_NODE_TAG} must contain exactly one required asset ${asset_name}; found ${asset_match_count}." >&2
exit 1
fi
asset_digest="$(printf '%s' "${asset_matches}" | jq -r '.[0].digest // empty')"
if [[ ! "${asset_digest}" =~ ^sha256:[a-f0-9]{64}$ ]]; then
echo "Windows source release ${WINDOWS_NODE_TAG} asset ${asset_name} is missing its immutable SHA-256 digest." >&2
exit 1
fi
approved_digest="$(printf '%s' "${approved_installer_digests}" | jq -r --arg name "${asset_name}" '.[$name]')"
if [[ "${asset_digest}" != "${approved_digest}" ]]; then
echo "Windows source release ${WINDOWS_NODE_TAG} asset ${asset_name} no longer matches its candidate-approved digest." >&2
exit 1
fi
done
echo "installer_digests=${approved_installer_digests}" >> "$GITHUB_OUTPUT"
echo "- Windows Node source release: prevalidated \`${WINDOWS_NODE_TAG}\`" >> "$GITHUB_STEP_SUMMARY"
- name: Download OpenClaw npm preflight manifest
id: preflight_artifact
if: ${{ inputs.publish_openclaw_npm }}
@@ -337,6 +431,7 @@ jobs:
TARGET_SHA: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
RELEASE_PROFILE: ${{ steps.full_manifest.outputs.release_profile || inputs.release_profile }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
run: |
{
echo "### Release target"
@@ -347,13 +442,16 @@ jobs:
if [[ -n "${FULL_RELEASE_VALIDATION_RUN_ID// }" ]]; then
echo "- Full release validation: \`${FULL_RELEASE_VALIDATION_RUN_ID}\`"
fi
if [[ -n "${WINDOWS_NODE_TAG// }" ]]; then
echo "- Windows Node source release: \`${WINDOWS_NODE_TAG}\`"
fi
} >> "$GITHUB_STEP_SUMMARY"
publish:
name: Publish plugins, then OpenClaw
needs: [resolve_release_target]
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 120
environment: npm-release
steps:
- name: Checkout release SHA
@@ -383,10 +481,16 @@ jobs:
WAIT_FOR_CLAWHUB: ${{ inputs.wait_for_clawhub && 'true' || 'false' }}
PREFLIGHT_ARTIFACT_NAME: ${{ needs.resolve_release_target.outputs.preflight_artifact_name }}
NPM_TELEGRAM_RUN_ID: ${{ inputs.npm_telegram_run_id }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
WINDOWS_NODE_INSTALLER_DIGESTS: ${{ needs.resolve_release_target.outputs.windows_node_installer_digests }}
POSTPUBLISH_EVIDENCE_DIR: ${{ runner.temp }}/openclaw-release-postpublish-evidence
run: |
set -euo pipefail
is_stable_release() {
[[ "${RELEASE_TAG}" != *"-alpha."* && "${RELEASE_TAG}" != *"-beta."* ]]
}
dispatch_workflow_at_ref() {
local workflow_ref="$1"
shift
@@ -836,10 +940,105 @@ jobs:
}
publish_github_release() {
if is_stable_release; then
verify_windows_release_asset_contract
fi
gh release edit "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --draft=false
echo "- GitHub release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
}
verify_windows_release_asset_contract() {
local actual_companion_assets actual_digest asset_name expected_companion_assets expected_digest expected_hash expected_installer_names manifest_dir manifest_json manifest_path release_json
# Add future promoted installer names, such as MSIX x64/ARM64, here.
local -a installer_assets=(
"OpenClawCompanion-Setup-x64.exe"
"OpenClawCompanion-Setup-arm64.exe"
)
local -a required_assets=(
"${installer_assets[@]}"
"OpenClawCompanion-SHA256SUMS.txt"
)
release_json="$(gh release view "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --json assets,url)"
expected_companion_assets="$(printf '%s\n' "${required_assets[@]}" | jq -R . | jq -sc 'sort')"
actual_companion_assets="$(printf '%s' "${release_json}" | jq -c '
[.assets[]? | select(.name | startswith("OpenClawCompanion-")) | .name] | sort
')"
if [[ "${actual_companion_assets}" != "${expected_companion_assets}" ]]; then
echo "Stable release OpenClawCompanion asset names do not exactly match the current contract." >&2
return 1
fi
for asset_name in "${required_assets[@]}"; do
if ! printf '%s' "${release_json}" | jq -e --arg name "${asset_name}" 'any(.assets[]?; .name == $name)' >/dev/null; then
echo "Stable release is missing required Windows asset ${asset_name}." >&2
return 1
fi
done
manifest_dir="${RUNNER_TEMP}/openclaw-windows-release-contract"
manifest_path="${manifest_dir}/OpenClawCompanion-SHA256SUMS.txt"
rm -rf "${manifest_dir}"
mkdir -p "${manifest_dir}"
gh release download "${RELEASE_TAG}" \
--repo "$GITHUB_REPOSITORY" \
--pattern "OpenClawCompanion-SHA256SUMS.txt" \
--dir "${manifest_dir}"
if ! manifest_json="$(jq -Rsc '
split("\n") as $lines |
(if $lines[-1] == "" then $lines[0:-1] else $lines end) |
map(sub("\r$"; "")) |
if all(.[]; test("^(?<hash>[a-f0-9]{64}) (?<name>[^/\\\\]+)$"))
then map(capture("^(?<hash>[a-f0-9]{64}) (?<name>[^/\\\\]+)$"))
else error("malformed Windows checksum manifest entry")
end
' "${manifest_path}")"; then
echo "Stable release Windows checksum manifest contains malformed entries." >&2
return 1
fi
expected_installer_names="$(printf '%s\n' "${installer_assets[@]}" | jq -R . | jq -sc 'sort')"
if ! printf '%s' "${manifest_json}" | jq -e --argjson expected "${expected_installer_names}" '
length == ($expected | length) and
([.[].name] | sort) == $expected and
([.[].name] | unique | length) == length
' >/dev/null; then
echo "Stable release Windows checksum manifest does not exactly match the installer asset contract." >&2
return 1
fi
for asset_name in "${installer_assets[@]}"; do
expected_digest="$(printf '%s' "${WINDOWS_NODE_INSTALLER_DIGESTS}" | jq -r --arg name "${asset_name}" '.[$name] // empty')"
actual_digest="$(printf '%s' "${release_json}" | jq -r --arg name "${asset_name}" '.assets[]? | select(.name == $name) | .digest // empty')"
if [[ -z "${expected_digest}" || "${actual_digest}" != "${expected_digest}" ]]; then
echo "Stable release Windows asset ${asset_name} does not match its pinned digest." >&2
return 1
fi
expected_hash="${expected_digest#sha256:}"
if ! printf '%s' "${manifest_json}" | jq -e --arg name "${asset_name}" --arg hash "${expected_hash}" '
any(.[]; .name == $name and .hash == $hash)
' >/dev/null; then
echo "Stable release Windows checksum manifest does not match pinned digest for ${asset_name}." >&2
return 1
fi
done
echo "- Windows Hub asset contract: verified" >> "$GITHUB_STEP_SUMMARY"
}
promote_windows_release_assets() {
if ! is_stable_release; then
return 0
fi
if [[ -z "${WINDOWS_NODE_INSTALLER_DIGESTS// }" ]]; then
echo "Stable release is missing prevalidated Windows installer digests." >&2
return 1
fi
windows_node_run_id="$(dispatch_workflow windows-node-release.yml \
-f tag="${RELEASE_TAG}" \
-f windows_node_tag="${WINDOWS_NODE_TAG}" \
-f expected_installer_digests="${WINDOWS_NODE_INSTALLER_DIGESTS}")"
echo "- Windows Node release run ID: \`${windows_node_run_id}\`" >> "$GITHUB_STEP_SUMMARY"
wait_for_run windows-node-release.yml "${windows_node_run_id}"
}
upload_dependency_evidence_release_asset() {
local release_version download_dir asset_path asset_name artifact_name
release_version="${RELEASE_TAG#v}"
@@ -913,13 +1112,14 @@ jobs:
}
append_release_proof_to_github_release() {
local release_version body_file notes_file tarball integrity telegram_line clawhub_line clawhub_bootstrap_line clawhub_runtime_state_path
local release_version body_file notes_file evidence_path tarball integrity telegram_line clawhub_line clawhub_bootstrap_line clawhub_runtime_state_path windows_line
release_version="${RELEASE_TAG#v}"
body_file="${RUNNER_TEMP}/release-body.md"
notes_file="${RUNNER_TEMP}/release-notes-with-proof.md"
tarball="$(npm view "openclaw@${release_version}" dist.tarball --json | jq -r '.')"
integrity="$(npm view "openclaw@${release_version}" dist.integrity --json | jq -r '.')"
evidence_path="${POSTPUBLISH_EVIDENCE_DIR}/release-postpublish-evidence.json"
tarball="$(jq -er '.openclawNpmTarball | select(type == "string" and length > 0)' "${evidence_path}")"
integrity="$(jq -er '.openclawNpmIntegrity | select(type == "string" and length > 0)' "${evidence_path}")"
gh release view "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" --json body --jq .body > "${body_file}"
if [[ -n "${NPM_TELEGRAM_RUN_ID// }" ]]; then
@@ -931,6 +1131,10 @@ jobs:
write_clawhub_runtime_state false "${clawhub_runtime_state_path}"
clawhub_line="$(jq -r '.proofLines.normal' "${clawhub_runtime_state_path}")"
clawhub_bootstrap_line="$(jq -r '.proofLines.bootstrap' "${clawhub_runtime_state_path}")"
windows_line=""
if [[ -n "${windows_node_run_id// }" ]]; then
windows_line="- Windows Hub promotion: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${windows_node_run_id} from openclaw/openclaw-windows-node@${WINDOWS_NODE_TAG}"
fi
RELEASE_BODY_FILE="${body_file}" \
RELEASE_NOTES_FILE="${notes_file}" \
@@ -948,6 +1152,7 @@ jobs:
CLAWHUB_LINE="${clawhub_line}" \
CLAWHUB_BOOTSTRAP_LINE="${clawhub_bootstrap_line}" \
TELEGRAM_LINE="${telegram_line}" \
WINDOWS_LINE="${windows_line}" \
node --input-type=module <<'NODE'
import { readFileSync, writeFileSync } from "node:fs";
@@ -974,6 +1179,7 @@ jobs:
process.env.CLAWHUB_BOOTSTRAP_LINE,
`- OpenClaw npm publish: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.OPENCLAW_NPM_RUN_ID}`,
process.env.TELEGRAM_LINE,
...(process.env.WINDOWS_LINE ? [process.env.WINDOWS_LINE] : []),
].join("\n");
const withoutOldProof = body.replace(/\n?### Release verification\n[\s\S]*?(?=\n### |\n## |$)/, "");
@@ -998,6 +1204,9 @@ jobs:
else
echo "- OpenClaw npm publish: skipped by input"
fi
if is_stable_release && [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
echo "- Windows Hub promotion: required before the GitHub release can be published"
fi
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
echo "- Workflow completion waits for ClawHub"
else
@@ -1142,6 +1351,7 @@ jobs:
failed=0
openclaw_failed=0
windows_node_run_id=""
if [[ -n "${openclaw_pid}" ]] && ! wait "${openclaw_pid}"; then
failed=1
openclaw_failed=1
@@ -1172,6 +1382,9 @@ jobs:
fi
create_or_update_github_release
upload_dependency_evidence_release_asset
if ! promote_windows_release_assets; then
failed=1
fi
append_release_proof_to_github_release
if [[ "${failed}" == "0" ]]; then
publish_github_release

View File

@@ -532,7 +532,6 @@ jobs:
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || '' }}
run: |
set -euo pipefail

View File

@@ -68,7 +68,7 @@ jobs:
days-before-pr-close: 7
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
@@ -100,7 +100,7 @@ jobs:
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
operations-per-run: 2000
ascending: true
include-only-assigned: true
@@ -172,7 +172,7 @@ jobs:
days-before-pr-close: 7
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
@@ -203,7 +203,7 @@ jobs:
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale,bad-barnacle,clawsweeper:queueable-fix,clawsweeper:source-repro,clawsweeper:fix-shape-clear
operations-per-run: 2000
ascending: true
include-only-assigned: true
@@ -277,6 +277,9 @@ jobs:
"security",
"no-stale",
"bad-barnacle",
"clawsweeper:queueable-fix",
"clawsweeper:source-repro",
"clawsweeper:fix-shape-clear",
]);
const prExemptLabels = new Set(["maintainer", "no-stale", "bad-barnacle"]);
const maintainerAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);

View File

@@ -8,9 +8,12 @@ on:
required: true
type: string
windows_node_tag:
description: openclaw-windows-node release tag to promote, or latest
description: Exact openclaw-windows-node release tag to promote, for example v0.6.3
required: true
type: string
expected_installer_digests:
description: Compact JSON map of installer asset names to pinned source sha256 digests
required: true
default: latest
type: string
permissions:
@@ -31,46 +34,129 @@ jobs:
env:
RELEASE_TAG: ${{ inputs.tag }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
EXPECTED_INSTALLER_DIGESTS: ${{ inputs.expected_installer_digests }}
GH_TOKEN: ${{ github.token }}
run: |
if ($env:RELEASE_TAG -notmatch '^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?$') {
throw "Invalid OpenClaw release tag: $env:RELEASE_TAG"
}
if ($env:WINDOWS_NODE_TAG -ne "latest" -and $env:WINDOWS_NODE_TAG -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$') {
throw "Invalid openclaw-windows-node release tag: $env:WINDOWS_NODE_TAG"
$stableRelease = -not (
$env:RELEASE_TAG.Contains("-alpha.") -or
$env:RELEASE_TAG.Contains("-beta.")
)
if ($env:WINDOWS_NODE_TAG -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$') {
throw "windows_node_tag must be an explicit openclaw-windows-node release tag, not latest: $env:WINDOWS_NODE_TAG"
}
try {
$expectedDigests = $env:EXPECTED_INSTALLER_DIGESTS | ConvertFrom-Json -AsHashtable
} catch {
throw "expected_installer_digests must be a JSON object: $_"
}
# Add future signed installer names, such as MSIX x64/ARM64, here.
$requiredInstallerNames = @(
"OpenClawCompanion-Setup-x64.exe",
"OpenClawCompanion-Setup-arm64.exe"
)
$allowedTargetCompanionAssetNames = @(
$requiredInstallerNames
"OpenClawCompanion-SHA256SUMS.txt"
)
if ($expectedDigests.Count -ne $requiredInstallerNames.Count) {
throw "expected_installer_digests must contain exactly the current installer asset contract."
}
foreach ($name in $requiredInstallerNames) {
$digest = [string]$expectedDigests[$name]
if ($digest -notmatch '^sha256:[A-Fa-f0-9]{64}$') {
throw "expected_installer_digests is missing a valid pinned digest for $name."
}
}
$targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json tagName,isDraft,isPrerelease,assets,url | ConvertFrom-Json
if ($targetRelease.tagName -ne $env:RELEASE_TAG) {
throw "OpenClaw release tag mismatch: expected $env:RELEASE_TAG, got $($targetRelease.tagName)"
}
$unexpectedTargetCompanionAssets = @(
$targetRelease.assets |
Where-Object {
$_.name.StartsWith("OpenClawCompanion-") -and
$_.name -notin $allowedTargetCompanionAssetNames
} |
ForEach-Object name |
Sort-Object
)
if ($unexpectedTargetCompanionAssets.Count -ne 0) {
throw "Target OpenClaw release contains unexpected OpenClawCompanion assets before upload: $($unexpectedTargetCompanionAssets -join ', ')"
}
$sourceRelease = gh release view $env:WINDOWS_NODE_TAG --repo openclaw/openclaw-windows-node --json tagName,isDraft,isPrerelease,assets,url | ConvertFrom-Json
if ($sourceRelease.tagName -ne $env:WINDOWS_NODE_TAG) {
throw "Windows source release tag mismatch: expected $env:WINDOWS_NODE_TAG, got $($sourceRelease.tagName)"
}
if ($sourceRelease.isDraft) {
throw "Windows source release must be published: $($sourceRelease.url)"
}
if ($stableRelease -and $sourceRelease.isPrerelease) {
throw "Stable OpenClaw releases require a non-prerelease Windows source release: $($sourceRelease.url)"
}
foreach ($name in $requiredInstallerNames) {
$sourceAssets = @($sourceRelease.assets | Where-Object name -eq $name)
if ($sourceAssets.Count -ne 1) {
throw "Windows source release must contain exactly one required asset $name; found $($sourceAssets.Count)."
}
if ([string]$sourceAssets[0].digest -ne [string]$expectedDigests[$name]) {
throw "Windows source release asset digest does not match the pinned digest: $name"
}
}
gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY | Out-Null
- name: Download Windows Hub release installers
shell: pwsh
env:
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
EXPECTED_INSTALLER_DIGESTS: ${{ inputs.expected_installer_digests }}
GH_TOKEN: ${{ github.token }}
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
$tagArgs = @()
if ($env:WINDOWS_NODE_TAG -ne "latest") {
$tagArgs += $env:WINDOWS_NODE_TAG
}
gh release download @tagArgs `
--repo openclaw/openclaw-windows-node `
--pattern "OpenClawCompanion-Setup-*.exe" `
--dir dist
$expected = @(
"dist/OpenClawCompanion-Setup-x64.exe",
"dist/OpenClawCompanion-Setup-arm64.exe"
# Add future signed installer patterns, such as MSIX x64/ARM64, here.
# Every matched installer is signature-checked, checksummed, and promoted.
$installerPatterns = @(
"OpenClawCompanion-Setup-x64.exe",
"OpenClawCompanion-Setup-arm64.exe"
)
foreach ($file in $expected) {
if (-not (Test-Path -LiteralPath $file)) {
throw "Missing expected Windows installer: $file"
$downloadArgs = @(
$env:WINDOWS_NODE_TAG,
"--repo", "openclaw/openclaw-windows-node",
"--dir", "dist"
)
foreach ($pattern in $installerPatterns) {
$downloadArgs += @("--pattern", $pattern)
}
gh release download @downloadArgs
if ($LASTEXITCODE -ne 0) {
throw "Failed to download Windows release assets from $env:WINDOWS_NODE_TAG."
}
foreach ($pattern in $installerPatterns) {
$patternMatches = @(Get-ChildItem -LiteralPath dist -File | Where-Object Name -Like $pattern)
if ($patternMatches.Count -ne 1) {
throw "Expected exactly one Windows installer matching '$pattern', found $($patternMatches.Count)."
}
}
$expectedDigests = $env:EXPECTED_INSTALLER_DIGESTS | ConvertFrom-Json -AsHashtable
foreach ($file in Get-ChildItem -LiteralPath dist -File) {
$expectedHash = ([string]$expectedDigests[$file.Name]) -replace '^sha256:', ''
$actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash
if ($actualHash -ne $expectedHash) {
throw "Downloaded Windows source asset does not match pinned digest: $($file.Name)"
}
}
- name: Verify Authenticode signatures
shell: pwsh
run: |
Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" | ForEach-Object {
$expectedSignerSubject = "CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US"
Get-ChildItem -LiteralPath dist -File | ForEach-Object {
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
if ($signature.Status -ne "Valid") {
throw "$($_.Name) Authenticode signature was $($signature.Status)."
@@ -78,6 +164,9 @@ jobs:
if (-not $signature.SignerCertificate) {
throw "$($_.Name) has no signer certificate."
}
if ($signature.SignerCertificate.Subject -ne $expectedSignerSubject) {
throw "$($_.Name) has unexpected signer subject $($signature.SignerCertificate.Subject)."
}
[pscustomobject]@{
File = $_.Name
Signer = $signature.SignerCertificate.Subject
@@ -88,7 +177,7 @@ jobs:
- name: Write SHA-256 manifest
shell: pwsh
run: |
Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" |
Get-ChildItem -LiteralPath dist -File |
Sort-Object Name |
ForEach-Object {
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName
@@ -101,12 +190,81 @@ jobs:
RELEASE_TAG: ${{ inputs.tag }}
GH_TOKEN: ${{ github.token }}
run: |
gh release upload $env:RELEASE_TAG `
dist/OpenClawCompanion-Setup-x64.exe `
dist/OpenClawCompanion-Setup-arm64.exe `
dist/OpenClawCompanion-SHA256SUMS.txt `
--repo $env:GITHUB_REPOSITORY `
--clobber
$releaseAssets = @(Get-ChildItem -LiteralPath dist -File | Sort-Object Name | ForEach-Object FullName)
gh release upload $env:RELEASE_TAG @releaseAssets --repo $env:GITHUB_REPOSITORY --clobber
if ($LASTEXITCODE -ne 0) {
throw "Failed to upload Windows release assets to $env:RELEASE_TAG."
}
- name: Verify promoted release asset contract
shell: pwsh
env:
RELEASE_TAG: ${{ inputs.tag }}
GH_TOKEN: ${{ github.token }}
run: |
New-Item -ItemType Directory -Force -Path verified | Out-Null
$expectedAssets = @(Get-ChildItem -LiteralPath dist -File | Sort-Object Name)
$expectedCompanionAssetNames = @($expectedAssets | ForEach-Object Name | Sort-Object)
$targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json assets | ConvertFrom-Json
$actualCompanionAssetNames = @(
$targetRelease.assets |
Where-Object { $_.name.StartsWith("OpenClawCompanion-") } |
ForEach-Object name |
Sort-Object
)
$assetContractDiff = @(
Compare-Object `
-ReferenceObject $expectedCompanionAssetNames `
-DifferenceObject $actualCompanionAssetNames
)
if (
$actualCompanionAssetNames.Count -ne $expectedCompanionAssetNames.Count -or
$assetContractDiff.Count -ne 0
) {
throw "Promoted OpenClawCompanion asset names do not exactly match the current contract."
}
foreach ($asset in $expectedAssets) {
gh release download $env:RELEASE_TAG `
--repo $env:GITHUB_REPOSITORY `
--pattern $asset.Name `
--dir verified
if ($LASTEXITCODE -ne 0) {
throw "Failed to download promoted Windows release asset $($asset.Name)."
}
}
$manifestPath = "verified/OpenClawCompanion-SHA256SUMS.txt"
$manifestEntries = @(Get-Content -LiteralPath $manifestPath | ForEach-Object {
if ($_ -notmatch '^([A-Fa-f0-9]{64}) ([^\\/]+)$') {
throw "Invalid Windows SHA-256 manifest entry: $_"
}
[PSCustomObject]@{
Hash = $Matches[1]
Name = $Matches[2]
}
})
$expectedInstallerNames = @(
$expectedAssets |
Where-Object Name -ne "OpenClawCompanion-SHA256SUMS.txt" |
ForEach-Object Name
)
$manifestInstallerNames = @($manifestEntries | ForEach-Object Name | Sort-Object)
$contractDiff = @(
Compare-Object `
-ReferenceObject $expectedInstallerNames `
-DifferenceObject $manifestInstallerNames
)
if ($contractDiff.Count -ne 0) {
throw "Promoted Windows SHA-256 manifest does not match the installer asset contract."
}
foreach ($entry in $manifestEntries) {
$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath "verified/$($entry.Name)").Hash
if ($hash -ne $entry.Hash) {
throw "Promoted Windows release asset checksum mismatch: $($entry.Name)"
}
}
- name: Summary
shell: pwsh
@@ -119,8 +277,9 @@ jobs:
OpenClaw release: $env:RELEASE_TAG
Source release: openclaw/openclaw-windows-node@$env:WINDOWS_NODE_TAG
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-x64.exe
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-arm64.exe
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-SHA256SUMS.txt
"@ >> $env:GITHUB_STEP_SUMMARY
Get-ChildItem -LiteralPath dist -File |
Sort-Object Name |
ForEach-Object {
"- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/$($_.Name)"
} >> $env:GITHUB_STEP_SUMMARY

View File

@@ -133,8 +133,9 @@ jobs:
$rootfs = "C:\wsl\ubuntu-noble-wsl.rootfs.tar.gz"
New-Item -ItemType Directory -Force -Path @((Split-Path -Parent $rootfs), $wslRoot) | Out-Null
Invoke-WebRequest -Uri $env:UBUNTU_WSL_ROOTFS_URL -OutFile $rootfs -UseBasicParsing
wsl.exe --import UbuntuProbe $wslRoot $rootfs --version 2
Write-Host "wsl_import_exit=$LASTEXITCODE"
$import = Invoke-WslText -Arguments @("--import", "UbuntuProbe", $wslRoot, $rootfs, "--version", "2")
Write-Host $import.Text
Write-Host "wsl_import_exit=$($import.Code)"
$list = Invoke-WslText -Arguments @("--list", "--verbose")
Write-Host $list.Text
Write-Host "wsl_list_after_import_exit=$($list.Code)"
@@ -144,14 +145,15 @@ jobs:
if ($distros.Count -gt 0) {
$distro = $distros[0]
Write-Host "wsl_probe_distro=$distro"
wsl.exe -d $distro --exec bash -lc 'set -euo pipefail; uname -a; if [ -f /etc/os-release ]; then sed -n "1,8p" /etc/os-release; fi'
$exec = Invoke-WslText -Arguments @("-d", $distro, "--exec", "bash", "-lc", 'set -euo pipefail; uname -a; if [ -f /etc/os-release ]; then sed -n "1,8p" /etc/os-release; fi')
} else {
wsl.exe --exec bash -lc 'set -euo pipefail; uname -a; if [ -f /etc/os-release ]; then sed -n "1,8p" /etc/os-release; fi'
$exec = Invoke-WslText -Arguments @("--exec", "bash", "-lc", 'set -euo pipefail; uname -a; if [ -f /etc/os-release ]; then sed -n "1,8p" /etc/os-release; fi')
}
if ($LASTEXITCODE -eq 0) {
Write-Host $exec.Text
if ($exec.Code -eq 0) {
$ok = $true
}
Write-Host "wsl_exec_exit=$LASTEXITCODE"
Write-Host "wsl_exec_exit=$($exec.Code)"
}
if ($ok) {

View File

@@ -251,3 +251,6 @@ jobs:
- name: Check plugin SDK API baseline drift
run: pnpm plugin-sdk:api:check
- name: Check plugin SDK surface budget
run: pnpm plugin-sdk:surface:check

2
.gitignore vendored
View File

@@ -127,6 +127,8 @@ mantis/
!.agents/skills/clawdtributor/**
!.agents/skills/control-ui-e2e/
!.agents/skills/control-ui-e2e/**
!.agents/skills/discord-user-post/
!.agents/skills/discord-user-post/**
!.agents/skills/gitcrawl/
!.agents/skills/gitcrawl/**
!.agents/skills/technical-documentation/

View File

@@ -172,7 +172,7 @@ Skills own workflows; root owns hard policy and routing.
- PR artifacts/screenshots: attach to PR/comment/external artifact store. Never push screenshots, videos, proof images, or proof assets to OpenClaw or any product repo branch, including temp artifact branches. Use Crabbox artifact publishing plus the manifest URL. Do not commit `.github/pr-assets`.
- CI polling: exact SHA, relevant checks only, minimal fields. Skip routine noise (`Auto response`, `Labeler`, docs agents, performance/stale). Logs only after failure/completion or concrete need.
- OpenClaw write-access maintainers may skip `Real behavior proof` when local tests or Crabbox verified behavior; record proof in PR verification.
- `/landpr`: use `~/.codex/prompts/landpr.md`; do not idle on `auto-response` or `check-docs`.
- Agent PR landing to `main`: use only the repo-native `scripts/pr` wrapper: run `scripts/pr review-init <PR>`, follow its emitted checkout/guard guidance, initialize and complete review artifacts with `scripts/pr review-artifacts-init <PR>`, validate them with `scripts/pr review-validate-artifacts <PR>`, then run `scripts/pr prepare-run <PR>` and `scripts/pr merge-run <PR>`; do not idle on `auto-response` or `check-docs`.
## Code
@@ -214,6 +214,7 @@ Skills own workflows; root owns hard policy and routing.
- Vitest. Colocated `*.test.ts`; e2e `*.e2e.test.ts`; example models `sonnet-4.6`, `gpt-5.5`; test GPT with 5.5 preferred, 5.4 ok; no GPT-4.x agent-smoke defaults.
- Prefer behavior tests over workflow/docs string greps. Put operator policy reminders in AGENTS/docs.
- QA scenario sources are YAML only: `qa/scenarios/index.yaml` and `qa/scenarios/<theme>/*.yaml`. Do not add fenced `qa-scenario`/`qa-flow` Markdown files under `qa/scenarios/`.
- Clean timers/env/globals/mocks/sockets/temp dirs/module state; `--isolate=false` safe.
- Prefer injection and narrow `*.runtime.ts` mocks over broad barrels or `openclaw/plugin-sdk/*`.
- Do not edit baseline/inventory/ignore/snapshot/expected-failure files to silence checks without explicit approval.

View File

@@ -2,6 +2,47 @@
Docs: https://docs.openclaw.ai
## 2026.6.8
### Highlights
- Telegram and WhatsApp channel delivery are richer and less brittle: Telegram can send structured rich text with tables, lists, expandable blockquotes, prompt-preserving CLI backend delivery, retired native draft migration, and safer rich-media boundaries, while WhatsApp now honors configured ACP bindings. (#92679, #84082, #89421, #92513) Thanks @obviyus, @jzakirov, @spacegeologist, and @TurboTheTurtle.
- Agent and Gateway recovery is sharper across account-scoped DM sends, generated media completions, restart shutdown aborts, yielded subagent pauses, yielded cron media, heartbeat dedupe, session identity prompts, and unknown OpenAI agent selector rejection. (#92788, #91246, #91357, #92631, #92146, #91287, #92468, #92510) Thanks @yetval, @TurboTheTurtle, @ooiuuii, @openperf, @IWhatsskill, @ZengWen-DT, and @zhangguiping-xydt.
- Provider/model handling expands and tightens with GLM-5.2, Claude Haiku 4.5 catalog rows, OpenRouter and Google Vertex provider-prefix normalization, managed SecretRef auth, bounded model browse discovery, storeless OpenAI Responses replay gating, and Claude 4.5 Copilot tool-streaming safety. (#92796, #90116, #92627, #91218, #90686, #92247, #90706, #75393) Thanks @arkyu2077, @liuhao1024, @bymle, @rohitjavvadi, @samson910022, @snowzlm, and @Kailigithub.
- `/usage` and reply payload hooks now have a native full footer renderer, default template, fixed-decimal formatting, credential-aware limits, better partial-count handling, and warnings for broken templates instead of silent bad output. (#92657, #89835, #89629) Thanks @Marvinthebored.
- UI and mobile flows are steadier: workspace files can collapse and start collapsed, WebChat backscroll survives streaming, the sidebar session picker remains interactive above the desktop workbench, reset soft args survive UI dispatch, stale dashboard session parent lineage is preserved, and iOS reconnects stale foreground gateways. (#92779, #92622, #92705, #91353, #90658, #92552) Thanks @shakkernerd, @TurboTheTurtle, @NianJiuZst, @zhouhe-xydt, @luoyanglang, and @Solvely-Colin.
- Memory, state, and diagnostics recover cleaner: oversized OpenAI embedding batches split before 431s, QMD memory search stays available in transient mode, SQLite avoids WAL on NFS state volumes, stuck-session recovery scheduling no longer resets warning backoff, and Infinity chunk limits stay genuinely unbounded. (#92650, #92618, #92639, #91247, #92752, #92735) Thanks @mushuiyu886, @TurboTheTurtle, @849261680, @gnanam1990, and @yhterrance.
### Changes
- Providers/models: add GLM-5.2 support and Claude Haiku 4.5 catalog entries while keeping provider-qualified model IDs normalized across OpenRouter and Google Vertex paths. (#92796, #90116, #92627, #91218) Thanks @arkyu2077, @liuhao1024, and @bymle.
- Channel plugins: ship Telegram rich-message delivery and WhatsApp ACP binding support, including rich prompt handoff to CLI backends and transport fixtures for richer drafts. (#92679, #92513) Thanks @obviyus and @TurboTheTurtle.
- Agent commands: support `/btw` in CLI-backed sessions and keep CLI usage-error exits classified as usage failures instead of successful runs. (#92669, #92162) Thanks @joshavant and @Pandah97.
- Usage hooks: add built-in full footer rendering, default footer templates, per-turn usage state, credential-aware limits, and fixed-decimal formatting for usage-bar templates. (#92657, #89835, #89629) Thanks @Marvinthebored.
- Docs and operator guidance: document node config examples, clarify before-install hook scope, correct agent default concurrency comments, refresh ZAI provider docs, and update channel/group docs for current Telegram and WhatsApp behavior. (#92677, #92766, #92695) Thanks @liuhao1024, @sallyom, and @ArielSmoliar.
### Fixes
- Onboarding/skills: show the Homebrew install recommendation only on macOS and Linux, so FreeBSD and other unsupported platforms no longer get a misleading brew prompt. Fixes #68893; carries forward #68894, #68910, #68941, #68943, #69002, and #69545. Thanks @yurivict, @Sanjays2402, @Eruditi, @JustInCache, @nnish16, and @Mlightsnow.
- Channels and delivery: preserve account-scoped DM channel send policy, rich Telegram final replies, rich Telegram tables and lists, Telegram thread-create CLI remapping, Slack outbound `message_sent` hooks, contributed message-tool schema optionality, same-channel generated media completions, and channel chunking around surrogate pairs and Infinity limits. (#92788, #92679, #89421, #89943, #91137, #91246, #92735) Thanks @yetval, @obviyus, @spacegeologist, @rishitamrakar, @lundog, @TurboTheTurtle, and @yhterrance.
- Auto-reply/groups: keep ordinary group text replies on automatic final-reply delivery while allowing `message(action=send)` for files, images, and other attachments to the same group or topic. Carries forward #43276; refs #48004. Thanks @NayukiChiba and @ShakaRover.
- iMessage: normalize leading NUL sent-message echo prefixes while preserving interior NUL bytes and the leading attributedBody marker handling from #73942. Carries forward #63581. Thanks @drvoss.
- Discord: give generated auto-thread titles a 60-second timeout and 4,096-token reasoning-model output budget, clamped to the selected model output cap. (#64734) Thanks @hanamizuki.
- Agent, cron, and Gateway runtime: mark active main sessions before restart shutdown aborts, pause yielded subagent runs whose terminal also signals abort, preserve yielded media completions, de-duplicate main-session heartbeat events, expose session identity in runtime prompts, reject unknown OpenAI agent selectors, keep generated media completions and slash-command block replies in WebChat, preserve fresh post-compaction usage while clearing stale usage snapshots, and require admin privileges for HTTP session/model override surfaces. (#91357, #92631, #92146, #91287, #92468, #92510, #91246, #50795, #50845, #82874, #92651, #92646) Thanks @ooiuuii, @openperf, @IWhatsskill, @ZengWen-DT, @zhangguiping-xydt, @Hollychou924, @leno23, and @TurboTheTurtle.
- Agents/exec: default empty-success background completion notices on only for real chat channels, preserving explicit opt-outs and keeping generic providers silent while carrying forward the narrow UX intent from #39726 and #46926. Thanks @Sapientropic and @wenkang-xie.
- Providers and model replay: preserve storeless OpenAI Responses replay compatibility, avoid eager tool streaming for Claude 4.5 in Copilot, honor profile auth for SecretRef model entries, bound model browsing, strip provider prefixes where runtimes need bare IDs, and surface nested embedding fetch failures. (#90706, #75393, #90686, #92247, #92627, #91218, #92628) Thanks @snowzlm, @Kailigithub, @rohitjavvadi, @samson910022, @liuhao1024, @bymle, and @mushuiyu886.
- Memory, state, diagnostics, and config: split header-too-large embedding batches, keep QMD memory search enabled in transient mode, avoid SQLite WAL on NFS volumes, preserve recovery scheduling outside stuck-session warning backoff, and keep shell environment fallbacks contained in config write tests. (#92650, #92618, #92639, #91247, #92752) Thanks @mushuiyu886, @TurboTheTurtle, @849261680, and @gnanam1990.
- Workspace setup state: store setup completion outside the workspace dot directory using an OpenClaw-named root file, migrate valid legacy state forward, and avoid clobbering generic root `workspace-state.json` files for TigerFS-style dot-path compatibility. This Clownfish replacement carries forward the focused #53326 fix idea because the original branch was closed and uneditable. (#53326, #44783, #39446) Thanks @1qh.
- UI/mobile/TUI: preserve dashboard session parent lineage, WebChat backscroll, reset soft command args, sidebar session picker interactivity, collapsed workspace files, resolved `/model` confirmation refs, and stale foreground iOS Gateway reconnects. (#90658, #92622, #91353, #92705, #92779, #92773, #92552) Thanks @luoyanglang, @TurboTheTurtle, @zhouhe-xydt, @NianJiuZst, @shakkernerd, @NarahariRaghava, and @Solvely-Colin.
- TUI: reload the active session after external `/new` or `/reset` session-change events so stale transcript and stream state clear promptly. Fixes #38966; carries forward #40472. Thanks @yizhanzjz and @wsyjh8.
- Control UI: preserve Gateway Access tokens during same-normalized WebSocket URL edits and reload gateway-scoped tokens when switching endpoints. Fixes #41545; repairs #42001 with additional source PRs #41546, #41552, and #41718. Thanks @wsyjh8, @llagy0020, @llagy007, @pingfanfan, and @zheliu2.
- Gateway CLI: tolerate a single transient clean WebSocket close before `hello-ok` so one-shot RPC calls reconnect instead of failing noisily, while repeated clean pre-hello closes still surface. Carries forward source PRs #54475 and #54774; #85253 covered adjacent connect assembly diagnostics. Thanks @ruanrrn.
- Release and test reliability: extend slow Gateway/full-suite watchdogs, split local full-suite shards when throttled, stabilize plugin auth marker fixtures, avoid brittle provider-ref error text, and keep QA Lab bootstrap selection assertions aligned with flow-only scenarios. (#92652)
- macOS Peekaboo bridge: update the embedded Peekaboo package to 3.5.2 and route bundled-skill CLI commands through the OpenClaw app bridge so they inherit its Screen Recording and Accessibility grants.
- Agent routing: route subagent RPC callbacks addressed to an agent-shaped `--to` target to the correct session key instead of falling back to the main session, so WeChat (and other channel) session-key callbacks reach the intended subagent session. (#90231) Thanks @zhangguiping-xydt.
- Cron: preserve model, fallback, thinking, timeout, light-context, unsafe-content, and tool allow-list overrides on implicit text payloads by promoting them to agent turns, while explicit system events still prune those fields. Fixes #28905; carries forward #64060 and #73946. Thanks @liaoandi.
- QQBot delivery: keep markdown table chunks self-contained across message boundaries by preserving table state across block deliveries, flushing unfinished table-row fragments as plain text, and detecting short pipe-terminated rows by column count so split rows are not sent as malformed markdown. (#92428) Thanks @sliverp.
## 2026.6.6
### Highlights

View File

@@ -138,7 +138,7 @@ ARG OPENCLAW_BUNDLED_PLUGIN_DIR
# BuildKit cache mounts are not part of cached layers; seed tarballs for the
# installed prod graph in the same step that runs offline prune.
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm list --prod --depth Infinity --json | node scripts/list-prod-store-packages.mjs | xargs -r pnpm store add && \
node scripts/list-prod-store-packages.mjs | xargs -r pnpm store add && \
CI=true pnpm prune --prod \
--config.offline=true \
--config.supportedArchitectures.os=linux \
@@ -147,6 +147,10 @@ RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/sto
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" OPENCLAW_BUNDLED_PLUGIN_DIR="$OPENCLAW_BUNDLED_PLUGIN_DIR" node scripts/prune-docker-plugin-dist.mjs && \
node scripts/postinstall-bundled-plugins.mjs && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
rm -rf \
/app/node_modules/openclaw \
/app/node_modules/.bin/openclaw \
/app/node_modules/.pnpm/openclaw@*/node_modules/openclaw && \
node scripts/check-package-dist-imports.mjs /app
# ── Runtime base image ──────────────────────────────────────────

View File

@@ -443,6 +443,7 @@ class NodeRuntime(
updateStatus()
micCapture.onGatewayConnectionChanged(true)
scope.launch {
subscribeOperatorSessionEvents()
refreshHomeCanvasOverviewIfConnected()
if (voiceReplySpeakerLazy.isInitialized()) {
voiceReplySpeaker.refreshConfig()
@@ -485,6 +486,14 @@ class NodeRuntime(
},
)
private suspend fun subscribeOperatorSessionEvents() {
try {
operatorSession.request("sessions.subscribe", null)
} catch (err: Throwable) {
Log.d("OpenClawRuntime", "sessions.subscribe failed: ${err.message ?: err::class.java.simpleName}")
}
}
private val nodeSession =
GatewaySession(
scope = scope,

View File

@@ -311,7 +311,6 @@ class ChatController(
}
}
/** Applies gateway chat/agent stream events to local transcript and pending-run state. */
fun handleGatewayEvent(
event: String,
payloadJson: String?,
@@ -321,7 +320,6 @@ class ChatController(
scope.launch { pollHealthIfNeeded(force = false) }
}
"health" -> {
// If we receive a health snapshot, the gateway is reachable.
_healthOk.value = true
}
"seqGap" -> {
@@ -332,6 +330,17 @@ class ChatController(
if (payloadJson.isNullOrBlank()) return
handleChatEvent(payloadJson)
}
"sessions.changed" -> {
if (payloadJson.isNullOrBlank()) {
refreshSessionsForCurrentWindow()
} else {
handleSessionsChangedEvent(payloadJson)
}
}
"session.message" -> {
if (payloadJson.isNullOrBlank()) return
handleSessionMessageEvent(payloadJson)
}
"agent" -> {
if (payloadJson.isNullOrBlank()) return
handleAgentEvent(payloadJson)
@@ -353,6 +362,7 @@ class ChatController(
)
if (!isCurrentHistoryLoad(sessionKey, _sessionKey.value, generation, historyLoadGeneration.get())) return
val history = parseHistory(historyJson, sessionKey = sessionKey, previousMessages = _messages.value)
updateSessionFromHistory(history)
prunePersistedOptimisticMessages(history.messages)
_messages.value = mergeOptimisticMessages(incoming = history.messages, optimistic = optimisticMessagesByRunId.values)
_sessionId.value = history.sessionId
@@ -388,6 +398,10 @@ class ChatController(
}
}
private fun refreshSessionsForCurrentWindow() {
scope.launch { fetchSessions(limit = _sessions.value.size.takeIf { it > 0 } ?: 100) }
}
private suspend fun pollHealthIfNeeded(force: Boolean) {
val now = System.currentTimeMillis()
val last = lastHealthPollAtMs
@@ -457,6 +471,7 @@ class ChatController(
sessionKey = currentSessionKey,
previousMessages = _messages.value,
)
updateSessionFromHistory(history)
prunePersistedOptimisticMessages(history.messages)
_messages.value = mergeOptimisticMessages(incoming = history.messages, optimistic = optimisticMessagesByRunId.values)
_sessionId.value = history.sessionId
@@ -472,6 +487,31 @@ class ChatController(
}
}
private fun handleSessionsChangedEvent(payloadJson: String) {
val payload = json.parseToJsonElement(payloadJson).asObjectOrNull() ?: return
if (payload["reason"].asStringOrNull() == "delete") {
removeSessionEntry(payload["sessionKey"].asStringOrNull() ?: payload["key"].asStringOrNull())
return
}
val entry = parseEventSessionEntry(payload)
if (entry != null) {
upsertSessionEntry(entry)
} else {
refreshSessionsForCurrentWindow()
}
}
private fun handleSessionMessageEvent(payloadJson: String) {
val payload = json.parseToJsonElement(payloadJson).asObjectOrNull() ?: return
val entry = parseEventSessionEntry(payload)
if (entry != null) {
upsertSessionEntry(entry)
}
}
private fun parseEventSessionEntry(payload: JsonObject): ChatSessionEntry? =
payload["session"].asObjectOrNull()?.let(::parseSessionEntry) ?: parseSessionEntry(payload)
private fun handleAgentEvent(payloadJson: String) {
val payload = json.parseToJsonElement(payloadJson).asObjectOrNull() ?: return
val sessionKey = payload["sessionKey"].asStringOrNull()?.trim()
@@ -600,6 +640,7 @@ class ChatController(
val root = json.parseToJsonElement(historyJson).asObjectOrNull() ?: return ChatHistory(sessionKey, null, null, emptyList())
val sid = root["sessionId"].asStringOrNull()
val thinkingLevel = root["thinkingLevel"].asStringOrNull()
val sessionInfo = root["sessionInfo"].asObjectOrNull()?.let { parseSessionEntry(it, fallbackKey = sessionKey) }
val array = root["messages"].asArrayOrNull() ?: JsonArray(emptyList())
val messages =
@@ -622,20 +663,69 @@ class ChatController(
sessionId = sid,
thinkingLevel = thinkingLevel,
messages = reconcileMessageIds(previous = previousMessages, incoming = messages),
sessionInfo = sessionInfo,
)
}
private fun parseSessions(jsonString: String): List<ChatSessionEntry> {
val root = json.parseToJsonElement(jsonString).asObjectOrNull() ?: return emptyList()
val sessions = root["sessions"].asArrayOrNull() ?: return emptyList()
return sessions.mapNotNull { item ->
val obj = item.asObjectOrNull() ?: return@mapNotNull null
val key = obj["key"].asStringOrNull()?.trim().orEmpty()
if (key.isEmpty()) return@mapNotNull null
val updatedAt = obj["updatedAt"].asLongOrNull()
val displayName = obj["displayName"].asStringOrNull()?.trim()
ChatSessionEntry(key = key, updatedAtMs = updatedAt, displayName = displayName)
}
return sessions.mapNotNull { item -> parseSessionEntry(item.asObjectOrNull()) }
}
private fun parseSessionEntry(
obj: JsonObject?,
fallbackKey: String? = null,
): ChatSessionEntry? {
if (obj == null) return null
val key =
obj["key"].asStringOrNull()?.trim().orEmpty()
.ifEmpty { obj["sessionKey"].asStringOrNull()?.trim().orEmpty() }
.ifEmpty { fallbackKey?.trim().orEmpty() }
if (key.isEmpty()) return null
return ChatSessionEntry(
key = key,
updatedAtMs = obj["updatedAt"].asLongOrNull(),
displayName = obj["displayName"].asStringOrNull()?.trim(),
totalTokens = obj["totalTokens"].asLongOrNull(),
totalTokensFresh = obj["totalTokensFresh"].asBooleanOrNull(),
contextTokens = obj["contextTokens"].asLongOrNull(),
hasContextUsageMetadata =
"totalTokens" in obj ||
"totalTokensFresh" in obj ||
"contextTokens" in obj,
)
}
private fun updateSessionFromHistory(history: ChatHistory) {
val info = history.sessionInfo ?: return
upsertSessionEntry(info, preserveExistingContextUsageWithoutTotal = true)
}
private fun upsertSessionEntry(
entry: ChatSessionEntry,
preserveExistingContextUsageWithoutTotal: Boolean = false,
) {
val current = _sessions.value
val index = current.indexOfFirst { it.key == entry.key }
_sessions.value =
if (index >= 0) {
current.toMutableList().also {
it[index] =
mergeChatSessionEntry(
existing = it[index],
next = entry,
preserveExistingContextUsageWithoutTotal = preserveExistingContextUsageWithoutTotal,
)
}
} else {
listOf(entry) + current
}
}
private fun removeSessionEntry(sessionKey: String?) {
val key = sessionKey?.trim()?.takeIf { it.isNotEmpty() } ?: return
_sessions.value = _sessions.value.filterNot { it.key == key }
}
private fun parseRunId(resJson: String): String? =
@@ -857,3 +947,44 @@ private fun JsonElement?.asLongOrNull(): Long? =
is JsonPrimitive -> content.toLongOrNull()
else -> null
}
private fun JsonElement?.asBooleanOrNull(): Boolean? =
when (this) {
is JsonPrimitive -> content.toBooleanStrictOrNull()
else -> null
}
internal fun mergeChatSessionEntry(
existing: ChatSessionEntry,
next: ChatSessionEntry,
preserveExistingContextUsageWithoutTotal: Boolean = false,
): ChatSessionEntry {
val preserveExistingContextUsage = preserveExistingContextUsageWithoutTotal && next.totalTokens == null
return existing.copy(
updatedAtMs = next.updatedAtMs ?: existing.updatedAtMs,
displayName = next.displayName ?: existing.displayName,
totalTokens =
when {
preserveExistingContextUsage -> existing.totalTokens
next.hasContextUsageMetadata -> next.totalTokens
else -> null
},
totalTokensFresh =
when {
preserveExistingContextUsage -> existing.totalTokensFresh
next.hasContextUsageMetadata -> next.totalTokensFresh
else -> null
},
contextTokens =
when {
preserveExistingContextUsage -> next.contextTokens ?: existing.contextTokens
next.hasContextUsageMetadata -> next.contextTokens
else -> null
},
hasContextUsageMetadata =
when {
preserveExistingContextUsage -> existing.hasContextUsageMetadata || next.contextTokens != null
else -> next.hasContextUsageMetadata
},
)
}

View File

@@ -40,6 +40,10 @@ data class ChatSessionEntry(
val key: String,
val updatedAtMs: Long?,
val displayName: String? = null,
val totalTokens: Long? = null,
val totalTokensFresh: Boolean? = null,
val contextTokens: Long? = null,
val hasContextUsageMetadata: Boolean = totalTokens != null || totalTokensFresh != null || contextTokens != null,
)
/**
@@ -50,6 +54,7 @@ data class ChatHistory(
val sessionId: String?,
val thinkingLevel: String?,
val messages: List<ChatMessage>,
val sessionInfo: ChatSessionEntry? = null,
)
/**

View File

@@ -74,6 +74,7 @@ import kotlinx.coroutines.withContext
import java.text.DateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.roundToInt
/** Full chat surface that wires MainViewModel state to messages, attachments, voice, and composer actions. */
@Composable
@@ -95,6 +96,7 @@ fun ChatScreen(
val sessions by viewModel.chatSessions.collectAsState()
val chatDraft by viewModel.chatDraft.collectAsState()
val pendingAssistantAutoSend by viewModel.pendingAssistantAutoSend.collectAsState()
val contextUsage = resolveChatContextUsage(sessionKey = sessionKey, mainSessionKey = mainSessionKey, sessions = sessions)
val context = LocalContext.current
val resolver = context.contentResolver
val scope = rememberCoroutineScope()
@@ -196,6 +198,7 @@ fun ChatScreen(
onValueChange = { input = it },
attachments = attachments,
thinkingLevel = thinkingLevel,
contextUsage = contextUsage,
healthOk = healthOk,
pendingRunCount = pendingRunCount,
onThinkingLevelChange = viewModel::setChatThinkingLevel,
@@ -685,6 +688,7 @@ private fun ChatComposer(
onValueChange: (String) -> Unit,
attachments: List<PendingImageAttachment>,
thinkingLevel: String,
contextUsage: ChatContextUsage,
healthOk: Boolean,
pendingRunCount: Int,
onThinkingLevelChange: (String) -> Unit,
@@ -699,7 +703,11 @@ private fun ChatComposer(
AttachmentStrip(attachments = attachments, onRemoveAttachment = onRemoveAttachment)
}
ChatContextMeter(thinkingLevel = thinkingLevel, onClick = { onThinkingLevelChange(nextThinkingValue(thinkingLevel)) })
ChatContextMeter(
thinkingLevel = thinkingLevel,
contextUsage = contextUsage,
onClick = { onThinkingLevelChange(nextThinkingValue(thinkingLevel)) },
)
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
ChatInputPill(value = value, onValueChange = onValueChange, onPickImages = onPickImages, onVoice = onVoice, modifier = Modifier.weight(1f))
@@ -735,8 +743,10 @@ private fun ChatComposer(
@Composable
private fun ChatContextMeter(
thinkingLevel: String,
contextUsage: ChatContextUsage,
onClick: () -> Unit,
) {
val contextFraction = contextMeterWidth(contextUsage) ?: 0f
Row(
modifier = Modifier.width(178.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -755,7 +765,13 @@ private fun ChatContextMeter(
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Icon(imageVector = Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(12.dp), tint = ClawTheme.colors.textSubtle)
Text(text = "Context ${contextPercent(thinkingLevel)}%", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted)
Text(
text = contextMeterLabel(contextUsage, thinkingLevel),
style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp),
color = ClawTheme.colors.textMuted,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
Box(
@@ -768,7 +784,7 @@ private fun ChatContextMeter(
Box(
modifier =
Modifier
.fillMaxWidth(thinkingMeterWidth(thinkingLevel))
.fillMaxWidth(contextFraction)
.height(3.dp)
.background(ClawTheme.colors.primary, RoundedCornerShape(999.dp)),
)
@@ -902,6 +918,32 @@ private fun isActiveSessionChoice(
return choiceKey == current
}
internal data class ChatContextUsage(
val totalTokens: Long?,
val totalTokensFresh: Boolean?,
val contextTokens: Long?,
)
internal fun resolveChatContextUsage(
sessionKey: String,
mainSessionKey: String,
sessions: List<ChatSessionEntry>,
): ChatContextUsage {
val entry =
sessions.firstOrNull {
isActiveSessionChoice(
choiceKey = it.key,
sessionKey = sessionKey,
mainSessionKey = mainSessionKey,
)
}
return ChatContextUsage(
totalTokens = entry?.totalTokens,
totalTokensFresh = entry?.totalTokensFresh,
contextTokens = entry?.contextTokens,
)
}
@Composable
private fun SendButton(
enabled: Boolean,
@@ -958,16 +1000,28 @@ private fun nextThinkingValue(value: String): String =
else -> "off"
}
/** Maps thinking presets to the visual context meter fill fraction. */
private fun thinkingMeterWidth(value: String): Float =
when (value.lowercase(Locale.US)) {
"low" -> 0.34f
"medium" -> 0.58f
"high" -> 0.82f
else -> 0.18f
}
internal fun contextMeterWidth(usage: ChatContextUsage): Float? {
if (usage.totalTokensFresh == false) return null
val total = usage.totalTokens?.takeIf { it >= 0L } ?: return null
val context = usage.contextTokens?.takeIf { it > 0L } ?: return null
return (total.toDouble() / context.toDouble()).coerceIn(0.0, 1.0).toFloat()
}
private fun contextPercent(value: String): Int = (thinkingMeterWidth(value) * 100).toInt()
internal fun contextMeterLabel(
usage: ChatContextUsage,
thinkingLevel: String,
): String {
val contextLabel = contextMeterWidth(usage)?.let { "Context ${(it * 100).roundToInt()}%" } ?: "Context --"
return "$contextLabel · ${contextMeterThinkingLabel(thinkingLevel)}"
}
internal fun contextMeterThinkingLabel(value: String): String =
when (value.lowercase(Locale.US)) {
"low" -> "low"
"medium" -> "medium"
"high" -> "high"
else -> "off"
}
private fun formatChatTimestamp(timestampMs: Long): String = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(Date(timestampMs))

View File

@@ -59,4 +59,96 @@ class ChatControllerSessionPolicyTest {
),
)
}
@Test
fun sessionMergeClearsUsageWhenNewSnapshotOmitsUsageMetadata() {
val existing =
ChatSessionEntry(
key = "agent:main:phone",
updatedAtMs = 1L,
displayName = "Phone",
totalTokens = 41_000L,
totalTokensFresh = true,
contextTokens = 100_000L,
)
val next =
ChatSessionEntry(
key = "agent:main:phone",
updatedAtMs = 2L,
displayName = "Phone renamed",
hasContextUsageMetadata = false,
)
val merged = mergeChatSessionEntry(existing, next)
assertEquals("agent:main:phone", merged.key)
assertEquals(2L, merged.updatedAtMs)
assertEquals("Phone renamed", merged.displayName)
assertEquals(null, merged.totalTokens)
assertEquals(null, merged.totalTokensFresh)
assertEquals(null, merged.contextTokens)
assertFalse(merged.hasContextUsageMetadata)
}
@Test
fun sessionMergePreservesUsageWhenHistorySnapshotOmitsTotalTokens() {
val existing =
ChatSessionEntry(
key = "agent:main:phone",
updatedAtMs = 1L,
displayName = "Phone",
totalTokens = 41_000L,
totalTokensFresh = true,
contextTokens = 100_000L,
)
val next =
ChatSessionEntry(
key = "agent:main:phone",
updatedAtMs = 2L,
displayName = "Phone renamed",
totalTokensFresh = false,
contextTokens = 120_000L,
)
val merged =
mergeChatSessionEntry(
existing = existing,
next = next,
preserveExistingContextUsageWithoutTotal = true,
)
assertEquals(2L, merged.updatedAtMs)
assertEquals("Phone renamed", merged.displayName)
assertEquals(41_000L, merged.totalTokens)
assertEquals(true, merged.totalTokensFresh)
assertEquals(120_000L, merged.contextTokens)
assertTrue(merged.hasContextUsageMetadata)
}
@Test
fun sessionMergeAppliesExplicitStaleUsageMetadata() {
val existing =
ChatSessionEntry(
key = "agent:main:phone",
updatedAtMs = 1L,
totalTokens = 41_000L,
totalTokensFresh = true,
contextTokens = 100_000L,
)
val next =
ChatSessionEntry(
key = "agent:main:phone",
updatedAtMs = 2L,
totalTokens = 82_000L,
totalTokensFresh = false,
contextTokens = 100_000L,
)
val merged = mergeChatSessionEntry(existing, next)
assertEquals(82_000L, merged.totalTokens)
assertEquals(false, merged.totalTokensFresh)
assertEquals(100_000L, merged.contextTokens)
assertTrue(merged.hasContextUsageMetadata)
}
}

View File

@@ -0,0 +1,84 @@
package ai.openclaw.app.ui.chat
import ai.openclaw.app.chat.ChatSessionEntry
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class ChatContextMeterTest {
@Test
fun contextMeterUsesActiveSessionTokenBudget() {
val sessions =
listOf(
ChatSessionEntry(key = "main", updatedAtMs = 1L, displayName = "Main", totalTokens = 8_000L, totalTokensFresh = true, contextTokens = 10_000L),
ChatSessionEntry(
key = "agent:main:mobile:test-device",
updatedAtMs = 2L,
displayName = "Phone",
totalTokens = 1_250L,
totalTokensFresh = true,
contextTokens = 5_000L,
),
)
val usage =
resolveChatContextUsage(
sessionKey = "agent:main:mobile:test-device",
mainSessionKey = "main",
sessions = sessions,
)
assertEquals(ChatContextUsage(totalTokens = 1_250L, totalTokensFresh = true, contextTokens = 5_000L), usage)
assertEquals(0.25f, contextMeterWidth(usage))
assertEquals("Context 25% · high", contextMeterLabel(usage, "high"))
}
@Test
fun contextMeterResolvesCanonicalMainAlias() {
val sessions =
listOf(
ChatSessionEntry(
key = "agent:main:node-phone",
updatedAtMs = 1L,
displayName = "Main",
totalTokens = 41_000L,
totalTokensFresh = true,
contextTokens = 100_000L,
),
)
val usage =
resolveChatContextUsage(
sessionKey = "main",
mainSessionKey = "agent:main:node-phone",
sessions = sessions,
)
assertEquals(ChatContextUsage(totalTokens = 41_000L, totalTokensFresh = true, contextTokens = 100_000L), usage)
assertEquals("Context 41% · off", contextMeterLabel(usage, "off"))
}
@Test
fun contextMeterDoesNotInventPercentWhenBudgetIsMissing() {
val usage = ChatContextUsage(totalTokens = 8_200L, totalTokensFresh = true, contextTokens = null)
assertNull(contextMeterWidth(usage))
assertEquals("Context -- · medium", contextMeterLabel(usage, "medium"))
}
@Test
fun contextMeterClampsOverfullSessions() {
val usage = ChatContextUsage(totalTokens = 150_000L, totalTokensFresh = true, contextTokens = 100_000L)
assertEquals(1.0f, contextMeterWidth(usage))
assertEquals("Context 100% · low", contextMeterLabel(usage, "low"))
}
@Test
fun contextMeterDoesNotDisplayStaleTokenUsage() {
val usage = ChatContextUsage(totalTokens = 82_000L, totalTokensFresh = false, contextTokens = 100_000L)
assertNull(contextMeterWidth(usage))
assertEquals("Context -- · high", contextMeterLabel(usage, "high"))
}
}

18
apps/ios/.periphery.yml Normal file
View File

@@ -0,0 +1,18 @@
project: OpenClaw.xcodeproj
schemes:
- OpenClaw
retain_codable_properties: true
retain_swift_ui_previews: true
retain_objc_accessible: true
retain_unused_protocol_func_params: true
retain_assign_only_properties: true
relative_results: true
disable_update_check: true
report_include:
- Sources/**
- ShareExtension/**
- ActivityWidget/**
- WatchExtension/Sources/**
build_arguments:
- -destination
- generic/platform=iOS Simulator

View File

@@ -58,11 +58,11 @@ Maintenance update for the current OpenClaw release.
## 2026.5.12 - 2026-05-12
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.
## 2026.5.10 - 2026-05-10
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.
- Gateway connections now recover after a trusted Gateway certificate changes by refreshing the stored certificate pin during reconnect.
@@ -128,7 +128,7 @@ Maintenance update for the current OpenClaw release.
## 2026.4.19 - 2026-04-19
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.
## 2026.4.18 - 2026-04-18
@@ -136,11 +136,11 @@ Maintenance update for the current OpenClaw release.
## 2026.4.15 - 2026-04-15
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.
## 2026.4.14 - 2026-04-14
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.
## 2026.4.12 - 2026-04-12

View File

@@ -0,0 +1,53 @@
{
"teamId": "FWJYW4S8P8",
"signingRepo": "git@github.com:openclaw/apps-signing.git",
"signingBranch": "main",
"profileType": "appstore",
"targets": [
{
"target": "OpenClaw",
"displayName": "OpenClaw",
"bundleId": "ai.openclawfoundation.app",
"platform": "IOS",
"profileKey": "OPENCLAW_APP_PROFILE",
"profileName": "OpenClaw App Store ai.openclawfoundation.app",
"capabilities": ["PUSH_NOTIFICATIONS"]
},
{
"target": "OpenClawShareExtension",
"displayName": "OpenClaw Share",
"bundleId": "ai.openclawfoundation.app.share",
"platform": "IOS",
"profileKey": "OPENCLAW_SHARE_PROFILE",
"profileName": "OpenClaw App Store ai.openclawfoundation.app.share",
"capabilities": []
},
{
"target": "OpenClawActivityWidget",
"displayName": "OpenClaw Activity Widget",
"bundleId": "ai.openclawfoundation.app.activitywidget",
"platform": "IOS",
"profileKey": "OPENCLAW_ACTIVITY_WIDGET_PROFILE",
"profileName": "OpenClaw App Store ai.openclawfoundation.app.activitywidget",
"capabilities": []
},
{
"target": "OpenClawWatchApp",
"displayName": "OpenClaw Watch App",
"bundleId": "ai.openclawfoundation.app.watchkitapp",
"platform": "IOS",
"profileKey": "OPENCLAW_WATCH_APP_PROFILE",
"profileName": "OpenClaw App Store ai.openclawfoundation.app.watchkitapp",
"capabilities": []
},
{
"target": "OpenClawWatchExtension",
"displayName": "OpenClaw Watch Extension",
"bundleId": "ai.openclawfoundation.app.watchkitapp.extension",
"platform": "IOS",
"profileKey": "OPENCLAW_WATCH_EXTENSION_PROFILE",
"profileName": "OpenClaw App Store ai.openclawfoundation.app.watchkitapp.extension",
"capabilities": []
}
]
}

View File

@@ -1,14 +1,16 @@
// Shared iOS signing defaults for local development + CI.
#include "Version.xcconfig"
OPENCLAW_IOS_DEFAULT_TEAM = Y5PE65HELJ
OPENCLAW_IOS_DEFAULT_TEAM = FWJYW4S8P8
OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM)
OPENCLAW_DEVELOPMENT_TEAM = $(OPENCLAW_IOS_SELECTED_TEAM)
OPENCLAW_CODE_SIGN_STYLE = Automatic
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.client
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.client.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.client.watchkitapp.extension
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.client.activitywidget
OPENCLAW_CODE_SIGN_IDENTITY = Apple Development
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp.extension
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
OPENCLAW_ACTIVITY_WIDGET_PROFILE =
OPENCLAW_WATCH_APP_PROFILE =
OPENCLAW_WATCH_EXTENSION_PROFILE =
@@ -18,7 +20,7 @@ OPENCLAW_WATCH_EXTENSION_PROFILE =
#include? "../LocalSigning.xcconfig"
CODE_SIGN_STYLE = $(OPENCLAW_CODE_SIGN_STYLE)
CODE_SIGN_IDENTITY = Apple Development
CODE_SIGN_IDENTITY = $(OPENCLAW_CODE_SIGN_IDENTITY)
DEVELOPMENT_TEAM = $(OPENCLAW_DEVELOPMENT_TEAM)
// Let Xcode manage provisioning for the selected local team unless a local override pins one.

View File

@@ -2,16 +2,18 @@
// This file is only an example and should stay committed.
OPENCLAW_CODE_SIGN_STYLE = Automatic
OPENCLAW_CODE_SIGN_IDENTITY = Apple Development
OPENCLAW_DEVELOPMENT_TEAM = YOUR_TEAM_ID
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.client
OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.client.share
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.client.activitywidget
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.client.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.client.watchkitapp.extension
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp.extension
// Leave empty with automatic signing.
OPENCLAW_APP_PROFILE =
OPENCLAW_SHARE_PROFILE =
OPENCLAW_ACTIVITY_WIDGET_PROFILE =
OPENCLAW_WATCH_APP_PROFILE =
OPENCLAW_WATCH_EXTENSION_PROFILE =

View File

@@ -4,8 +4,8 @@ This iOS app is super-alpha and internal-use only. The first public App Store re
## Distribution Status
- Public distribution: not available.
- Internal beta distribution: local archive + TestFlight upload via Fastlane.
- Public distribution: App Store Connect app created; production signing is configured through the App Store release Fastlane path.
- Internal TestFlight distribution: uses the same App Store distribution archive uploaded to App Store Connect.
- Local/manual deploy from source via Xcode remains the default development path.
## Super-Alpha Disclaimer
@@ -47,7 +47,7 @@ Shortcut command (same flow + open project):
pnpm ios:open
```
## Local Beta Release Flow
## App Store Release Flow
Prereqs:
@@ -55,51 +55,82 @@ Prereqs:
- `pnpm`
- `xcodegen`
- `fastlane`
- Apple account signed into Xcode for automatic signing/provisioning
- App Store Connect API key set up in Keychain via `scripts/ios-asc-keychain-setup.sh` when auto-resolving a beta build number or uploading to TestFlight
- Apple account signed into Xcode for the canonical OpenClaw team (`FWJYW4S8P8`)
- Fastlane Apple Developer Portal session for the canonical OpenClaw team when creating bundle IDs or enabling services
- Release-owner access to the encrypted signing repo password (`MATCH_PASSWORD`)
- App Store Connect app already created for `ai.openclawfoundation.app`
- App Store Connect API key set up in Keychain via `scripts/ios-app-store-connect-keychain-setup.sh` when auto-resolving a build number or uploading to App Store Connect
Release behavior:
- Local development keeps using unique per-developer bundle IDs from `scripts/ios-configure-signing.sh`.
- Beta release uses canonical `ai.openclaw.client*` bundle IDs through a temporary generated xcconfig in `apps/ios/build/BetaRelease.xcconfig`.
- Beta release also switches the app to `OpenClawPushTransport=relay`, `OpenClawPushDistribution=official`, and `OpenClawPushAPNsEnvironment=production`.
- The beta flow does not modify `apps/ios/.local-signing.xcconfig` or `apps/ios/LocalSigning.xcconfig`.
- Local development uses the canonical `ai.openclawfoundation.app*` bundle IDs when the OpenClaw team is available, and unique `ai.openclawfoundation.app.test.*` bundle IDs only for non-canonical fallback teams.
- App Store release uses canonical `ai.openclawfoundation.app*` bundle IDs through a temporary generated xcconfig in `apps/ios/build/AppStoreRelease.xcconfig`.
- App Store release uses manual `Apple Distribution` signing with profile names pinned in `apps/ios/Config/AppStoreSigning.json`.
- Fastlane owns one-time Developer Portal setup, encrypted `match` signing sync to the repo/branch pinned in `apps/ios/Config/AppStoreSigning.json`, and release handling.
- App Store release also switches the app to `OpenClawPushTransport=relay`, `OpenClawPushDistribution=official`, `OpenClawPushAPNsEnvironment=production`, and a production `aps-environment` entitlement.
- `pnpm ios:release:upload` generates App Store screenshots and uploads release notes before archiving and uploading the IPA.
- `pnpm ios:release` remains a compatibility alias for `pnpm ios:release:upload`; prefer the explicit upload command in new release docs and automation.
- App Review submission is manual in App Store Connect. The release lane uploads a build and metadata, but does not submit for review.
- The release flow does not modify `apps/ios/.local-signing.xcconfig` or `apps/ios/LocalSigning.xcconfig`.
- `apps/ios/version.json` is the pinned iOS release version source.
- `apps/ios/CHANGELOG.md` is the iOS-only changelog and release-note source.
- The pinned iOS version must use CalVer like `2026.4.10`.
- That pinned value becomes:
- `CFBundleShortVersionString = 2026.4.10`
- `CFBundleVersion = next TestFlight build number for 2026.4.10`
- `CFBundleVersion = next App Store Connect build number for 2026.4.10`
- Changing the root gateway version does not change the iOS app version until you explicitly pin from the gateway.
- See `apps/ios/VERSIONING.md` for the full workflow.
Relay behavior for beta builds:
Relay behavior for App Store builds:
- Beta builds default to `https://ios-push-relay.openclaw.ai`.
- Release builds default to `https://ios-push-relay.openclaw.ai`.
- Optional custom relay override: `OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com`
This must be a plain `https://host[:port][/path]` base URL without whitespace, query params, fragments, or xcconfig metacharacters.
Signing setup commands:
```bash
pnpm ios:release:signing:plan
pnpm ios:release:signing:check
pnpm ios:release:signing:setup
MATCH_PASSWORD=... pnpm ios:release:signing:sync:push
MATCH_PASSWORD=... pnpm ios:release:signing:sync:pull
```
Release-owner secrets:
- App Store Connect API auth uses Keychain for private key material plus non-secret `apps/ios/fastlane/.env` variables.
- The encrypted signing repo password lives outside this repo in the release-owner vault and is exposed locally as `MATCH_PASSWORD`.
- Apple Distribution private keys, certificates, provisioning profiles, and decrypted signing sync output stay under `apps/ios/build/` or Keychain and are gitignored.
- Rotating release signing means refreshing Fastlane `match` assets and pushing a fresh encrypted sync state.
Prepare the generated release xcconfig/project without archiving:
```bash
pnpm ios:release:prepare -- --build-number 7
```
Archive without upload:
```bash
pnpm ios:beta:archive
pnpm ios:release:archive
```
Archive and upload to TestFlight:
Archive and upload to App Store Connect:
```bash
pnpm ios:beta
pnpm ios:release:upload
```
If you need to force a specific build number:
```bash
pnpm ios:beta -- --build-number 7
pnpm ios:release:upload -- --build-number 7
```
### Maintainer Quick Release Checklist
Use this when a clone is missing local iOS release setup and you want the shortest path to a TestFlight upload.
Use this when a clone is missing local iOS release setup and you want the shortest path to an App Store Connect upload.
1. Confirm Fastlane auth is set up:
@@ -111,46 +142,58 @@ fastlane ios auth_check
2. If auth is missing, bootstrap it once on this Mac:
```bash
scripts/ios-asc-keychain-setup.sh \
scripts/ios-app-store-connect-keychain-setup.sh \
--key-path /absolute/path/to/AuthKey_XXXXXXXXXX.p8 \
--issuer-id YOUR_ISSUER_ID \
--write-env
```
This should create `apps/ios/fastlane/.env` with the non-secret ASC variables while the private key stays in Keychain.
This should create `apps/ios/fastlane/.env` with non-secret App Store Connect variables while the private key stays in Keychain.
3. Optional: set a custom official/TestFlight relay URL for the build. If unset, the beta flow uses `https://ios-push-relay.openclaw.ai`.
3. Confirm the App Store Connect app and Apple Developer identifiers/capabilities exist for:
- `ai.openclawfoundation.app`
- `ai.openclawfoundation.app.share`
- `ai.openclawfoundation.app.activitywidget`
- `ai.openclawfoundation.app.watchkitapp`
- `ai.openclawfoundation.app.watchkitapp.extension`
Use `pnpm ios:release:signing:setup` for the initial portal setup, then `MATCH_PASSWORD=... pnpm ios:release:signing:sync:push` to publish encrypted Fastlane match assets to the shared private repo.
4. Optional: set a custom official relay URL for the build. If unset, the release flow uses `https://ios-push-relay.openclaw.ai`.
```bash
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
```
4. If you are starting a brand-new production release train, pin iOS to the current gateway version first:
5. If you are starting a brand-new production release train, pin iOS to the current gateway version first:
```bash
pnpm ios:version:pin -- --from-gateway
```
5. Upload the beta:
6. Upload the build:
```bash
pnpm ios:beta
pnpm ios:release:upload
```
6. Expected behavior:
7. Expected behavior:
- Fastlane reads `apps/ios/version.json`
- verifies synced iOS versioning artifacts
- resolves the next TestFlight build number for that short version
- generates `apps/ios/build/BetaRelease.xcconfig`
- resolves the next App Store Connect build number for that short version
- generates deterministic App Store screenshots
- uploads release notes and screenshots to the editable App Store version
- generates `apps/ios/build/AppStoreRelease.xcconfig`
- archives `OpenClaw`
- uploads the IPA to TestFlight
- uploads the IPA to App Store Connect for TestFlight/App Review use
- leaves App Review submission for a maintainer to complete manually
7. Expected outputs after a successful run:
- `apps/ios/build/beta/OpenClaw-<version>.ipa`
- `apps/ios/build/beta/OpenClaw-<version>.app.dSYM.zip`
- Fastlane log line like `Uploaded iOS beta: version=<version> short=<short> build=<build>`
8. Expected outputs after a successful run:
- `apps/ios/build/app-store/OpenClaw-<version>.ipa`
- `apps/ios/build/app-store/OpenClaw-<version>.app.dSYM.zip`
- Fastlane log line like `Uploaded iOS App Store build: version=<version> short=<short> build=<build>`
8. If this is a fresh clone on a maintainer machine that already works elsewhere, it is OK to copy the non-secret `apps/ios/fastlane/.env` from another trusted local clone on the same Mac. The Keychain-backed private key remains machine-local and is not stored in the repo.
9. If this is a fresh clone on a maintainer machine that already works elsewhere, it is OK to copy the non-secret `apps/ios/fastlane/.env` from another trusted local clone on the same Mac. The Keychain-backed private key remains machine-local and is not stored in the repo.
## iOS Versioning Workflow
@@ -176,7 +219,7 @@ Recommended flow:
1. Keep `apps/ios/version.json` pinned to the current train version.
2. Update `apps/ios/CHANGELOG.md`, usually under `## Unreleased` while iterating.
3. Run `pnpm ios:version:sync` after changelog changes.
4. Upload more TestFlight builds with `pnpm ios:beta`.
4. Upload more TestFlight builds with `pnpm ios:release:upload`.
5. Let Fastlane bump only the numeric build number.
### Starting the next production release train
@@ -189,7 +232,7 @@ pnpm ios:version:pin -- --from-gateway
2. Update `apps/ios/CHANGELOG.md` for the new release as needed.
3. Run `pnpm ios:version:sync`.
4. Submit the first TestFlight build for that newly pinned version.
4. Submit the first App Store Connect build for that newly pinned version.
5. Keep iterating on that same version until the release candidate is ready.
See `apps/ios/VERSIONING.md` for the detailed spec.
@@ -197,9 +240,9 @@ See `apps/ios/VERSIONING.md` for the detailed spec.
## APNs Expectations For Local/Manual Builds
- The app calls `registerForRemoteNotifications()` at launch.
- `apps/ios/Sources/OpenClaw.entitlements` sets `aps-environment` to `development`.
- `apps/ios/Sources/OpenClaw.entitlements` derives `aps-environment` from the active build configuration/signing override.
- APNs token registration to gateway happens only after gateway connection (`push.apns.register`).
- Local/manual builds default to `OpenClawPushTransport=direct` and `OpenClawPushDistribution=local`.
- Local/manual builds default to `OpenClawPushTransport=direct`, `OpenClawPushDistribution=local`, and a development `aps-environment` entitlement.
- Your selected team/profile must support Push Notifications for the app bundle ID you are signing.
- If push capability or provisioning is wrong, APNs registration fails at runtime (check Xcode logs for `APNs registration failed`).
- The gateway host also needs direct APNs auth configured separately with `OPENCLAW_APNS_TEAM_ID`, `OPENCLAW_APNS_KEY_ID`, and either `OPENCLAW_APNS_PRIVATE_KEY_P8` or `OPENCLAW_APNS_PRIVATE_KEY_PATH`.
@@ -319,7 +362,7 @@ Automatic wake/reconnect hardening:
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`
- `ai.openclawfoundation.app`
- `GatewayDiag`
- `APNs registration failed`
7. Validate background expectations:

View File

@@ -17,7 +17,7 @@ final class ShareViewController: UIViewController {
var attachments: [ShareAttachment]
}
private let logger = Logger(subsystem: "ai.openclaw.ios", category: "ShareExtension")
private let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "ShareExtension")
private var statusLabel: UILabel?
private let draftTextView = UITextView()
private let sendButton = UIButton(type: .system)

View File

@@ -5,16 +5,21 @@
#include "Config/Version.xcconfig"
OPENCLAW_CODE_SIGN_STYLE = Manual
OPENCLAW_DEVELOPMENT_TEAM = Y5PE65HELJ
OPENCLAW_CODE_SIGN_IDENTITY = Apple Development
OPENCLAW_DEVELOPMENT_TEAM = FWJYW4S8P8
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.client
OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.client.share
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.client.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.client.watchkitapp.extension
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.client.activitywidget
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp.extension
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT = development
OPENCLAW_APP_PROFILE = ai.openclaw.client Development
OPENCLAW_SHARE_PROFILE = ai.openclaw.client.share Development
OPENCLAW_APP_PROFILE = ai.openclawfoundation.app Development
OPENCLAW_SHARE_PROFILE = ai.openclawfoundation.app.share Development
OPENCLAW_ACTIVITY_WIDGET_PROFILE =
OPENCLAW_WATCH_APP_PROFILE =
OPENCLAW_WATCH_EXTENSION_PROFILE =
// Keep local includes after defaults: xcconfig is evaluated top-to-bottom,
// so later assignments in local files override the defaults above.

View File

@@ -14,7 +14,50 @@ enum AppleReviewDemoMode {
}
static var agents: [AgentSummary] {
[
LocalChatFixture.appleReviewDemo.agents
}
}
enum ScreenshotFixtureMode {
static let gatewayName = "OpenClaw Gateway"
static let gatewayAddress = "Mac Studio on local network"
static let gatewayID = "screenshot-fixture-gateway"
static var agents: [AgentSummary] {
LocalChatFixture.appScreenshots.agents
}
}
struct LocalChatFixture {
let sessionKey: String
let sessionIDPrefix: String
let displayName: String
let subject: String
let workspace: String
let modelProvider: String
let modelID: String
let modelName: String
let responsePrefix: String
let seedMessages: [String]
let agents: [AgentSummary]
static let appleReviewDemo = LocalChatFixture(
sessionKey: "main",
sessionIDPrefix: "apple-review-demo",
displayName: "Apple Review Demo",
subject: "Gateway review flow",
workspace: "Apple Review Demo",
modelProvider: "demo",
modelID: "local-demo",
modelName: "Apple Review Demo",
responsePrefix: "Demo mode is active.",
seedMessages: [
"""
Apple Review demo mode is active. This local chat transport lets reviewers inspect the iOS app \
without a private Gateway.
""",
],
agents: [
AgentSummary(
id: "main",
name: "Main",
@@ -25,12 +68,70 @@ enum AppleReviewDemoMode {
thinkinglevels: nil,
thinkingoptions: ["auto", "low", "medium"],
thinkingdefault: "auto"),
]
}
])
static let appScreenshots = LocalChatFixture(
sessionKey: "main",
sessionIDPrefix: "screenshot-fixture",
displayName: "Molty",
subject: "Mobile command center",
workspace: "OpenClaw",
modelProvider: "openai",
modelID: "gpt-5.5",
modelName: "GPT-5.5",
responsePrefix: "OpenClaw is connected to your gateway.",
seedMessages: [
"""
OpenClaw is connected to your gateway. I can coordinate agents, inspect project context, and prepare \
actions from your phone.
""",
"""
The Molty agent is ready. Recent context, voice controls, and gateway settings are available \
across the app.
""",
],
agents: [
AgentSummary(
id: "main",
name: "Molty",
identity: ["emoji": AnyCodable("M")],
workspace: "OpenClaw",
model: ["provider": AnyCodable("openai"), "model": AnyCodable("gpt-5.5")],
agentruntime: ["kind": AnyCodable("gateway")],
thinkinglevels: nil,
thinkingoptions: ["auto", "low", "medium", "high"],
thinkingdefault: "auto"),
AgentSummary(
id: "research",
name: "Research",
identity: ["emoji": AnyCodable("RS")],
workspace: "OpenClaw",
model: ["provider": AnyCodable("openai"), "model": AnyCodable("gpt-5.5")],
agentruntime: ["kind": AnyCodable("gateway")],
thinkinglevels: nil,
thinkingoptions: ["auto", "low", "medium", "high"],
thinkingdefault: "medium"),
AgentSummary(
id: "automation",
name: "Automation",
identity: ["emoji": AnyCodable("AU")],
workspace: "OpenClaw",
model: ["provider": AnyCodable("openai"), "model": AnyCodable("gpt-5.5")],
agentruntime: ["kind": AnyCodable("gateway")],
thinkinglevels: nil,
thinkingoptions: ["auto", "low", "medium", "high"],
thinkingdefault: "auto"),
])
}
struct AppleReviewDemoChatTransport: OpenClawChatTransport {
private let store = AppleReviewDemoChatStore()
struct LocalFixtureChatTransport: OpenClawChatTransport {
private let fixture: LocalChatFixture
private let store: LocalFixtureChatStore
init(fixture: LocalChatFixture) {
self.fixture = fixture
self.store = LocalFixtureChatStore(fixture: fixture)
}
func createSession(
key: String,
@@ -47,9 +148,9 @@ struct AppleReviewDemoChatTransport: OpenClawChatTransport {
func listModels() async throws -> [OpenClawChatModelChoice] {
[
OpenClawChatModelChoice(
modelID: "local-demo",
name: "Apple Review Demo",
provider: "demo",
modelID: self.fixture.modelID,
name: self.fixture.modelName,
provider: self.fixture.modelProvider,
contextWindow: 128_000),
]
}
@@ -101,26 +202,102 @@ struct AppleReviewDemoChatTransport: OpenClawChatTransport {
func compactSession(sessionKey _: String) async throws {}
}
private actor AppleReviewDemoChatStore {
private let sessionKey = "main"
struct AppleReviewDemoChatTransport: OpenClawChatTransport {
private let transport = LocalFixtureChatTransport(fixture: .appleReviewDemo)
func createSession(
key: String,
label: String?,
parentSessionKey: String?) async throws -> OpenClawChatCreateSessionResponse
{
try await self.transport.createSession(key: key, label: label, parentSessionKey: parentSessionKey)
}
func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
try await self.transport.requestHistory(sessionKey: sessionKey)
}
func listModels() async throws -> [OpenClawChatModelChoice] {
try await self.transport.listModels()
}
func sendMessage(
sessionKey: String,
message: String,
thinking: String,
idempotencyKey: String,
attachments: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse
{
try await self.transport.sendMessage(
sessionKey: sessionKey,
message: message,
thinking: thinking,
idempotencyKey: idempotencyKey,
attachments: attachments)
}
func abortRun(sessionKey: String, runId: String) async throws {
try await self.transport.abortRun(sessionKey: sessionKey, runId: runId)
}
func listSessions(limit: Int?) async throws -> OpenClawChatSessionsListResponse {
try await self.transport.listSessions(limit: limit)
}
func setSessionModel(sessionKey: String, model: String?) async throws {
try await self.transport.setSessionModel(sessionKey: sessionKey, model: model)
}
func setSessionThinking(sessionKey: String, thinkingLevel: String) async throws {
try await self.transport.setSessionThinking(sessionKey: sessionKey, thinkingLevel: thinkingLevel)
}
func requestHealth(timeoutMs: Int) async throws -> Bool {
try await self.transport.requestHealth(timeoutMs: timeoutMs)
}
func waitForRunCompletion(runId: String, timeoutMs: Int) async -> Bool {
await self.transport.waitForRunCompletion(runId: runId, timeoutMs: timeoutMs)
}
func events() -> AsyncStream<OpenClawChatTransportEvent> {
self.transport.events()
}
func setActiveSessionKey(_ sessionKey: String) async throws {
try await self.transport.setActiveSessionKey(sessionKey)
}
func resetSession(sessionKey: String) async throws {
try await self.transport.resetSession(sessionKey: sessionKey)
}
func compactSession(sessionKey: String) async throws {
try await self.transport.compactSession(sessionKey: sessionKey)
}
}
private actor LocalFixtureChatStore {
private let fixture: LocalChatFixture
private var messages: [OpenClawChatMessage]
init() {
self.messages = AppleReviewDemoChatStore.seedMessages()
init(fixture: LocalChatFixture) {
self.fixture = fixture
self.messages = Self.seedMessages(fixture: fixture)
}
func createSession(key: String) throws -> OpenClawChatCreateSessionResponse {
try Self.decode(
CreateSessionPayload(ok: true, key: key, sessionId: "apple-review-demo-\(key)"),
CreateSessionPayload(ok: true, key: key, sessionId: "\(self.fixture.sessionIDPrefix)-\(key)"),
as: OpenClawChatCreateSessionResponse.self)
}
func history(sessionKey: String) throws -> OpenClawChatHistoryPayload {
let normalizedSessionKey = Self.normalizedSessionKey(sessionKey)
let normalizedSessionKey = Self.normalizedSessionKey(sessionKey, fallback: self.fixture.sessionKey)
return try Self.decode(
HistoryPayload(
sessionKey: normalizedSessionKey,
sessionId: "apple-review-demo-\(normalizedSessionKey)",
sessionId: "\(self.fixture.sessionIDPrefix)-\(normalizedSessionKey)",
messages: self.messages,
thinkingLevel: "auto"),
as: OpenClawChatHistoryPayload.self)
@@ -135,9 +312,8 @@ private actor AppleReviewDemoChatStore {
Self.message(
role: "assistant",
text: """
Demo mode is active. I can show the review flow locally for \(subject), including chat, agent \
selection, settings, and Gateway-connected UI states. Live automation requires pairing a real \
OpenClaw Gateway.
\(self.fixture.responsePrefix) I can help with \(subject), summarize current project context, \
prepare agent actions, and keep the mobile workflow connected to the gateway.
""",
timestamp: now + 1))
return try Self.decode(
@@ -147,15 +323,15 @@ private actor AppleReviewDemoChatStore {
func sessions() throws -> OpenClawChatSessionsListResponse {
let entry = OpenClawChatSessionEntry(
key: self.sessionKey,
key: self.fixture.sessionKey,
kind: "chat",
displayName: "Apple Review Demo",
displayName: self.fixture.displayName,
surface: "ios",
subject: "Gateway review flow",
subject: self.fixture.subject,
room: nil,
space: nil,
updatedAt: Date().timeIntervalSince1970 * 1000,
sessionId: "apple-review-demo-main",
sessionId: "\(self.fixture.sessionIDPrefix)-\(self.fixture.sessionKey)",
systemSent: true,
abortedLastRun: false,
thinkingLevel: "auto",
@@ -163,52 +339,51 @@ private actor AppleReviewDemoChatStore {
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
modelProvider: "demo",
model: "local-demo",
modelProvider: self.fixture.modelProvider,
model: self.fixture.modelID,
contextTokens: 128_000,
thinkingLevels: [
OpenClawChatThinkingLevelOption(id: "auto", label: "Auto"),
OpenClawChatThinkingLevelOption(id: "low", label: "Low"),
OpenClawChatThinkingLevelOption(id: "medium", label: "Medium"),
],
thinkingOptions: ["auto", "low", "medium"],
thinkingLevels: Self.thinkingLevels,
thinkingOptions: Self.thinkingOptions,
thinkingDefault: "auto")
return OpenClawChatSessionsListResponse(
ts: Date().timeIntervalSince1970 * 1000,
path: nil,
count: 1,
defaults: OpenClawChatSessionsDefaults(
modelProvider: "demo",
model: "local-demo",
modelProvider: self.fixture.modelProvider,
model: self.fixture.modelID,
contextTokens: 128_000,
thinkingLevels: [
OpenClawChatThinkingLevelOption(id: "auto", label: "Auto"),
OpenClawChatThinkingLevelOption(id: "low", label: "Low"),
OpenClawChatThinkingLevelOption(id: "medium", label: "Medium"),
],
thinkingOptions: ["auto", "low", "medium"],
thinkingLevels: Self.thinkingLevels,
thinkingOptions: Self.thinkingOptions,
thinkingDefault: "auto",
mainSessionKey: self.sessionKey),
mainSessionKey: self.fixture.sessionKey),
sessions: [entry])
}
func reset() {
self.messages = Self.seedMessages()
self.messages = Self.seedMessages(fixture: self.fixture)
}
private static func seedMessages() -> [OpenClawChatMessage] {
let now = Date().timeIntervalSince1970 * 1000
return [
self.message(
role: "assistant",
text: """
Apple Review demo mode is active. This local chat transport lets reviewers inspect the iOS app \
without a private Gateway.
""",
timestamp: now),
private static var thinkingOptions: [String] {
["auto", "low", "medium", "high"]
}
private static var thinkingLevels: [OpenClawChatThinkingLevelOption] {
[
OpenClawChatThinkingLevelOption(id: "auto", label: "Auto"),
OpenClawChatThinkingLevelOption(id: "low", label: "Low"),
OpenClawChatThinkingLevelOption(id: "medium", label: "Medium"),
OpenClawChatThinkingLevelOption(id: "high", label: "High"),
]
}
private static func seedMessages(fixture: LocalChatFixture) -> [OpenClawChatMessage] {
let now = Date().timeIntervalSince1970 * 1000
return fixture.seedMessages.enumerated().map { index, text in
self.message(role: "assistant", text: text, timestamp: now + Double(index))
}
}
private static func message(role: String, text: String, timestamp: Double) -> OpenClawChatMessage {
OpenClawChatMessage(
role: role,
@@ -223,9 +398,9 @@ private actor AppleReviewDemoChatStore {
timestamp: timestamp)
}
private static func normalizedSessionKey(_ value: String) -> String {
private static func normalizedSessionKey(_ value: String, fallback: String) -> String {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? "main" : trimmed
return trimmed.isEmpty ? fallback : trimmed
}
private static func decode<T: Decodable>(_ value: some Encodable, as type: T.Type) throws -> T {

View File

@@ -5,7 +5,7 @@ import OpenClawProtocol
import OSLog
struct IOSGatewayChatTransport: OpenClawChatTransport {
static let logger = Logger(subsystem: "ai.openclaw", category: "ios.chat.transport")
static let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "ios.chat.transport")
static let defaultChatSendTimeoutMs = 30000
private let gateway: GatewayNodeSession

View File

@@ -202,10 +202,4 @@ final class ContactsService: ContactsServicing {
phoneNumbers: contact.phoneNumbers.map(\.value.stringValue),
emails: contact.emailAddresses.map { String($0.value) })
}
#if DEBUG
static func _test_matches(contact: CNContact, phoneNumbers: [String], emails: [String]) -> Bool {
self.matchContacts(contacts: [contact], phoneNumbers: phoneNumbers, emails: emails) != nil
}
#endif
}

View File

@@ -1,5 +1,4 @@
import OpenClawKit
import OpenClawProtocol
import SwiftUI
extension AgentProTab {

View File

@@ -303,7 +303,7 @@ extension AgentProTab {
}
.padding(.vertical, 14)
.padding(.horizontal, 13)
.frame(minHeight: AgentLayout.rowMinHeight, alignment: .center)
.frame(maxWidth: .infinity, minHeight: AgentLayout.rowMinHeight, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
self.appModel.setSelectedAgentId(agent.id)
@@ -557,7 +557,7 @@ extension AgentProTab {
}
var liveGatewayConnected: Bool {
!self.appModel.isAppleReviewDemoModeEnabled &&
!self.appModel.isLocalGatewayFixtureEnabled &&
self.gatewayConnected &&
self.appModel.isOperatorGatewayConnected
}

View File

@@ -1,12 +1,10 @@
import OpenClawKit
import OpenClawProtocol
import SwiftUI
struct AgentProTab: View {
@Environment(NodeAppModel.self) var appModel
@Environment(\.colorScheme) var colorScheme
@Environment(\.scenePhase) var scenePhase
let initialRoute: AgentRoute?
let directRoute: AgentRoute?
let headerLeadingAction: OpenClawSidebarHeaderAction?
let headerTitle: String
@@ -127,13 +125,11 @@ struct AgentProTab: View {
}
init(
initialRoute: AgentRoute? = nil,
directRoute: AgentRoute? = nil,
headerLeadingAction: OpenClawSidebarHeaderAction? = nil,
headerTitle: String = "Agents",
openSettings: (() -> Void)? = nil)
{
self.initialRoute = initialRoute
self.directRoute = directRoute
self.headerLeadingAction = headerLeadingAction
self.headerTitle = headerTitle
@@ -184,9 +180,6 @@ struct AgentProTab: View {
self.destination(for: route)
}
}
.onAppear {
self.applyInitialRouteIfNeeded()
}
}
private func directDestination(for route: AgentRoute) -> some View {
@@ -195,11 +188,4 @@ struct AgentProTab: View {
self.directHeaderLeadingAction(for: route) == nil ? .visible : .hidden,
for: .navigationBar)
}
private func applyInitialRouteIfNeeded() {
guard self.directRoute == nil else { return }
guard let initialRoute else { return }
guard self.navigationPath != [initialRoute] else { return }
self.navigationPath = [initialRoute]
}
}

View File

@@ -6,7 +6,7 @@ struct ChatProTab: View {
@Environment(NodeAppModel.self) private var appModel
@Environment(\.colorScheme) private var colorScheme
@State private var viewModel: OpenClawChatViewModel?
@State private var viewModelUsesAppleReviewDemoTransport = false
@State private var viewModelTransportModeID = ""
let headerLeadingAction: OpenClawSidebarHeaderAction?
let headerTitle: String?
let headerSubtitle: String?
@@ -64,6 +64,7 @@ struct ChatProTab: View {
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.safeAreaPadding(.top, 8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.navigationBarHidden(true)
@@ -78,6 +79,10 @@ struct ChatProTab: View {
self.syncChatViewModel()
self.viewModel?.refresh()
}
.onChange(of: self.appModel.isScreenshotFixtureModeEnabled) { _, _ in
self.syncChatViewModel()
self.viewModel?.refresh()
}
.onChange(of: self.appModel.isOperatorGatewayConnected) { _, connected in
guard connected else { return }
self.syncChatViewModel()
@@ -103,7 +108,6 @@ struct ChatProTab: View {
self.connectionPillButton
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
.padding(.top, 8)
.padding(.bottom, 4)
}
@@ -135,14 +139,12 @@ struct ChatProTab: View {
private func syncChatViewModel() {
let sessionKey = self.appModel.chatSessionKey
let usesDemoTransport = self.appModel.isAppleReviewDemoModeEnabled
let transportModeID = self.appModel.chatTransportModeID
guard let viewModel else {
self.viewModelUsesAppleReviewDemoTransport = usesDemoTransport
self.viewModelTransportModeID = transportModeID
self.viewModel = OpenClawChatViewModel(
sessionKey: sessionKey,
transport: usesDemoTransport
? AppleReviewDemoChatTransport()
: IOSGatewayChatTransport(gateway: self.appModel.operatorSession),
transport: self.appModel.makeChatTransport(),
onSessionChanged: { sessionKey in
self.appModel.focusChatSession(sessionKey)
},
@@ -151,13 +153,11 @@ struct ChatProTab: View {
})
return
}
if self.viewModelUsesAppleReviewDemoTransport != usesDemoTransport {
self.viewModelUsesAppleReviewDemoTransport = usesDemoTransport
if self.viewModelTransportModeID != transportModeID {
self.viewModelTransportModeID = transportModeID
self.viewModel = OpenClawChatViewModel(
sessionKey: sessionKey,
transport: usesDemoTransport
? AppleReviewDemoChatTransport()
: IOSGatewayChatTransport(gateway: self.appModel.operatorSession),
transport: self.appModel.makeChatTransport(),
onSessionChanged: { sessionKey in
self.appModel.focusChatSession(sessionKey)
},
@@ -226,7 +226,7 @@ struct ChatProTab: View {
guard self.gatewayDisplayState == .connected else {
return false
}
return self.appModel.isAppleReviewDemoModeEnabled || self.appModel.isOperatorGatewayConnected
return self.appModel.isLocalChatFixtureEnabled || self.appModel.isOperatorGatewayConnected
}
private var gatewayDisplayState: GatewayDisplayState {

View File

@@ -185,33 +185,3 @@ struct CommandEmptyStateRow: View {
}
}
}
struct CommandTaskRow: View {
let item: CommandCenterTab.WorkItem
var body: some View {
HStack(alignment: .center, spacing: 6) {
Text(self.item.title)
.font(.footnote.weight(.semibold))
.lineLimit(1)
.minimumScaleFactor(0.80)
.frame(maxWidth: .infinity, minHeight: 20, alignment: .leading)
Text(self.item.detail)
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
.lineLimit(1)
.minimumScaleFactor(0.78)
.frame(width: 64, alignment: .leading)
if let progress = self.item.progress {
ProProgressBar(progress: progress, color: self.item.color)
.frame(width: 56)
}
Text(self.item.state)
.font(.footnote.weight(.medium))
.foregroundStyle(self.item.progress == nil ? self.item.color : .secondary)
.lineLimit(1)
.frame(width: self.item.progress == nil ? 58 : 34, alignment: .trailing)
}
.padding(.vertical, 8)
}
}

View File

@@ -370,12 +370,11 @@ struct CommandCenterTab: View {
}
private var sessionListAvailable: Bool {
self.appModel.isAppleReviewDemoModeEnabled || self.appModel.isOperatorGatewayConnected
self.appModel.isLocalChatFixtureEnabled || self.appModel.isOperatorGatewayConnected
}
private var sessionListMode: String {
if self.appModel.isAppleReviewDemoModeEnabled { return "demo" }
return self.appModel.isOperatorGatewayConnected ? "operator" : "offline"
self.appModel.chatTransportModeID
}
private var sessionItems: [WorkItem] {
@@ -414,9 +413,7 @@ struct CommandCenterTab: View {
}
do {
let transport: any OpenClawChatTransport = self.appModel.isAppleReviewDemoModeEnabled
? AppleReviewDemoChatTransport()
: IOSGatewayChatTransport(gateway: self.appModel.operatorSession)
let transport = self.appModel.makeChatTransport()
let response = try await transport.listSessions(limit: Self.recentSessionsFetchLimit)
self.defaultChatSessionEntry = response.sessions.first {
$0.key == self.appModel.defaultChatSessionKey
@@ -765,9 +762,7 @@ struct CommandSessionsScreen: View {
defer { self.isLoading = false }
do {
let transport: any OpenClawChatTransport = self.appModel.isAppleReviewDemoModeEnabled
? AppleReviewDemoChatTransport()
: IOSGatewayChatTransport(gateway: self.appModel.operatorSession)
let transport = self.appModel.makeChatTransport()
let response = try await transport.listSessions(limit: CommandCenterTab.recentSessionsFetchLimit)
self.sessions = response.sessions
} catch {
@@ -779,11 +774,10 @@ struct CommandSessionsScreen: View {
extension NodeAppModel {
fileprivate var isCommandSessionListAvailable: Bool {
self.isAppleReviewDemoModeEnabled || self.isOperatorGatewayConnected
self.isLocalChatFixtureEnabled || self.isOperatorGatewayConnected
}
fileprivate var commandSessionListMode: String {
if self.isAppleReviewDemoModeEnabled { return "demo" }
return self.isOperatorGatewayConnected ? "operator" : "offline"
self.chatTransportModeID
}
}

View File

@@ -180,12 +180,11 @@ struct IPadActivityScreen: View {
}
private var sessionsAvailable: Bool {
self.appModel.isAppleReviewDemoModeEnabled || self.appModel.isOperatorGatewayConnected
self.appModel.isLocalChatFixtureEnabled || self.appModel.isOperatorGatewayConnected
}
private var sessionsMode: String {
if self.appModel.isAppleReviewDemoModeEnabled { return "demo" }
return self.appModel.isOperatorGatewayConnected ? "operator" : "offline"
self.appModel.chatTransportModeID
}
private var sessionRows: [CommandCenterTab.WorkItem] {
@@ -215,9 +214,7 @@ struct IPadActivityScreen: View {
defer { self.isLoading = false }
do {
let transport: any OpenClawChatTransport = self.appModel.isAppleReviewDemoModeEnabled
? AppleReviewDemoChatTransport()
: IOSGatewayChatTransport(gateway: self.appModel.operatorSession)
let transport = self.appModel.makeChatTransport()
let response = try await transport.listSessions(limit: CommandCenterTab.recentSessionsFetchLimit)
self.sessions = response.sessions
} catch {

View File

@@ -213,32 +213,6 @@ struct IPadSkillWorkshopScreen: View {
}
}
private var statusMenu: some View {
HStack(spacing: 8) {
Text("Status")
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
Menu {
ForEach(Self.proposalStatusFilters, id: \.self) { filter in
Button(Self.proposalStatusFilterLabel(filter)) {
self.statusFilter = filter
}
}
} label: {
HStack(spacing: 6) {
Text(self.statusFilterLabel)
.font(.subheadline.weight(.semibold))
Image(systemName: "chevron.up.chevron.down")
.font(.caption2.weight(.bold))
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.buttonStyle(.bordered)
.controlSize(.small)
.tint(self.neutralControlTint)
}
}
private var agentScopeMenu: some View {
HStack(spacing: 8) {
Text("Agent")
@@ -1130,7 +1104,6 @@ struct IPadSkillProposalRecord: Decodable {
let description: String
let createdAt: String
let updatedAt: String
let proposedVersion: String
let target: IPadSkillProposalTarget
}

View File

@@ -47,13 +47,6 @@ enum AppAppearancePreference: String, CaseIterable, Identifiable {
}
enum OpenClawBrand {
static let lightCanvasTop = Color(red: 246 / 255.0, green: 247 / 255.0, blue: 249 / 255.0)
static let lightCanvasMiddle = Color(red: 250 / 255.0, green: 251 / 255.0, blue: 252 / 255.0)
static let lightCanvasBottom = Color.white
static let darkCanvasTop = Color(red: 3 / 255.0, green: 7 / 255.0, blue: 7 / 255.0)
static let darkCanvasMiddle = Color(red: 13 / 255.0, green: 17 / 255.0, blue: 17 / 255.0)
static let darkCanvasBottom = Color(red: 17 / 255.0, green: 18 / 255.0, blue: 20 / 255.0)
static let accent = Color(uiColor: UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 198 / 255.0, green: 62 / 255.0, blue: 56 / 255.0, alpha: 1)
@@ -81,11 +74,6 @@ enum OpenClawBrand {
? UIColor(red: 34 / 255.0, green: 36 / 255.0, blue: 39 / 255.0, alpha: 1)
: UIColor.white
})
static let graphiteSoft = Color(uiColor: UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 148 / 255.0, green: 163 / 255.0, blue: 184 / 255.0, alpha: 1)
: UIColor(red: 102 / 255.0, green: 112 / 255.0, blue: 133 / 255.0, alpha: 1)
})
static var sheetBackground: LinearGradient {
LinearGradient(
@@ -97,40 +85,6 @@ enum OpenClawBrand {
startPoint: .topLeading,
endPoint: .bottomTrailing)
}
static var toolbarChrome: LinearGradient {
LinearGradient(
colors: [
graphiteElevated.opacity(0.92),
graphite.opacity(0.78),
],
startPoint: .topLeading,
endPoint: .bottomTrailing)
}
static func glassFill(brighten: Bool) -> Color {
Color.black.opacity(brighten ? 0.10 : 0.22)
}
static func glassStroke(brighten: Bool, increasedContrast: Bool, active: Bool = false) -> Color {
if active {
return self.accent.opacity(increasedContrast ? 0.70 : 0.46)
}
return Color.white.opacity(increasedContrast ? 0.50 : (brighten ? 0.24 : 0.16))
}
static func formSectionHeader(_ title: String) -> some View {
Text(title)
.font(.caption.weight(.semibold))
.foregroundStyle(self.accent)
.textCase(.uppercase)
}
static func canvasColors(for colorScheme: ColorScheme) -> [Color] {
colorScheme == .dark
? [self.darkCanvasTop, self.darkCanvasMiddle, self.darkCanvasBottom]
: [self.lightCanvasTop, self.lightCanvasMiddle, self.lightCanvasBottom]
}
}
extension View {

View File

@@ -5,7 +5,6 @@ enum OpenClawProMetric {
static let cardRadius: CGFloat = 10
static let controlRadius: CGFloat = 8
static let bottomScrollInset: CGFloat = 96
static let heroRadius: CGFloat = 12
}
struct OpenClawProBackground: View {
@@ -250,13 +249,6 @@ struct OpenClawSidebarRevealButton: View {
self.headerAction = action
}
init(action: @escaping () -> Void) {
self.headerAction = OpenClawSidebarHeaderAction(
systemName: "sidebar.left",
accessibilityLabel: "Show Sidebar",
action: action)
}
var body: some View {
let button = Button(action: self.headerAction.action) {
Image(systemName: self.headerAction.systemName)
@@ -430,46 +422,6 @@ struct ProProgressBar: View {
}
}
struct ProWorkRow: View {
let icon: String
let title: String
let detail: String
let state: String
let trailing: String
let color: Color
var progress: Double?
var body: some View {
HStack(alignment: .top, spacing: 12) {
ProIconBadge(systemName: self.icon, color: self.color)
VStack(alignment: .leading, spacing: 5) {
HStack(alignment: .firstTextBaseline) {
Text(self.title)
.font(.subheadline.weight(.semibold))
Spacer(minLength: 8)
Text(self.trailing)
.font(.caption2)
.foregroundStyle(.secondary)
}
Text(self.detail)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
HStack(spacing: 8) {
if let progress {
ProProgressBar(progress: progress, color: self.color)
.frame(maxWidth: 120)
}
Text(self.state)
.font(.caption2.weight(.semibold))
.foregroundStyle(self.color)
}
}
}
.padding(.vertical, 9)
}
}
struct ProCapsule: View {
@Environment(\.colorScheme) private var colorScheme
let title: String
@@ -553,94 +505,6 @@ struct OpenClawGatewayCompactPill: View {
}
}
struct ProSegmentedControl: View {
@Environment(\.colorScheme) private var colorScheme
let labels: [String]
@Binding var selection: Int
var body: some View {
HStack(spacing: 4) {
ForEach(Array(self.labels.enumerated()), id: \.offset) { index, label in
Button {
self.selection = index
} label: {
Text(label)
.font(.subheadline.weight(self.selection == index ? .semibold : .regular))
.frame(maxWidth: .infinity)
.padding(.vertical, 9)
.background(self.segmentFill(isSelected: self.selection == index), in: Capsule())
}
.buttonStyle(.plain)
}
}
.padding(4)
.background {
Capsule()
.fill(self.trackFill)
.overlay {
Capsule().strokeBorder(self.trackStroke, lineWidth: 1)
}
}
}
private func segmentFill(isSelected: Bool) -> Color {
guard isSelected else { return .clear }
return self.colorScheme == .dark ? Color.white.opacity(0.12) : Color.primary.opacity(0.08)
}
private var trackFill: Color {
self.colorScheme == .dark ? Color.white.opacity(0.045) : Color.white.opacity(0.72)
}
private var trackStroke: Color {
self.colorScheme == .dark ? Color.white.opacity(0.10) : Color.black.opacity(0.06)
}
}
struct ProHeroActionButton: View {
@Environment(\.colorScheme) private var colorScheme
let title: String
let detail: String
let systemImage: String
let action: () -> Void
var body: some View {
Button(action: self.action) {
HStack(spacing: 12) {
Image(systemName: self.systemImage)
.font(.headline.weight(.semibold))
.foregroundStyle(.white)
.frame(width: 42, height: 42)
.background(OpenClawBrand.accentHot, in: RoundedRectangle(cornerRadius: 13, style: .continuous))
VStack(alignment: .leading, spacing: 3) {
Text(self.title)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
Text(self.detail)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer(minLength: 8)
Image(systemName: "arrow.right")
.font(.subheadline.weight(.bold))
.foregroundStyle(OpenClawBrand.accentHot)
}
.padding(12)
.proGlassSurface(
fill: self.colorScheme == .dark ? Color.white.opacity(0.045) : Color.white.opacity(0.68),
stroke: OpenClawBrand.accent.opacity(self.colorScheme == .dark ? 0.22 : 0.14),
radius: 18,
isProminent: true,
interactive: true)
}
.buttonStyle(.plain)
}
}
struct ProMetricTile: View {
@Environment(\.colorScheme) private var colorScheme
let title: String
@@ -795,24 +659,3 @@ struct ProStatusRow: View {
.padding(.vertical, 10)
}
}
struct ProTimelineRow: View {
let done: Bool
let title: String
let detail: String
var body: some View {
HStack(alignment: .top, spacing: 10) {
ProIconBadge(
systemName: self.done ? "checkmark.circle.fill" : "clock.fill",
color: self.done ? OpenClawBrand.ok : OpenClawBrand.warn)
VStack(alignment: .leading, spacing: 3) {
Text(self.title)
.font(.subheadline.weight(.medium))
Text(self.detail)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}

View File

@@ -1,3 +0,0 @@
import SwiftUI
// Pro UI surfaces are split by tab to keep SwiftLint file-length signal useful.

View File

@@ -332,65 +332,6 @@ struct SettingsChannelsDestination: View {
}
}
struct SettingsChannelsScreen: View {
let headerLeadingAction: OpenClawSidebarHeaderAction?
let gatewayAction: (() -> Void)?
init(headerLeadingAction: OpenClawSidebarHeaderAction? = nil, gatewayAction: (() -> Void)? = nil) {
self.headerLeadingAction = headerLeadingAction
self.gatewayAction = gatewayAction
}
var body: some View {
ZStack {
OpenClawProBackground()
ScrollView {
VStack(alignment: .leading, spacing: 14) {
self.header
SettingsChannelsDestination(showsSummaryCard: false)
}
.padding(.top, 18)
.padding(.bottom, OpenClawProMetric.bottomScrollInset)
}
}
.navigationTitle("Channels")
.navigationBarTitleDisplayMode(.inline)
}
private var header: some View {
HStack(alignment: .top, spacing: 12) {
if let headerLeadingAction {
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
}
VStack(alignment: .leading, spacing: 5) {
Text("Channels / Integrations")
.font(.title3.weight(.semibold))
Text("Message routing and external channel clients.")
.font(.callout)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer(minLength: 8)
self.gatewayPill
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
@ViewBuilder
private var gatewayPill: some View {
if let gatewayAction {
Button(action: gatewayAction) {
OpenClawGatewayCompactPill()
}
.buttonStyle(.plain)
.accessibilityHint("Opens Settings / Gateway")
} else {
OpenClawGatewayCompactPill()
}
}
}
private struct SettingsChannelRow: View {
let entry: SettingsChannelEntry
let canAdmin: Bool

View File

@@ -139,15 +139,6 @@ extension SettingsProTab {
await self.gatewayController.connectLastKnown()
}
func refreshGateway() async {
guard !self.isRefreshingGateway else { return }
self.isRefreshingGateway = true
defer { self.isRefreshingGateway = false }
self.gatewayController.refreshActiveGatewayRegistrationFromSettings()
self.gatewayController.restartDiscovery()
await self.appModel.refreshGatewayOverviewIfConnected()
}
@MainActor
func runDiagnostics() async {
guard !self.isRefreshingGateway else { return }
@@ -200,7 +191,7 @@ extension SettingsProTab {
self.setupStatusText = "Failed: invalid port"
return
}
guard await self.preflightGateway(host: host, port: port, useTLS: self.manualGatewayTLS) else { return }
guard await self.preflightGateway(host: host, port: port) else { return }
self.setupStatusText = "Setup code applied. Connecting..."
await self.connectManual()
}
@@ -298,7 +289,7 @@ extension SettingsProTab {
self.setupStatusText = "Failed: invalid port"
return
}
guard await self.preflightGateway(host: host, port: port, useTLS: self.manualGatewayTLS) else { return }
guard await self.preflightGateway(host: host, port: port) else { return }
await self.connectManual()
}
@@ -327,7 +318,7 @@ extension SettingsProTab {
authOverride: authOverride)
}
func preflightGateway(host: String, port: Int, useTLS: Bool) async -> Bool {
func preflightGateway(host: String, port: Int) async -> Bool {
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return false }
if Self.isTailnetHostOrIP(trimmed), !Self.hasTailnetIPv4() {

View File

@@ -6,7 +6,7 @@ final class NetworkStatusService: @unchecked Sendable {
func currentStatus(timeoutMs: Int = 1500) async -> OpenClawNetworkStatusPayload {
await withCheckedContinuation { cont in
let monitor = NWPathMonitor()
let queue = DispatchQueue(label: "ai.openclaw.ios.network-status")
let queue = DispatchQueue(label: "ai.openclawfoundation.app.network-status")
let state = NetworkStatusState()
monitor.pathUpdateHandler = { path in

View File

@@ -3,7 +3,6 @@ import Contacts
import CoreLocation
import CoreMotion
import CryptoKit
import Darwin
import EventKit
import Foundation
import Network
@@ -126,6 +125,7 @@ final class GatewayConnectionController {
private(set) var pendingTrustPrompt: TrustPrompt?
private let discovery = GatewayDiscoveryModel()
private let discoveryEnabled: Bool
private weak var appModel: NodeAppModel?
private var didAutoConnect = false
private var pendingServiceResolvers: [String: GatewayServiceResolver] = [:]
@@ -138,6 +138,7 @@ final class GatewayConnectionController {
}
init(appModel: NodeAppModel, startDiscovery: Bool = true) {
self.discoveryEnabled = startDiscovery
self.appModel = appModel
GatewaySettingsStore.bootstrapPersistence()
@@ -147,7 +148,7 @@ final class GatewayConnectionController {
self.updateFromDiscovery()
self.observeDiscovery()
if startDiscovery {
if self.discoveryEnabled {
self.discovery.start()
}
}
@@ -157,6 +158,11 @@ final class GatewayConnectionController {
}
func setScenePhase(_ phase: ScenePhase) {
guard self.discoveryEnabled else {
self.discovery.stop()
return
}
switch phase {
case .background:
self.discovery.stop()
@@ -169,12 +175,13 @@ final class GatewayConnectionController {
}
}
func allowAutoConnectAgain() {
self.didAutoConnect = false
self.maybeAutoConnect()
}
func restartDiscovery() {
guard self.discoveryEnabled else {
self.discovery.stop()
self.updateFromDiscovery()
return
}
self.discovery.stop()
self.didAutoConnect = false
self.discovery.start()
@@ -522,8 +529,7 @@ final class GatewayConnectionController {
let stableID = self.manualStableID(host: manualHost, port: resolvedPort)
let tlsParams = self.resolveManualTLSParams(
stableID: stableID,
tlsEnabled: resolvedUseTLS,
allowTOFUReset: self.shouldRequireTLS(host: manualHost))
tlsEnabled: resolvedUseTLS)
guard let url = self.buildGatewayURL(
host: manualHost,
@@ -719,8 +725,7 @@ final class GatewayConnectionController {
}
private func resolveDiscoveredTLSParams(
gateway: GatewayDiscoveryModel.DiscoveredGateway,
allowTOFU: Bool) -> GatewayTLSParams?
gateway: GatewayDiscoveryModel.DiscoveredGateway) -> GatewayTLSParams?
{
let stableID = gateway.stableID
let stored = GatewayTLSStore.loadFingerprint(stableID: stableID)
@@ -747,8 +752,7 @@ final class GatewayConnectionController {
private func resolveManualTLSParams(
stableID: String,
tlsEnabled: Bool,
allowTOFUReset: Bool = false) -> GatewayTLSParams?
tlsEnabled: Bool) -> GatewayTLSParams?
{
let stored = GatewayTLSStore.loadFingerprint(stableID: stableID)
if tlsEnabled || stored != nil {
@@ -785,126 +789,6 @@ final class GatewayConnectionController {
resolver.start()
}
}
private func resolveHostPortFromBonjourEndpoint(_ endpoint: NWEndpoint) async -> (host: String, port: Int)? {
switch endpoint {
case let .hostPort(host, port):
(host: host.debugDescription, port: Int(port.rawValue))
case let .service(name, type, domain, _):
await Self.resolveBonjourServiceToHostPort(name: name, type: type, domain: domain)
default:
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 }
let bytes = buffer.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) }
return String(bytes: bytes, encoding: .utf8)
}
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)
}
}
}
}
extension GatewayConnectionController {
@@ -1162,30 +1046,10 @@ extension GatewayConnectionController {
self.currentCommands()
}
func _test_currentPermissions() async -> [String: Bool] {
await self.currentPermissions()
}
static func _test_isLocationAvailable(servicesEnabled: Bool, status: CLAuthorizationStatus) -> Bool {
self.isLocationAvailable(servicesEnabled: servicesEnabled, status: status)
}
func _test_platformString() -> String {
DeviceInfoHelper.platformString()
}
func _test_deviceFamily() -> String {
DeviceInfoHelper.deviceFamily()
}
func _test_modelIdentifier() -> String {
DeviceInfoHelper.modelIdentifier()
}
func _test_appVersion() -> String {
DeviceInfoHelper.appVersion()
}
func _test_setGateways(_ gateways: [GatewayDiscoveryModel.DiscoveredGateway]) {
self.gateways = gateways
}
@@ -1199,10 +1063,9 @@ extension GatewayConnectionController {
}
func _test_resolveDiscoveredTLSParams(
gateway: GatewayDiscoveryModel.DiscoveredGateway,
allowTOFU: Bool) -> GatewayTLSParams?
gateway: GatewayDiscoveryModel.DiscoveredGateway) -> GatewayTLSParams?
{
self.resolveDiscoveredTLSParams(gateway: gateway, allowTOFU: allowTOFU)
self.resolveDiscoveredTLSParams(gateway: gateway)
}
func _test_resolveManualUseTLS(host: String, useTLS: Bool) -> Bool {

View File

@@ -59,7 +59,7 @@ final class GatewayDiscoveryModel {
let browser = GatewayDiscoveryBrowserSupport.makeBrowser(
serviceType: OpenClawBonjour.gatewayServiceType,
domain: domain,
queueLabelPrefix: "ai.openclaw.ios.gateway-discovery",
queueLabelPrefix: "ai.openclawfoundation.app.gateway-discovery",
onState: { [weak self] state in
guard let self else { return }
self.statesByDomain[domain] = state

View File

@@ -2,18 +2,13 @@ import Foundation
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 gatewayService = "ai.openclawfoundation.app.gateway"
private static let nodeService = "ai.openclawfoundation.app.node"
private static let talkService = "ai.openclawfoundation.app.talk"
private static let instanceIdDefaultsKey = "node.instanceId"
private static let preferredGatewayStableIDDefaultsKey = "gateway.preferredStableID"
private static let lastDiscoveredGatewayStableIDDefaultsKey = "gateway.lastDiscoveredStableID"
private static let manualEnabledDefaultsKey = "gateway.manual.enabled"
private static let manualHostDefaultsKey = "gateway.manual.host"
private static let manualPortDefaultsKey = "gateway.manual.port"
private static let manualTlsDefaultsKey = "gateway.manual.tls"
private static let discoveryDebugLogsDefaultsKey = "gateway.discovery.debugLogs"
private static let lastGatewayKindDefaultsKey = "gateway.last.kind"
private static let lastGatewayHostDefaultsKey = "gateway.last.host"
private static let lastGatewayPortDefaultsKey = "gateway.last.port"
@@ -184,24 +179,6 @@ enum GatewaySettingsStore {
enum LastGatewayConnection: Equatable {
case manual(host: String, port: Int, useTLS: Bool, stableID: String)
case discovered(stableID: String, useTLS: Bool)
var stableID: String {
switch self {
case let .manual(_, _, _, stableID):
stableID
case let .discovered(stableID, _):
stableID
}
}
var useTLS: Bool {
switch self {
case let .manual(_, _, useTLS, _):
useTLS
case let .discovered(_, useTLS):
useTLS
}
}
}
private enum LastGatewayKind: String, Codable {
@@ -229,17 +206,6 @@ enum GatewaySettingsStore {
return nil
}
static func saveTalkProviderApiKey(_ apiKey: String?, provider: String) {
guard let providerId = self.normalizedTalkProviderID(provider) else { return }
let account = self.talkProviderApiKeyAccount(providerId: providerId)
let trimmed = apiKey?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if trimmed.isEmpty {
_ = KeychainStore.delete(service: self.talkService, account: account)
return
}
_ = KeychainStore.saveString(trimmed, service: self.talkService, account: account)
}
static func saveLastGatewayConnectionManual(host: String, port: Int, useTLS: Bool, stableID: String) {
let payload = LastGatewayConnectionData(
kind: .manual, stableID: stableID, useTLS: useTLS, host: host, port: port)
@@ -477,8 +443,8 @@ enum GatewaySettingsStore {
}
enum GatewayDiagnostics {
private static let logger = Logger(subsystem: "ai.openclaw.ios", category: "GatewayDiag")
private static let queue = DispatchQueue(label: "ai.openclaw.gateway.diagnostics")
private static let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "GatewayDiag")
private static let queue = DispatchQueue(label: "ai.openclawfoundation.app.gateway.diagnostics")
private static let maxLogBytes: Int64 = 512 * 1024
private static let keepLogBytes: Int64 = 256 * 1024
private static let logSizeCheckEveryWrites = 50
@@ -580,11 +546,4 @@ enum GatewayDiagnostics {
}
}
}
static func reset() {
guard let url = fileURL else { return }
self.queue.async {
try? FileManager.default.removeItem(at: url)
}
}
}

View File

@@ -4,7 +4,7 @@
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>ai.openclaw.ios.bgrefresh</string>
<string>$(OPENCLAW_APP_BUNDLE_ID).bgrefresh</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
@@ -28,7 +28,7 @@
<array>
<dict>
<key>CFBundleURLName</key>
<string>ai.openclaw.ios</string>
<string>ai.openclawfoundation.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>openclaw</string>

View File

@@ -7,7 +7,7 @@ import os
final class LiveActivityManager {
static let shared = LiveActivityManager()
private let logger = Logger(subsystem: "ai.openclaw.ios", category: "LiveActivity")
private let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "LiveActivity")
private let connectingStaleSeconds: TimeInterval = 120
private let hydrationStaleSeconds: TimeInterval = 300
private var currentActivity: Activity<OpenClawActivityAttributes>?
@@ -17,15 +17,6 @@ final class LiveActivityManager {
self.hydrateCurrentAndPruneDuplicates()
}
var isActive: Bool {
guard let activity = self.currentActivity else { return false }
guard activity.activityState == .active else {
self.currentActivity = nil
return false
}
return true
}
func showConnecting(statusText: String = "Connecting...", agentName: String, sessionKey: String) {
self.hydrateCurrentAndPruneDuplicates()
@@ -96,10 +87,6 @@ final class LiveActivityManager {
self.endActivity(reason: "connected")
}
func handleDisconnect() {
self.endActivity(reason: "disconnected")
}
func endActivity(reason: String) {
guard let activity = self.currentActivity else { return }
self.currentActivity = nil
@@ -183,15 +170,6 @@ final class LiveActivityManager {
startedAt: self.activityStartDate)
}
private func idleState() -> OpenClawActivityAttributes.ContentState {
OpenClawActivityAttributes.ContentState(
statusText: "Idle",
isIdle: true,
isDisconnected: false,
isConnecting: false,
startedAt: self.activityStartDate)
}
private func disconnectedState() -> OpenClawActivityAttributes.ContentState {
OpenClawActivityAttributes.ContentState(
statusText: "Disconnected",

View File

@@ -14,39 +14,3 @@ struct OpenClawActivityAttributes: ActivityAttributes {
var startedAt: Date
}
}
#if DEBUG
extension OpenClawActivityAttributes {
static let preview = OpenClawActivityAttributes(agentName: "main", sessionKey: "main")
}
extension OpenClawActivityAttributes.ContentState {
static let connecting = OpenClawActivityAttributes.ContentState(
statusText: "Connecting...",
isIdle: false,
isDisconnected: false,
isConnecting: true,
startedAt: .now)
static let idle = OpenClawActivityAttributes.ContentState(
statusText: "Idle",
isIdle: true,
isDisconnected: false,
isConnecting: false,
startedAt: .now)
static let disconnected = OpenClawActivityAttributes.ContentState(
statusText: "Disconnected",
isIdle: false,
isDisconnected: true,
isConnecting: false,
startedAt: .now)
static let attention = OpenClawActivityAttributes.ContentState(
statusText: "Approval needed",
isIdle: false,
isDisconnected: false,
isConnecting: false,
startedAt: .now)
}
#endif

View File

@@ -12,8 +12,6 @@ final class LocationService: NSObject, CLLocationManagerDelegate, LocationServic
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
@@ -84,42 +82,6 @@ final class LocationService: NSObject, CLLocationManagerDelegate, LocationServic
try await AsyncTimeout.withTimeoutMs(timeoutMs: timeoutMs, onTimeout: { Error.timeout }, operation: operation)
}
func startLocationUpdates(
desiredAccuracy: OpenClawLocationAccuracy,
significantChangesOnly: Bool) -> AsyncStream<CLLocation>
{
self.stopLocationUpdates()
self.manager.desiredAccuracy = LocationCurrentRequest.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 }
@@ -127,13 +89,6 @@ final class LocationService: NSObject, CLLocationManagerDelegate, LocationServic
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
@@ -161,9 +116,6 @@ final class LocationService: NSObject, CLLocationManagerDelegate, LocationServic
if let callback = self.significantLocationCallback, let latest = locs.last {
callback(latest)
}
if let latest = locs.last, let updates = self.updatesContinuation {
updates.yield(latest)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -44,3 +44,160 @@ final class WatchReplyCoordinator {
self.queuedReplies.count
}
}
@MainActor
final class WatchChatCoordinator {
enum Decision {
case dropMissingFields
case dropMissingTarget
case deduped(commandId: String)
case queue(commandId: String)
case forward
}
private static let persistedQueueKey = "watch.chat.command.queue.v1"
private static let maxRecentCommandIds = 128
private struct QueuedCommand: Codable, Equatable {
var gatewayStableID: String
var event: WatchAppCommandEvent
}
private let defaults: UserDefaults
private var queuedCommands: [QueuedCommand] = []
private var recentCommandIds: [String] = []
private var seenCommandIds = Set<String>()
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
self.restoreQueue()
}
func ingest(
_ event: WatchAppCommandEvent,
isChatAvailable: Bool,
gatewayStableID: String?) -> Decision
{
let commandId = event.commandId.trimmingCharacters(in: .whitespacesAndNewlines)
let text = event.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if commandId.isEmpty || text.isEmpty {
return .dropMissingFields
}
if self.seenCommandIds.contains(commandId) {
return .deduped(commandId: commandId)
}
self.rememberRecentCommandId(commandId)
if !isChatAvailable {
let owner = gatewayStableID?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !owner.isEmpty else { return .dropMissingTarget }
self.queuedCommands.append(
QueuedCommand(gatewayStableID: owner, event: self.command(event, taggedFor: owner)))
self.rebuildSeenCommandIds()
self.persistQueue()
return .queue(commandId: commandId)
}
return .forward
}
func nextQueuedCommand(isChatAvailable: Bool, gatewayStableID: String?) -> WatchAppCommandEvent? {
let owner = gatewayStableID?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard isChatAvailable, !owner.isEmpty else { return nil }
return self.queuedCommands.first { $0.gatewayStableID == owner }?.event
}
func removeQueuedCommand(commandId: String, gatewayStableID: String?) {
let commandId = commandId.trimmingCharacters(in: .whitespacesAndNewlines)
let owner = gatewayStableID?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !commandId.isEmpty, !owner.isEmpty else { return }
guard let index = self.queuedCommands.firstIndex(where: {
$0.gatewayStableID == owner && $0.event.commandId == commandId
}) else { return }
self.queuedCommands.remove(at: index)
self.rememberRecentCommandId(commandId)
self.persistQueue()
}
func requeueFront(_ event: WatchAppCommandEvent, gatewayStableID: String?) {
let commandId = event.commandId.trimmingCharacters(in: .whitespacesAndNewlines)
let owner = gatewayStableID?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !owner.isEmpty else { return }
if !commandId.isEmpty {
self.rememberRecentCommandId(commandId)
self.queuedCommands.removeAll { $0.event.commandId == commandId }
}
self.queuedCommands.insert(
QueuedCommand(gatewayStableID: owner, event: self.command(event, taggedFor: owner)),
at: 0)
self.rebuildSeenCommandIds()
self.persistQueue()
}
var queuedCount: Int {
self.queuedCommands.count
}
var queuedCommandIds: [String] {
self.queuedCommands.map(\.event.commandId)
}
private func restoreQueue() {
guard let data = defaults.data(forKey: Self.persistedQueueKey),
let persisted = try? JSONDecoder().decode([QueuedCommand].self, from: data)
else {
return
}
var seen: [String] = []
var seenSet = Set<String>()
self.queuedCommands = persisted.compactMap { queued in
let owner = queued.gatewayStableID.trimmingCharacters(in: .whitespacesAndNewlines)
let commandId = queued.event.commandId.trimmingCharacters(in: .whitespacesAndNewlines)
let text = queued.event.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !owner.isEmpty, !commandId.isEmpty, !text.isEmpty, seenSet.insert(commandId).inserted else {
return nil
}
seen.append(commandId)
return QueuedCommand(gatewayStableID: owner, event: self.command(queued.event, taggedFor: owner))
}
self.recentCommandIds = Array(seen.suffix(Self.maxRecentCommandIds))
self.rebuildSeenCommandIds()
if self.queuedCommands.count != persisted.count {
self.persistQueue()
}
}
private func rememberRecentCommandId(_ commandId: String) {
guard !commandId.isEmpty else { return }
self.recentCommandIds.removeAll { $0 == commandId }
self.recentCommandIds.append(commandId)
if self.recentCommandIds.count > Self.maxRecentCommandIds {
self.recentCommandIds.removeFirst(self.recentCommandIds.count - Self.maxRecentCommandIds)
}
self.rebuildSeenCommandIds()
}
private func rebuildSeenCommandIds() {
var ids = Set(self.recentCommandIds)
ids.formUnion(self.queuedCommands.map(\.event.commandId))
self.seenCommandIds = ids
}
private func persistQueue() {
if self.queuedCommands.isEmpty {
self.defaults.removeObject(forKey: Self.persistedQueueKey)
return
}
guard let data = try? JSONEncoder().encode(queuedCommands) else { return }
self.defaults.set(data, forKey: Self.persistedQueueKey)
}
private func command(_ event: WatchAppCommandEvent, taggedFor gatewayStableID: String) -> WatchAppCommandEvent {
var tagged = event
tagged.gatewayStableID = gatewayStableID
return tagged
}
static func resetPersistedQueue(defaults: UserDefaults = .standard) {
defaults.removeObject(forKey: self.persistedQueueKey)
}
}

View File

@@ -1,365 +0,0 @@
import Foundation
import OpenClawKit
import SwiftUI
struct GatewayOnboardingView: View {
var body: some View {
NavigationStack {
List {
Section {
Text("Connect to your gateway to get started.")
.foregroundStyle(.secondary)
}
Section {
NavigationLink("Auto detect") {
AutoDetectStep()
}
NavigationLink("Manual entry") {
ManualEntryStep()
}
}
}
.navigationTitle("Connect Gateway")
}
.gatewayTrustPromptAlert()
}
}
private struct AutoDetectStep: View {
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
@Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController
@AppStorage("gateway.preferredStableID") private var preferredGatewayStableID: String = ""
@AppStorage("gateway.lastDiscoveredStableID") private var lastDiscoveredGatewayStableID: String = ""
@State private var connectingGatewayID: String?
@State private var connectStatusText: String?
var body: some View {
Form {
Section {
Text("Well scan for gateways on your network and connect automatically when we find one.")
.foregroundStyle(.secondary)
}
gatewayConnectionStatusSection(
appModel: self.appModel,
gatewayController: self.gatewayController,
secondaryLine: self.connectStatusText)
Section {
Button("Retry") {
resetGatewayConnectionState(
appModel: self.appModel,
connectStatusText: &self.connectStatusText,
connectingGatewayID: &self.connectingGatewayID)
self.triggerAutoConnect()
}
.disabled(self.connectingGatewayID != nil)
}
}
.navigationTitle("Auto detect")
.onAppear { self.triggerAutoConnect() }
.onChange(of: self.gatewayController.gateways) { _, _ in
self.triggerAutoConnect()
}
}
private func triggerAutoConnect() {
guard self.appModel.gatewayServerName == nil else { return }
guard self.connectingGatewayID == nil else { return }
guard let candidate = self.autoCandidate() else { return }
self.connectingGatewayID = candidate.id
Task {
defer { self.connectingGatewayID = nil }
await self.gatewayController.connect(candidate)
}
}
private func autoCandidate() -> GatewayDiscoveryModel.DiscoveredGateway? {
let preferred = self.preferredGatewayStableID.trimmingCharacters(in: .whitespacesAndNewlines)
let lastDiscovered = self.lastDiscoveredGatewayStableID.trimmingCharacters(in: .whitespacesAndNewlines)
if !preferred.isEmpty,
let match = self.gatewayController.gateways.first(where: { $0.stableID == preferred })
{
return match
}
if !lastDiscovered.isEmpty,
let match = self.gatewayController.gateways.first(where: { $0.stableID == lastDiscovered })
{
return match
}
if self.gatewayController.gateways.count == 1 {
return self.gatewayController.gateways.first
}
return nil
}
}
private struct ManualEntryStep: View {
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
@Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController
@State private var setupCode: String = ""
@State private var setupStatusText: String?
@State private var manualHost: String = ""
@State private var manualPortText: String = ""
@State private var manualUseTLS: Bool = true
@State private var manualToken: String = ""
@State private var manualPassword: String = ""
@State private var pendingManualAuthOverride: GatewayConnectionController.ManualAuthOverride?
@State private var connectingGatewayID: String?
@State private var connectStatusText: String?
var body: some View {
Form {
Section("Setup code") {
Text("Use /pair in your bot to get a setup code.")
.font(.footnote)
.foregroundStyle(.secondary)
TextField("Paste setup code", text: self.$setupCode)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
Button("Apply setup code") {
self.applySetupCode()
}
.disabled(self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
if let setupStatusText, !setupStatusText.isEmpty {
Text(setupStatusText)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
Section {
TextField("Host", text: self.$manualHost)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
TextField("Port", text: self.$manualPortText)
.keyboardType(.numberPad)
Toggle("Use TLS", isOn: self.$manualUseTLS)
TextField("Gateway token", text: self.$manualToken)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
SecureField("Gateway password", text: self.$manualPassword)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
gatewayConnectionStatusSection(
appModel: self.appModel,
gatewayController: self.gatewayController,
secondaryLine: self.connectStatusText)
Section {
Button {
Task { await self.connectManual() }
} label: {
if self.connectingGatewayID == "manual" {
HStack(spacing: 8) {
ProgressView()
.progressViewStyle(.circular)
Text("Connecting…")
}
} else {
Text("Connect")
}
}
.disabled(self.connectingGatewayID != nil)
Button("Retry") {
resetGatewayConnectionState(
appModel: self.appModel,
connectStatusText: &self.connectStatusText,
connectingGatewayID: &self.connectingGatewayID)
self.resetManualForm()
}
.disabled(self.connectingGatewayID != nil)
}
}
.navigationTitle("Manual entry")
}
private func connectManual() async {
let host = self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines)
guard !host.isEmpty else {
self.connectStatusText = "Failed: host required"
return
}
if let port = self.manualPortValue(), !(1...65535).contains(port) {
self.connectStatusText = "Failed: invalid port"
return
}
let defaults = UserDefaults.standard
defaults.set(true, forKey: "gateway.manual.enabled")
defaults.set(host, forKey: "gateway.manual.host")
defaults.set(self.manualPortValue() ?? 0, forKey: "gateway.manual.port")
defaults.set(self.manualUseTLS, forKey: "gateway.manual.tls")
let instanceId = GatewaySettingsStore.currentInstanceID()
if !instanceId.isEmpty {
let trimmedToken = self.manualToken.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedPassword = self.manualPassword.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedToken.isEmpty {
GatewaySettingsStore.saveGatewayToken(trimmedToken, instanceId: instanceId)
}
GatewaySettingsStore.saveGatewayPassword(trimmedPassword, instanceId: instanceId)
}
self.connectingGatewayID = "manual"
defer { self.connectingGatewayID = nil }
let authOverride = GatewayConnectionController.ManualAuthOverride.currentManualInput(
token: self.manualToken,
pendingOverride: self.pendingManualAuthOverride,
password: self.manualPassword)
self.pendingManualAuthOverride = nil
await self.gatewayController.connectManual(
host: host,
port: self.manualPortValue() ?? 0,
useTLS: self.manualUseTLS,
authOverride: authOverride)
}
private func manualPortValue() -> Int? {
let trimmed = self.manualPortText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
return Int(trimmed.filter(\.isNumber))
}
private func resetManualForm() {
self.setupCode = ""
self.setupStatusText = nil
self.manualHost = ""
self.manualPortText = ""
self.manualUseTLS = true
self.manualToken = ""
self.manualPassword = ""
}
private func applySetupCode() {
let raw = self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines)
guard !raw.isEmpty else {
self.setupStatusText = "Paste a setup code to continue."
return
}
if AppleReviewDemoMode.isSetupCode(raw) {
self.setupCode = ""
self.setupStatusText = "Apple Review demo mode enabled."
self.appModel.enterAppleReviewDemoMode()
return
}
guard let link = GatewayConnectDeepLink.fromSetupInput(raw) else {
self.setupStatusText = "Setup code not recognized or uses an insecure ws:// gateway URL."
return
}
self.manualHost = link.host
self.manualPortText = String(link.port)
self.manualUseTLS = link.tls
let setupAuth = GatewayConnectionController.ManualAuthOverride.setupAuth(from: link)
if setupAuth.shouldApplyTokenField {
self.manualToken = setupAuth.token
}
if setupAuth.shouldApplyPasswordField {
self.manualPassword = setupAuth.password
}
let trimmedInstanceId = GatewaySettingsStore.currentInstanceID()
if !trimmedInstanceId.isEmpty {
if setupAuth.hasBootstrapToken {
GatewayOnboardingReset.prepareForBootstrapPairing(
appModel: self.appModel,
instanceId: trimmedInstanceId)
}
GatewaySettingsStore.saveGatewayBootstrapToken(setupAuth.bootstrapToken, instanceId: trimmedInstanceId)
}
self.pendingManualAuthOverride = setupAuth.manualAuthOverride
self.setupStatusText = "Setup code applied."
}
}
@MainActor
private func gatewayConnectionStatusLines(
appModel: NodeAppModel,
gatewayController: GatewayConnectionController) -> [String]
{
ConnectionStatusBox.defaultLines(appModel: appModel, gatewayController: gatewayController)
}
@MainActor
private func resetGatewayConnectionState(
appModel: NodeAppModel,
connectStatusText: inout String?,
connectingGatewayID: inout String?)
{
appModel.disconnectGateway()
connectStatusText = nil
connectingGatewayID = nil
}
@MainActor
private func gatewayConnectionStatusSection(
appModel: NodeAppModel,
gatewayController: GatewayConnectionController,
secondaryLine: String?) -> some View
{
Section("Connection status") {
ConnectionStatusBox(
statusLines: gatewayConnectionStatusLines(
appModel: appModel,
gatewayController: gatewayController),
secondaryLine: secondaryLine)
}
}
private struct ConnectionStatusBox: View {
let statusLines: [String]
let secondaryLine: String?
var body: some View {
VStack(alignment: .leading, spacing: 6) {
ForEach(self.statusLines, id: \.self) { line in
Text(line)
.font(.system(size: 12, weight: .regular, design: .monospaced))
.foregroundStyle(.secondary)
}
if let secondaryLine, !secondaryLine.isEmpty {
Text(secondaryLine)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10, style: .continuous))
}
static func defaultLines(
appModel: NodeAppModel,
gatewayController: GatewayConnectionController) -> [String]
{
var lines: [String] = [
"gateway: \(appModel.gatewayDisplayStatusText)",
"discovery: \(gatewayController.discoveryStatusText)",
]
lines.append("server: \(appModel.gatewayServerName ?? "")")
lines.append("address: \(appModel.gatewayRemoteAddress ?? "")")
return lines
}
}

View File

@@ -53,10 +53,6 @@ enum OnboardingStateStore {
defaults.set(true, forKey: self.firstRunIntroSeenDefaultsKey)
}
static func markIncomplete(defaults: UserDefaults = .standard) {
defaults.set(false, forKey: self.completedDefaultsKey)
}
static func reset(defaults: UserDefaults = .standard) {
defaults.set(false, forKey: self.completedDefaultsKey)
defaults.set(false, forKey: self.firstRunIntroSeenDefaultsKey)

View File

@@ -17,10 +17,6 @@ private enum OnboardingStep: Int, CaseIterable {
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]

View File

@@ -3,7 +3,6 @@
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<string>$(OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT)</string>
</dict>
</plist>

View File

@@ -22,9 +22,22 @@ enum OpenClawAppModelRegistry {
@MainActor
final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrency UNUserNotificationCenterDelegate {
private let logger = Logger(subsystem: "ai.openclaw.ios", category: "Push")
private let backgroundWakeLogger = Logger(subsystem: "ai.openclaw.ios", category: "BackgroundWake")
private static let wakeRefreshTaskIdentifier = "ai.openclaw.ios.bgrefresh"
private let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "Push")
private let backgroundWakeLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "BackgroundWake")
private static var wakeRefreshTaskIdentifier: String {
"\(appBundleIdentifier).bgrefresh"
}
private static var appBundleIdentifier: String {
guard let bundleId = Bundle.main.bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines),
!bundleId.isEmpty
else {
return "ai.openclawfoundation.app"
}
return bundleId
}
private var backgroundWakeTask: Task<Bool, Never>?
private var pendingAPNsDeviceToken: Data?
private var pendingWatchPromptActions: [PendingWatchPromptAction] = []
@@ -92,6 +105,10 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
func _test_resolvedAppModel() -> NodeAppModel? {
self.resolvedAppModel()
}
func _test_wakeRefreshTaskIdentifier() -> String {
Self.wakeRefreshTaskIdentifier
}
#endif
func application(
@@ -611,9 +628,21 @@ struct OpenClawApp: App {
Self.installUncaughtExceptionLogger()
GatewaySettingsStore.bootstrapPersistence()
let appModel = NodeAppModel()
#if DEBUG
if Self.screenshotModeEnabled {
UIView.setAnimationsEnabled(false)
UserDefaults.standard.set(true, forKey: "gateway.onboardingComplete")
UserDefaults.standard.set(true, forKey: "gateway.hasConnectedOnce")
UserDefaults.standard.set(true, forKey: "onboarding.quickSetupDismissed")
appModel.enterScreenshotFixtureMode()
}
#endif
OpenClawAppModelRegistry.appModel = appModel
_appModel = State(initialValue: appModel)
_gatewayController = State(initialValue: GatewayConnectionController(appModel: appModel))
_gatewayController = State(
initialValue: GatewayConnectionController(
appModel: appModel,
startDiscovery: !Self.screenshotModeEnabled))
}
var body: some Scene {
@@ -649,6 +678,14 @@ struct OpenClawApp: App {
?? .system
}
private static var screenshotModeEnabled: Bool {
#if DEBUG
ProcessInfo.processInfo.arguments.contains("--openclaw-screenshot-mode")
#else
false
#endif
}
@MainActor
private func applyAppearancePreference() {
let style = self.appearancePreference.userInterfaceStyle

View File

@@ -39,10 +39,6 @@ struct PushBuildConfig {
self.relayBaseURL = Self.readURL(bundle: bundle, key: "OpenClawPushRelayBaseURL")
}
var usesRelay: Bool {
self.transport == .relay
}
private static func readURL(bundle: Bundle, key: String) -> URL? {
guard let raw = bundle.object(forInfoDictionaryKey: key) as? String else { return nil }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)

View File

@@ -13,7 +13,7 @@ private struct StoredPushRelayRegistrationState: Codable {
}
enum PushRelayRegistrationStore {
private static let service = "ai.openclaw.pushrelay"
private static let service = "ai.openclawfoundation.app.pushrelay"
private static let registrationStateAccount = "registration-state"
private static let appAttestKeyIDAccount = "app-attest-key-id"
private static let appAttestedKeyIDAccount = "app-attested-key-id"
@@ -71,11 +71,6 @@ enum PushRelayRegistrationStore {
return KeychainStore.saveString(raw, service: self.service, account: self.registrationStateAccount)
}
@discardableResult
static func clearRegistrationState() -> Bool {
KeychainStore.delete(service: self.service, account: self.registrationStateAccount)
}
static func loadAppAttestKeyID() -> String? {
let value = KeychainStore.loadString(service: self.service, account: self.appAttestKeyIDAccount)?
.trimmingCharacters(in: .whitespacesAndNewlines)

View File

@@ -7,7 +7,6 @@ extension RootTabs {
980
}
static let sidebarSplitMinimumWidth: CGFloat = 292
static let sidebarSplitIdealWidth: CGFloat = 316
static let sidebarSplitMaximumWidth: CGFloat = 340
static let sidebarDrawerMaximumWidth: CGFloat = 340

View File

@@ -1,15 +0,0 @@
import SwiftUI
struct RootView: View {
@AppStorage(AppAppearancePreference.storageKey) private var appearancePreferenceRaw: String =
AppAppearancePreference.system.rawValue
var body: some View {
RootTabs()
.preferredColorScheme(self.appearancePreference.colorScheme)
}
private var appearancePreference: AppAppearancePreference {
AppAppearancePreference(rawValue: self.appearancePreferenceRaw) ?? .system
}
}

View File

@@ -181,16 +181,6 @@ final class ScreenController {
return try await WebViewJavaScriptSupport.evaluateToString(webView: webView, javaScript: javaScript)
}
func snapshotPNGBase64(maxWidth: CGFloat? = nil) async throws -> String {
let image = try await self.snapshotImage(maxWidth: maxWidth)
guard let data = image.pngData() else {
throw NSError(domain: "Screen", code: 1, userInfo: [
NSLocalizedDescriptionKey: "snapshot encode failed",
])
}
return data.base64EncodedString()
}
func snapshotBase64(
maxWidth: CGFloat? = nil,
format: OpenClawCanvasSnapshotFormat,

View File

@@ -56,7 +56,7 @@ final class ScreenRecordService: @unchecked Sendable {
outPath: outPath)
let state = CaptureState()
let recordQueue = DispatchQueue(label: "ai.openclaw.screenrecord")
let recordQueue = DispatchQueue(label: "ai.openclawfoundation.app.screenrecord")
try await self.startCapture(state: state, config: config, recordQueue: recordQueue)
try await Task.sleep(nanoseconds: UInt64(config.durationMs) * 1_000_000)

View File

@@ -31,12 +31,7 @@ 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()
}
@MainActor
@@ -102,6 +97,22 @@ struct WatchExecApprovalSnapshotRequestEvent: Equatable {
var transport: String
}
struct WatchAppSnapshotRequestEvent: Equatable {
var requestId: String
var sentAtMs: Int?
var transport: String
}
struct WatchAppCommandEvent: Codable, Equatable {
var commandId: String
var command: OpenClawWatchAppCommand
var sessionKey: String?
var gatewayStableID: String?
var text: String?
var sentAtMs: Int?
var transport: String
}
struct WatchNotificationSendResult: Equatable {
var deliveredImmediately: Bool
var queuedForDelivery: Bool
@@ -115,6 +126,8 @@ protocol WatchMessagingServicing: AnyObject, Sendable {
func setExecApprovalResolveHandler(_ handler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?)
func setExecApprovalSnapshotRequestHandler(
_ handler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?)
func setAppSnapshotRequestHandler(_ handler: (@Sendable (WatchAppSnapshotRequestEvent) -> Void)?)
func setAppCommandHandler(_ handler: (@Sendable (WatchAppCommandEvent) -> Void)?)
func sendNotification(
id: String,
params: OpenClawWatchNotifyParams) async throws -> WatchNotificationSendResult
@@ -126,6 +139,8 @@ protocol WatchMessagingServicing: AnyObject, Sendable {
_ message: OpenClawWatchExecApprovalExpiredMessage) async throws -> WatchNotificationSendResult
func syncExecApprovalSnapshot(
_ message: OpenClawWatchExecApprovalSnapshotMessage) async throws -> WatchNotificationSendResult
func syncAppSnapshot(
_ message: OpenClawWatchAppSnapshotMessage) async throws -> WatchNotificationSendResult
}
extension CameraController: CameraServicing {}

View File

@@ -7,6 +7,8 @@ private struct WatchConnectivityTransportCallbacks {
var replyHandler: (@Sendable (WatchQuickReplyEvent) -> Void)?
var execApprovalResolveHandler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?
var execApprovalSnapshotRequestHandler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?
var appSnapshotRequestHandler: (@Sendable (WatchAppSnapshotRequestEvent) -> Void)?
var appCommandHandler: (@Sendable (WatchAppCommandEvent) -> Void)?
}
private func sendReachableWatchMessage(_ payload: [String: Any], with session: WCSession) async throws {
@@ -26,7 +28,7 @@ private func sendReachableWatchMessage(_ payload: [String: Any], with session: W
}
final class WatchConnectivityTransport: NSObject, @unchecked Sendable {
private nonisolated static let logger = Logger(subsystem: "ai.openclaw", category: "watch.messaging")
private nonisolated static let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "watch.messaging")
private let session: WCSession?
private let callbacksLock = NSLock()
@@ -96,6 +98,14 @@ final class WatchConnectivityTransport: NSObject, @unchecked Sendable {
self.updateCallbacks { $0.execApprovalSnapshotRequestHandler = handler }
}
func setAppSnapshotRequestHandler(_ handler: (@Sendable (WatchAppSnapshotRequestEvent) -> Void)?) {
self.updateCallbacks { $0.appSnapshotRequestHandler = handler }
}
func setAppCommandHandler(_ handler: (@Sendable (WatchAppCommandEvent) -> Void)?) {
self.updateCallbacks { $0.appCommandHandler = handler }
}
func sendPayload(_ payload: [String: Any]) async throws -> WatchNotificationSendResult {
await self.ensureActivated()
let session = try self.requireReadySession()
@@ -227,6 +237,24 @@ final class WatchConnectivityTransport: NSObject, @unchecked Sendable {
}
}
private func emitAppSnapshotRequest(_ event: WatchAppSnapshotRequestEvent) {
guard let handler = self.callbacksSnapshot().appSnapshotRequestHandler else {
return
}
Task { @MainActor in
handler(event)
}
}
private func emitAppCommand(_ event: WatchAppCommandEvent) {
guard let handler = self.callbacksSnapshot().appCommandHandler else {
return
}
Task { @MainActor in
handler(event)
}
}
private nonisolated static func status(for session: WCSession) -> WatchMessagingStatus {
WatchMessagingStatus(
supported: true,
@@ -296,6 +324,20 @@ extension WatchConnectivityTransport: WCSessionDelegate {
transport: "sendMessage")
{
self.emitExecApprovalSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppSnapshotRequestPayload(
message,
transport: "sendMessage")
{
self.emitAppSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppCommandPayload(
message,
transport: "sendMessage")
{
self.emitAppCommand(event)
}
}
@@ -327,6 +369,22 @@ extension WatchConnectivityTransport: WCSessionDelegate {
self.emitExecApprovalSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppSnapshotRequestPayload(
message,
transport: "sendMessage")
{
replyHandler(["ok": true])
self.emitAppSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppCommandPayload(
message,
transport: "sendMessage")
{
replyHandler(["ok": true])
self.emitAppCommand(event)
return
}
replyHandler(["ok": false, "error": "unsupported_payload"])
}
@@ -352,6 +410,20 @@ extension WatchConnectivityTransport: WCSessionDelegate {
transport: "transferUserInfo")
{
self.emitExecApprovalSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppSnapshotRequestPayload(
userInfo,
transport: "transferUserInfo")
{
self.emitAppSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppCommandPayload(
userInfo,
transport: "transferUserInfo")
{
self.emitAppCommand(event)
}
}

View File

@@ -151,6 +151,55 @@ enum WatchMessagingPayloadCodec {
return payload
}
static func encodeAppSnapshotPayload(
_ message: OpenClawWatchAppSnapshotMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.appSnapshot.rawValue,
"gatewayStatusText": message.gatewayStatusText,
"gatewayConnected": message.gatewayConnected,
"agentName": message.agentName,
"sessionKey": message.sessionKey,
"talkStatusText": message.talkStatusText,
"talkEnabled": message.talkEnabled,
"talkListening": message.talkListening,
"talkSpeaking": message.talkSpeaking,
"pendingApprovalCount": message.pendingApprovalCount,
]
if let agentAvatarURL = nonEmpty(message.agentAvatarURL) {
payload["agentAvatarUrl"] = agentAvatarURL
}
if let agentAvatarText = nonEmpty(message.agentAvatarText) {
payload["agentAvatarText"] = agentAvatarText
}
if let gatewayStableID = nonEmpty(message.gatewayStableID) {
payload["gatewayStableID"] = gatewayStableID
}
if let sentAtMs = message.sentAtMs {
payload["sentAtMs"] = sentAtMs
}
if let chatItems = message.chatItems {
payload["chatItems"] = chatItems.map { item in
var encoded: [String: Any] = [
"id": item.id,
"role": item.role,
"text": item.text,
]
if let timestampMs = item.timestampMs {
encoded["timestampMs"] = timestampMs
}
return encoded
}
}
if let chatStatusText = nonEmpty(message.chatStatusText) {
payload["chatStatusText"] = chatStatusText
}
if let snapshotId = nonEmpty(message.snapshotId) {
payload["snapshotId"] = snapshotId
}
return payload
}
static func parseQuickReplyPayload(
_ payload: [String: Any],
transport: String) -> WatchQuickReplyEvent?
@@ -216,4 +265,46 @@ enum WatchMessagingPayloadCodec {
sentAtMs: sentAtMs,
transport: transport)
}
static func parseAppSnapshotRequestPayload(
_ payload: [String: Any],
transport: String) -> WatchAppSnapshotRequestEvent?
{
guard (payload["type"] as? String) == OpenClawWatchPayloadType.appSnapshotRequest.rawValue else {
return nil
}
let requestId = self.nonEmpty(payload["requestId"] as? String) ?? UUID().uuidString
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchAppSnapshotRequestEvent(
requestId: requestId,
sentAtMs: sentAtMs,
transport: transport)
}
static func parseAppCommandPayload(
_ payload: [String: Any],
transport: String) -> WatchAppCommandEvent?
{
guard (payload["type"] as? String) == OpenClawWatchPayloadType.appCommand.rawValue else {
return nil
}
guard let rawCommand = nonEmpty(payload["command"] as? String),
let command = OpenClawWatchAppCommand(rawValue: rawCommand)
else {
return nil
}
let commandId = self.nonEmpty(payload["commandId"] as? String) ?? UUID().uuidString
let sessionKey = self.nonEmpty(payload["sessionKey"] as? String)
let gatewayStableID = self.nonEmpty(payload["gatewayStableID"] as? String)
let text = self.nonEmpty(payload["text"] as? String)
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchAppCommandEvent(
commandId: commandId,
command: command,
sessionKey: sessionKey,
gatewayStableID: gatewayStableID,
text: text,
sentAtMs: sentAtMs,
transport: transport)
}
}

View File

@@ -27,6 +27,8 @@ final class WatchMessagingService: @preconcurrency WatchMessagingServicing {
private var execApprovalResolveHandler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?
private var execApprovalSnapshotRequestHandler: (
@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?
private var appSnapshotRequestHandler: (@Sendable (WatchAppSnapshotRequestEvent) -> Void)?
private var appCommandHandler: (@Sendable (WatchAppCommandEvent) -> Void)?
init(transport: WatchConnectivityTransport = WatchConnectivityTransport()) {
self.transport = transport
@@ -50,6 +52,16 @@ final class WatchMessagingService: @preconcurrency WatchMessagingServicing {
self?.emitExecApprovalSnapshotRequest(event)
}
}
self.transport.setAppSnapshotRequestHandler { [weak self] event in
Task { @MainActor [weak self] in
self?.emitAppSnapshotRequest(event)
}
}
self.transport.setAppCommandHandler { [weak self] event in
Task { @MainActor [weak self] in
self?.emitAppCommand(event)
}
}
}
nonisolated static func isSupportedOnDevice() -> Bool {
@@ -95,6 +107,14 @@ final class WatchMessagingService: @preconcurrency WatchMessagingServicing {
self.execApprovalSnapshotRequestHandler = handler
}
func setAppSnapshotRequestHandler(_ handler: (@Sendable (WatchAppSnapshotRequestEvent) -> Void)?) {
self.appSnapshotRequestHandler = handler
}
func setAppCommandHandler(_ handler: (@Sendable (WatchAppCommandEvent) -> Void)?) {
self.appCommandHandler = handler
}
func sendNotification(
id: String,
params: OpenClawWatchNotifyParams) async throws -> WatchNotificationSendResult
@@ -131,6 +151,13 @@ final class WatchMessagingService: @preconcurrency WatchMessagingServicing {
WatchMessagingPayloadCodec.encodeExecApprovalSnapshotPayload(message))
}
func syncAppSnapshot(
_ message: OpenClawWatchAppSnapshotMessage) async throws -> WatchNotificationSendResult
{
try await self.transport.sendSnapshotPayload(
WatchMessagingPayloadCodec.encodeAppSnapshotPayload(message))
}
private func emitStatusIfChanged(_ snapshot: WatchMessagingStatus) {
guard snapshot != self.lastEmittedStatus else {
return
@@ -159,4 +186,20 @@ final class WatchMessagingService: @preconcurrency WatchMessagingServicing {
+ "sentAtMs=\(event.sentAtMs ?? -1)")
self.execApprovalSnapshotRequestHandler?(event)
}
private func emitAppSnapshotRequest(_ event: WatchAppSnapshotRequestEvent) {
GatewayDiagnostics.log(
"watch messaging: app snapshot request "
+ "id=\(event.requestId) transport=\(event.transport) "
+ "sentAtMs=\(event.sentAtMs ?? -1)")
self.appSnapshotRequestHandler?(event)
}
private func emitAppCommand(_ event: WatchAppCommandEvent) {
GatewayDiagnostics.log(
"watch messaging: app command "
+ "id=\(event.commandId) command=\(event.command.rawValue) "
+ "transport=\(event.transport)")
self.appCommandHandler?(event)
}
}

View File

@@ -22,11 +22,4 @@ enum SessionKey {
let agentId = String(parts[1]).trimmingCharacters(in: .whitespacesAndNewlines)
return agentId.isEmpty ? nil : agentId
}
static func isCanonicalMainSessionKey(_ value: String?) -> Bool {
let trimmed = (value ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { return false }
if trimmed == "global" { return true }
return trimmed.hasPrefix("agent:")
}
}

View File

@@ -121,13 +121,18 @@ struct PrivacyAccessSectionView: View {
switch self.contactsStatus {
case .notDetermined:
Task {
_ = await PermissionRequestBridge.awaitRequest { completion in
let granted = await PermissionRequestBridge.awaitRequest { completion in
let store = CNContactStore()
store.requestAccess(for: .contacts) { granted, _ in
completion(granted)
}
}
await MainActor.run { self.refreshAll() }
await MainActor.run {
self.refreshAll()
if granted {
self.contactsStatus = .authorized
}
}
}
case .denied, .restricted:
self.openSettings()
@@ -164,8 +169,13 @@ struct PrivacyAccessSectionView: View {
switch self.calendarStatus {
case .notDetermined:
Task {
_ = await self.requestCalendarWriteOnly()
await MainActor.run { self.refreshAll() }
let granted = await self.requestCalendarWriteOnly()
await MainActor.run {
self.refreshAll()
if granted {
self.calendarStatus = .writeOnly
}
}
}
case .denied, .restricted:
self.openSettings()
@@ -206,8 +216,13 @@ struct PrivacyAccessSectionView: View {
switch self.calendarStatus {
case .notDetermined, .writeOnly:
Task {
_ = await self.requestCalendarFull()
await MainActor.run { self.refreshAll() }
let granted = await self.requestCalendarFull()
await MainActor.run {
self.refreshAll()
if granted {
self.calendarStatus = .fullAccess
}
}
}
case .denied, .restricted:
self.openSettings()
@@ -248,8 +263,13 @@ struct PrivacyAccessSectionView: View {
switch self.remindersStatus {
case .notDetermined, .writeOnly:
Task {
_ = await self.requestRemindersFull()
await MainActor.run { self.refreshAll() }
let granted = await self.requestRemindersFull()
await MainActor.run {
self.refreshAll()
if granted {
self.remindersStatus = .fullAccess
}
}
}
case .denied, .restricted:
self.openSettings()

View File

@@ -1,40 +0,0 @@
import Foundation
struct SettingsHostPort: Equatable {
var host: String
var port: Int
}
enum SettingsNetworkingHelpers {
static func parseHostPort(from address: String) -> SettingsHostPort? {
let trimmed = address.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
if trimmed.hasPrefix("["),
let close = trimmed.firstIndex(of: "]"),
close < trimmed.endIndex
{
let host = String(trimmed[trimmed.index(after: trimmed.startIndex)..<close])
let portStart = trimmed.index(after: close)
guard portStart < trimmed.endIndex, trimmed[portStart] == ":" else { return nil }
let portString = String(trimmed[trimmed.index(after: portStart)...])
guard let port = Int(portString) else { return nil }
return SettingsHostPort(host: host, port: port)
}
guard let colon = trimmed.lastIndex(of: ":") else { return nil }
let host = String(trimmed[..<colon])
let portString = String(trimmed[trimmed.index(after: colon)...])
guard !host.isEmpty, let port = Int(portString) else { return nil }
return SettingsHostPort(host: host, port: port)
}
static func httpURLString(host: String?, port: Int?, fallback: String) -> String {
if let host, let port {
let needsBrackets = host.contains(":") && !host.hasPrefix("[") && !host.hasSuffix("]")
let hostPart = needsBrackets ? "[\(host)]" : host
return "http://\(hostPart):\(port)"
}
return "http://\(fallback)"
}
}

View File

@@ -121,7 +121,7 @@ final class RealtimeTalkRelaySession {
private let gateway: GatewayNodeSession
private let options: Options
private let pcmPlayer: PCMStreamingAudioPlaying
private let logger = Logger(subsystem: "ai.openclaw", category: "RealtimeTalkRelay")
private let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "RealtimeTalkRelay")
private let onStatus: (String) -> Void
private let onIssue: (TalkRuntimeIssue) -> Void
private let onSpeakingChanged: (Bool) -> Void

View File

@@ -3,7 +3,6 @@ import OpenClawKit
enum TalkModeExecutionMode {
case native
case realtimeClient
case realtimeRelay
}

View File

@@ -64,22 +64,6 @@ extension TalkModeManager {
}
}
static func permissionMessage(
kind: String,
status: AVAudioSession.RecordPermission) -> String
{
switch status {
case .denied:
return "\(kind) permission denied"
case .undetermined:
return "\(kind) permission not granted"
case .granted:
return "\(kind) permission denied"
@unknown default:
return "\(kind) permission denied"
}
}
static func permissionMessage(
kind: String,
status: SFSpeechRecognizerAuthorizationStatus) -> String

View File

@@ -70,11 +70,6 @@ final class TalkModeManager: NSObject {
self.gatewayConnected
}
var hasActiveAudioCapture: Bool {
self.isEnabled || self.isListening || self.isPushToTalkActive || self.realtimeRelaySession != nil
|| self.realtimeRelayStartInFlight
}
private enum CaptureMode {
case idle
case continuous
@@ -175,7 +170,7 @@ final class TalkModeManager: NSObject {
private var incrementalSpeechPrefetch: IncrementalSpeechPrefetchState?
private var incrementalSpeechPrefetchMonitorTask: Task<Void, Never>?
private let logger = Logger(subsystem: "ai.openclaw", category: "TalkMode")
private let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "TalkMode")
private static func nowSeconds() -> TimeInterval {
ProcessInfo.processInfo.systemUptime
@@ -225,6 +220,34 @@ final class TalkModeManager: NSObject {
}
}
func enterScreenshotFixtureMode() {
self.updateGatewayConnected(true)
self.isEnabled = false
self.isListening = false
self.isSpeaking = false
self.isUserSpeechDetected = false
self.statusText = "Ready"
self.gatewayTalkConfigLoaded = true
self.gatewayTalkApiKeyConfigured = true
self.gatewayTalkDefaultModelId = "gpt-realtime-2"
self.gatewayTalkDefaultVoiceId = "marin"
self.gatewayTalkProviderLabel = "OpenAI"
self.gatewayTalkTransportLabel = "Gateway Relay"
self.gatewayTalkUsesRealtime = true
self.gatewayTalkUsesRealtimeRelay = true
self.gatewayTalkRealtimeProviderLabel = "OpenAI"
self.gatewayTalkRealtimeModelId = "gpt-realtime-2"
self.gatewayTalkRealtimeVoiceId = "marin"
self.gatewayTalkVoiceModeTitle = "Realtime Voice"
self.gatewayTalkVoiceModeSubtitle = "Gateway relay ready"
self.gatewayTalkVoiceModeAccessibilityValue = "Realtime Voice, Gateway relay ready"
self.gatewayTalkActiveModeTitle = "Ready"
self.gatewayTalkActiveModeSubtitle = "Listening starts from this phone"
self.gatewayTalkLastIssueText = nil
self.gatewayTalkCurrentFallbackIssue = nil
self.gatewayTalkPermissionState = .ready
}
func setEnabled(_ enabled: Bool) {
self.isEnabled = enabled
if enabled {
@@ -475,13 +498,6 @@ final class TalkModeManager: NSObject {
return wasActive
}
func setForegroundAudioCaptureAllowed(_ allowed: Bool) {
self.foregroundAudioCaptureAllowed = allowed
if !allowed {
self.cancelPendingStart()
}
}
func resumeAfterBackground(wasSuspended: Bool, wasKeptActive: Bool = false) async {
if wasKeptActive { return }
guard wasSuspended else { return }
@@ -489,14 +505,6 @@ final class TalkModeManager: NSObject {
await self.start()
}
func userTappedOrb() {
if let realtimeSession {
realtimeSession.cancelResponse()
}
self.realtimeRelaySession?.cancelOutput()
self.stopSpeaking()
}
func beginPushToTalk() async throws -> OpenClawTalkPTTStartPayload {
guard self.gatewayConnected else {
self.statusText = "Offline"
@@ -3104,23 +3112,6 @@ extension TalkModeManager {
self.gatewayTalkCurrentFallbackIssue
}
func _test_seedTranscript(_ transcript: String) {
self.lastTranscript = transcript
self.lastHeard = Date()
}
func _test_handleTranscript(_ transcript: String, isFinal: Bool) async {
await self.handleTranscript(transcript: transcript, isFinal: isFinal)
}
func _test_backdateLastHeard(seconds: TimeInterval) {
self.lastHeard = Date().addingTimeInterval(-seconds)
}
func _test_runSilenceCheck() async {
await self.checkSilence()
}
func _test_incrementalReset() {
self.incrementalSpeechBuffer = IncrementalSpeechBuffer()
}

View File

@@ -3,7 +3,6 @@ import SwiftUI
struct TalkPermissionPromptView: View {
enum Style {
case card
case settings
case sheet
}

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