mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-11 00:11:53 +08:00
Compare commits
1264 Commits
worktree-s
...
feat/effec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91e48c8329 | ||
|
|
0f882e6fcf | ||
|
|
16a702a254 | ||
|
|
9fd5939edf | ||
|
|
00c4d06bc7 | ||
|
|
63c9b412bc | ||
|
|
03a5ab9907 | ||
|
|
07ee8f14bf | ||
|
|
414a042cfd | ||
|
|
47d34eeb9b | ||
|
|
740c68eda4 | ||
|
|
8d822214fa | ||
|
|
66a7a56414 | ||
|
|
ffb0f23c6e | ||
|
|
162fe50f1b | ||
|
|
489c0d7a28 | ||
|
|
c5b3352326 | ||
|
|
e57b137aef | ||
|
|
23f73b3ecf | ||
|
|
2fa853dce5 | ||
|
|
e975c3b212 | ||
|
|
df0d061c7a | ||
|
|
916234977a | ||
|
|
e650f8930d | ||
|
|
31bbe6eb0f | ||
|
|
f1f7525aa5 | ||
|
|
2be9abfb1c | ||
|
|
4b940f66fd | ||
|
|
41b4eb97f0 | ||
|
|
8b7d28354e | ||
|
|
2bcb0abbb8 | ||
|
|
f9d015346e | ||
|
|
2bdbf240a9 | ||
|
|
6920ec6c54 | ||
|
|
5d4166a368 | ||
|
|
306ca4d3eb | ||
|
|
3bc7d4061b | ||
|
|
ae42768e5f | ||
|
|
c98ccbe513 | ||
|
|
bd81a0323c | ||
|
|
bf7bd8dcd1 | ||
|
|
4159a11ea3 | ||
|
|
da8c11aaae | ||
|
|
dccf5f6842 | ||
|
|
8c9ec0724e | ||
|
|
9aec9200f1 | ||
|
|
d7d85d1eb6 | ||
|
|
0642b0033b | ||
|
|
7f46876a5d | ||
|
|
29951fdbb7 | ||
|
|
51f4a5e8a0 | ||
|
|
92492ecdd0 | ||
|
|
ba48d162af | ||
|
|
af8d2f2948 | ||
|
|
37ba583b05 | ||
|
|
05b3774c28 | ||
|
|
24e4dc68b7 | ||
|
|
2c9f284c1e | ||
|
|
80a563b4e4 | ||
|
|
b7d61c8daf | ||
|
|
c8bec51869 | ||
|
|
f436b4310a | ||
|
|
f78434985a | ||
|
|
33685e1474 | ||
|
|
abf78a0727 | ||
|
|
bc81d243ba | ||
|
|
c8c6df73a9 | ||
|
|
202dd7590d | ||
|
|
639107b7db | ||
|
|
872e068470 | ||
|
|
0eef4c49f6 | ||
|
|
0240cc578c | ||
|
|
80ca48418a | ||
|
|
90ae151154 | ||
|
|
372a8e4d22 | ||
|
|
9aa9c57625 | ||
|
|
c2c7396ffb | ||
|
|
bb07cbc2be | ||
|
|
72581fbb48 | ||
|
|
9c7e36bf81 | ||
|
|
aa6ea4510c | ||
|
|
445c7d050a | ||
|
|
f1c8570e0c | ||
|
|
069ad23b74 | ||
|
|
81d1424ad9 | ||
|
|
0d21524736 | ||
|
|
84c9286434 | ||
|
|
12d3ebe1e0 | ||
|
|
9f7d606469 | ||
|
|
b72f008183 | ||
|
|
37799a1437 | ||
|
|
d095319b08 | ||
|
|
dc6528f9ff | ||
|
|
111b65a6fb | ||
|
|
e01a885d18 | ||
|
|
b3ede77528 | ||
|
|
6c8f49386c | ||
|
|
cf7c46dff6 | ||
|
|
5d93fb3ed8 | ||
|
|
44a3301e50 | ||
|
|
3b663ad1c1 | ||
|
|
7206811b80 | ||
|
|
8bc927b294 | ||
|
|
6208f2b678 | ||
|
|
af2b313194 | ||
|
|
2754dc895a | ||
|
|
ae594d263c | ||
|
|
48b3a1cff9 | ||
|
|
1ea48edeff | ||
|
|
37f8507b4b | ||
|
|
4d2acd9481 | ||
|
|
5f85e6b692 | ||
|
|
89a9b4e75a | ||
|
|
56aec53dde | ||
|
|
e403af2d85 | ||
|
|
31cc26230f | ||
|
|
721fb6570e | ||
|
|
a70c90a52b | ||
|
|
3064d6181d | ||
|
|
783ef1d22b | ||
|
|
eb7a082b77 | ||
|
|
859286b95e | ||
|
|
b436927a43 | ||
|
|
46b0fc0c0a | ||
|
|
14142a927d | ||
|
|
7d96d1f41a | ||
|
|
c320da79ed | ||
|
|
c8cee2dce4 | ||
|
|
7ccd3b8e8e | ||
|
|
c4810e33f4 | ||
|
|
f553dad560 | ||
|
|
2ee856985b | ||
|
|
0e7cc1ca53 | ||
|
|
efabae2f9b | ||
|
|
a1e208ee26 | ||
|
|
9a1fa5f23f | ||
|
|
9c2044d3b7 | ||
|
|
73aab6abd8 | ||
|
|
1dac68c0bb | ||
|
|
b08e0da25b | ||
|
|
49e9382cc0 | ||
|
|
8d3b5c2d19 | ||
|
|
64b94daf92 | ||
|
|
ea16a5e9e1 | ||
|
|
6921d9072e | ||
|
|
0ad3d25fb7 | ||
|
|
b7e8f6da6a | ||
|
|
a87fcefe31 | ||
|
|
e22a7e45a4 | ||
|
|
d0218d3e59 | ||
|
|
25a8f5f3f8 | ||
|
|
a6dd9fdf08 | ||
|
|
cce12697a1 | ||
|
|
903e246e87 | ||
|
|
2e5a86adfe | ||
|
|
b9ffc0eda9 | ||
|
|
fa9c7ddadf | ||
|
|
ad29f089e4 | ||
|
|
f1351bcbcf | ||
|
|
ddcfde1489 | ||
|
|
4b11d65ada | ||
|
|
b5fde36c41 | ||
|
|
8c8241140e | ||
|
|
46409d30b7 | ||
|
|
ba8b4377f7 | ||
|
|
d4b98f0dc9 | ||
|
|
61f3a4d71d | ||
|
|
4db5cb1a20 | ||
|
|
7ed4ad8d17 | ||
|
|
6f7897d8ad | ||
|
|
fb0f29b9cc | ||
|
|
c2e659472a | ||
|
|
56cc150771 | ||
|
|
503c3d139c | ||
|
|
3de97057d0 | ||
|
|
445ed9b0b4 | ||
|
|
f0ceb3c5aa | ||
|
|
68e0cdc478 | ||
|
|
333f65fc8a | ||
|
|
0b7ff665f6 | ||
|
|
ba8a6499f0 | ||
|
|
ea4e3cd4fa | ||
|
|
628c753f3b | ||
|
|
638bfc0bf5 | ||
|
|
dd4613a268 | ||
|
|
9e9a825aa5 | ||
|
|
d2e0a8231f | ||
|
|
4add9bab77 | ||
|
|
8ba0bb2a8a | ||
|
|
947c9512c3 | ||
|
|
d5eec80e34 | ||
|
|
f964b1c3aa | ||
|
|
472523360d | ||
|
|
dea6207dd1 | ||
|
|
532759e1ab | ||
|
|
b376780715 | ||
|
|
ff6b38750e | ||
|
|
66f89540c2 | ||
|
|
a6aa48350b | ||
|
|
b21d3e5362 | ||
|
|
317f1f8b5b | ||
|
|
4e10969ade | ||
|
|
48b4e5b361 | ||
|
|
c3a211993c | ||
|
|
5ce1e45848 | ||
|
|
f3403708a3 | ||
|
|
7de413499f | ||
|
|
7270bb95b7 | ||
|
|
6ca9de1e0a | ||
|
|
fc9798a788 | ||
|
|
daef8e73fc | ||
|
|
c7dcf79585 | ||
|
|
1bcc071385 | ||
|
|
7715b29aa2 | ||
|
|
011f98c5a6 | ||
|
|
c746ec5fc7 | ||
|
|
dd457474b3 | ||
|
|
e51d27d2ac | ||
|
|
ae5591eca9 | ||
|
|
8ac30279b3 | ||
|
|
30e2654ac2 | ||
|
|
f1708f8b14 | ||
|
|
69eb76b9bb | ||
|
|
4859edd9f8 | ||
|
|
55c275b00a | ||
|
|
3e5070efbf | ||
|
|
6330fe607d | ||
|
|
e79e5dbbdf | ||
|
|
1ba75463a5 | ||
|
|
4e6c85d930 | ||
|
|
c96795d272 | ||
|
|
8338412b54 | ||
|
|
f3f2c784c4 | ||
|
|
ac2e72a8e6 | ||
|
|
d5b87672f8 | ||
|
|
cf79689ca1 | ||
|
|
65dd71d42d | ||
|
|
c6ddb1afb7 | ||
|
|
29b5563ccd | ||
|
|
9c38948700 | ||
|
|
9ee93e8ea7 | ||
|
|
4718e6272c | ||
|
|
2204b25f1d | ||
|
|
5aefc9dda4 | ||
|
|
1b62168a3a | ||
|
|
06ec35452f | ||
|
|
c5a4d7af41 | ||
|
|
25864ee540 | ||
|
|
127156a88a | ||
|
|
bbf50a406e | ||
|
|
2eee70e0a6 | ||
|
|
369917ff79 | ||
|
|
9393be2e4b | ||
|
|
4780e69352 | ||
|
|
99dfb8291f | ||
|
|
abbfd276ee | ||
|
|
d24a847279 | ||
|
|
76ec830c7a | ||
|
|
80b5a0138f | ||
|
|
40789da1ef | ||
|
|
6472b05fad | ||
|
|
bfc674876d | ||
|
|
d8ae3ec4c8 | ||
|
|
492f59e586 | ||
|
|
41810a462e | ||
|
|
1b87ba8ca5 | ||
|
|
031655b933 | ||
|
|
934fc6ceeb | ||
|
|
d61051558b | ||
|
|
adac07f1d8 | ||
|
|
c91e20ac0c | ||
|
|
2d91a3b200 | ||
|
|
1e31bd2ac2 | ||
|
|
111a17b6e8 | ||
|
|
d91f58ee25 | ||
|
|
f74436dc81 | ||
|
|
8cc1aee9d8 | ||
|
|
bc461965a3 | ||
|
|
38e53a89b2 | ||
|
|
5779d485fd | ||
|
|
6de8563827 | ||
|
|
56303b96d0 | ||
|
|
2b7c150de3 | ||
|
|
b67bcd93cc | ||
|
|
e0f7dafcea | ||
|
|
2ea0c6c929 | ||
|
|
cb695b0986 | ||
|
|
b4f6cb29b8 | ||
|
|
d89732efca | ||
|
|
cda4689d71 | ||
|
|
391b4916dc | ||
|
|
a541aa3b0b | ||
|
|
2a02d83e2e | ||
|
|
8859e89e07 | ||
|
|
2d8339529b | ||
|
|
aeb06bf4ef | ||
|
|
795ad845d6 | ||
|
|
d00cd314d1 | ||
|
|
7958eb0fc4 | ||
|
|
f97645428e | ||
|
|
e3ad37c24f | ||
|
|
0dc34ca171 | ||
|
|
d4c83edba8 | ||
|
|
e6dc6c52fe | ||
|
|
215e43aa94 | ||
|
|
e360e105a3 | ||
|
|
f06e9f6358 | ||
|
|
b3d9bef38d | ||
|
|
b6809b5e31 | ||
|
|
8ff722fe7d | ||
|
|
3bedce151e | ||
|
|
373f709130 | ||
|
|
a383baac03 | ||
|
|
93b9223bee | ||
|
|
4aa37c3261 | ||
|
|
db3c4ba8d3 | ||
|
|
7c639d4b46 | ||
|
|
fb7dc43043 | ||
|
|
386fbd6594 | ||
|
|
e4a1032072 | ||
|
|
622728757f | ||
|
|
b2d04646c1 | ||
|
|
f04d20f8f9 | ||
|
|
c0fe7ab34a | ||
|
|
2ac011b8ae | ||
|
|
c8d53fdf1b | ||
|
|
b2dfa98877 | ||
|
|
3537d8a613 | ||
|
|
8bfb943945 | ||
|
|
a1a6cd6508 | ||
|
|
f686bb519f | ||
|
|
f1b92c8885 | ||
|
|
239def7838 | ||
|
|
cd91bd9a1e | ||
|
|
764cfd5552 | ||
|
|
dfeaf6f7cf | ||
|
|
cd9b2c0af4 | ||
|
|
0e5f4ea18c | ||
|
|
b04e42812e | ||
|
|
21b6dcbe37 | ||
|
|
44840007d4 | ||
|
|
778ad09ff2 | ||
|
|
582f834269 | ||
|
|
fc5349688f | ||
|
|
3b1497789c | ||
|
|
b9fbc57bbd | ||
|
|
238b0fc76f | ||
|
|
e30be460e1 | ||
|
|
eb1e6099d2 | ||
|
|
d656087b31 | ||
|
|
df70ed2b9c | ||
|
|
63ad5b4f97 | ||
|
|
3fd4b02eb5 | ||
|
|
24e88bcdd1 | ||
|
|
8650f4ba19 | ||
|
|
3b2fb9e63d | ||
|
|
9bad29261d | ||
|
|
55322d7301 | ||
|
|
9d51d2a8b8 | ||
|
|
5121f30d2d | ||
|
|
28f59a9124 | ||
|
|
ec9d56601a | ||
|
|
b180b8ae48 | ||
|
|
a099acc557 | ||
|
|
06e79b2762 | ||
|
|
8503418274 | ||
|
|
be166b9ae4 | ||
|
|
930852af29 | ||
|
|
64d4f99d26 | ||
|
|
f4d90eb36a | ||
|
|
5f89cabeb5 | ||
|
|
66ba611f5b | ||
|
|
6623444f8d | ||
|
|
7acbb2260c | ||
|
|
0dc8552cb3 | ||
|
|
bea597b2d6 | ||
|
|
1458829822 | ||
|
|
396d5b26da | ||
|
|
e1d69bc433 | ||
|
|
b24a6d2cbd | ||
|
|
925861f9b3 | ||
|
|
aff30f9671 | ||
|
|
fa43ed33d8 | ||
|
|
7c39d0d60c | ||
|
|
c1eb1ba7cd | ||
|
|
8e515e5fa8 | ||
|
|
d6cb0e5adf | ||
|
|
e5a3e3b357 | ||
|
|
fb87c0b470 | ||
|
|
45c3faeb1e | ||
|
|
b521dae2ec | ||
|
|
3f0c875e4c | ||
|
|
9fdd7c56b9 | ||
|
|
3a0029a297 | ||
|
|
a1ee350cc9 | ||
|
|
3146470ee1 | ||
|
|
c3dc015cac | ||
|
|
599432b5a7 | ||
|
|
65aa6c8a77 | ||
|
|
2e2da1f2b9 | ||
|
|
1109abc947 | ||
|
|
91f01b7664 | ||
|
|
de48410384 | ||
|
|
0beae266da | ||
|
|
916fc3d09a | ||
|
|
7bc73ded71 | ||
|
|
9f211bb802 | ||
|
|
b830beb34b | ||
|
|
0cbd2d3b08 | ||
|
|
4a188e7ca5 | ||
|
|
2645492fde | ||
|
|
5734193fdf | ||
|
|
7289e14dae | ||
|
|
87724c964f | ||
|
|
59d2e7686c | ||
|
|
7e9a863423 | ||
|
|
c5ca8a17ce | ||
|
|
a543d21352 | ||
|
|
41992581b8 | ||
|
|
5f8200e6e8 | ||
|
|
cc129a0dd3 | ||
|
|
f1deb53d43 | ||
|
|
93c799eb16 | ||
|
|
942334f97d | ||
|
|
cdc9a380d1 | ||
|
|
a4cd482488 | ||
|
|
510628ca6f | ||
|
|
57e699a09a | ||
|
|
de18f77737 | ||
|
|
1713930bbe | ||
|
|
d471540ceb | ||
|
|
c462e68df7 | ||
|
|
4d28450312 | ||
|
|
aedcfa76a4 | ||
|
|
11984c7997 | ||
|
|
42f6d90917 | ||
|
|
1139777765 | ||
|
|
22a0ca9e3b | ||
|
|
7db44b979f | ||
|
|
b672be59ae | ||
|
|
7e7ce53e5a | ||
|
|
4505a88d88 | ||
|
|
dae90067e9 | ||
|
|
a9aafc84b1 | ||
|
|
b26dcb3390 | ||
|
|
36411cde8f | ||
|
|
e72c849359 | ||
|
|
0d22fbf312 | ||
|
|
f12e9c41fa | ||
|
|
0db0979365 | ||
|
|
204941c03e | ||
|
|
bf400c08c3 | ||
|
|
8985cd596a | ||
|
|
0844e771a8 | ||
|
|
346a773b18 | ||
|
|
2156b204ea | ||
|
|
695a4f5039 | ||
|
|
d0034e2f99 | ||
|
|
4ce979934f | ||
|
|
f0803c576f | ||
|
|
31bfe7f084 | ||
|
|
8659eabba7 | ||
|
|
9c88241a85 | ||
|
|
30cfcf21b7 | ||
|
|
60a6945a6e | ||
|
|
c98459d9da | ||
|
|
376a792100 | ||
|
|
ffae8f32d8 | ||
|
|
acbe461c16 | ||
|
|
70ebf4898b | ||
|
|
380ba7071f | ||
|
|
d16f79f49d | ||
|
|
a709927698 | ||
|
|
9f99464119 | ||
|
|
c589680e95 | ||
|
|
1d46a2f0b5 | ||
|
|
62375ae860 | ||
|
|
6e191f0e1e | ||
|
|
d77f428441 | ||
|
|
a118e114fe | ||
|
|
0179efc022 | ||
|
|
4199034de2 | ||
|
|
92f8c1edac | ||
|
|
eb07aba973 | ||
|
|
77e5a492f2 | ||
|
|
c59e900903 | ||
|
|
63d72e2191 | ||
|
|
d7e946eb34 | ||
|
|
eb4e20ca1d | ||
|
|
4004c9342d | ||
|
|
9cfb4c747a | ||
|
|
70ed7c9873 | ||
|
|
3f80f889fa | ||
|
|
bcbf4fc35f | ||
|
|
3f0a39510b | ||
|
|
bd0555d5fc | ||
|
|
f6c00456dc | ||
|
|
99a6b1c5a8 | ||
|
|
abf59205fc | ||
|
|
079bf99671 | ||
|
|
926bf66ee3 | ||
|
|
cca4f3c63c | ||
|
|
f8ae0fb1c4 | ||
|
|
f9112f0e0a | ||
|
|
ba61d12c45 | ||
|
|
82f4297311 | ||
|
|
1d8d664570 | ||
|
|
f6f05c4859 | ||
|
|
1a5548203e | ||
|
|
d9ff8cfb01 | ||
|
|
6971036043 | ||
|
|
21f1b46f8a | ||
|
|
86b0a7ddda | ||
|
|
817dca5ae9 | ||
|
|
686b93e5c7 | ||
|
|
e575325af6 | ||
|
|
c404711703 | ||
|
|
130c2d5044 | ||
|
|
f64feab47a | ||
|
|
386d321634 | ||
|
|
c9b6b0be0e | ||
|
|
59be6d6390 | ||
|
|
28550a798c | ||
|
|
d0f22ccf97 | ||
|
|
0de6f93805 | ||
|
|
85eb3cda65 | ||
|
|
52c9860bde | ||
|
|
3f132370f4 | ||
|
|
83d7ab0d36 | ||
|
|
1f45b37fe1 | ||
|
|
83b8289ee2 | ||
|
|
74dae6088b | ||
|
|
42b95a9eb1 | ||
|
|
e692f5c1cf | ||
|
|
2d6fd54ebd | ||
|
|
8813b79990 | ||
|
|
d9c6036a8f | ||
|
|
2d9ef76d5b | ||
|
|
1bdb151d0d | ||
|
|
1b77553115 | ||
|
|
1fc82347a7 | ||
|
|
ae0cb0ac6f | ||
|
|
b02de2e948 | ||
|
|
8f7a3cbff3 | ||
|
|
8754094292 | ||
|
|
c96181fdbe | ||
|
|
278e3eabf2 | ||
|
|
f3fecb7218 | ||
|
|
79bd0185f5 | ||
|
|
02f2e08493 | ||
|
|
f3875ac937 | ||
|
|
99d7b206fd | ||
|
|
49e288823c | ||
|
|
a1de61bf65 | ||
|
|
7a65b8a3d5 | ||
|
|
348ffe6061 | ||
|
|
095de07135 | ||
|
|
260e1e138b | ||
|
|
a2963f51d5 | ||
|
|
2b04cedfb1 | ||
|
|
d25bece9f6 | ||
|
|
724868cec8 | ||
|
|
6b6538bd13 | ||
|
|
dbabfc550f | ||
|
|
c822824503 | ||
|
|
fe97f1fa4f | ||
|
|
b9dc6d86b8 | ||
|
|
13c2e245aa | ||
|
|
f3f6a866ca | ||
|
|
6ae9c8bead | ||
|
|
8ba7927f6e | ||
|
|
84ec355af8 | ||
|
|
23ed804657 | ||
|
|
23cfc81bcd | ||
|
|
92524fcf98 | ||
|
|
a47132734b | ||
|
|
b405c6e640 | ||
|
|
c70adb8528 | ||
|
|
6b3998aa40 | ||
|
|
365c986a5b | ||
|
|
ac5674b32c | ||
|
|
731c8843ff | ||
|
|
554dfbd017 | ||
|
|
4feb4e6623 | ||
|
|
266722500c | ||
|
|
4e76d6e427 | ||
|
|
ee9d471865 | ||
|
|
d5abbd29cc | ||
|
|
a5c1956ca1 | ||
|
|
2268ce3a14 | ||
|
|
e28d66d531 | ||
|
|
41aee75cd1 | ||
|
|
04605f1670 | ||
|
|
a0f35574d0 | ||
|
|
66b98b7294 | ||
|
|
a582fc2d5c | ||
|
|
beea866a53 | ||
|
|
1d121c1f08 | ||
|
|
12b8db34ee | ||
|
|
fd244fd76d | ||
|
|
c35634c729 | ||
|
|
0668f1e003 | ||
|
|
94f3ecae9a | ||
|
|
641ad418c9 | ||
|
|
336ba2a2b3 | ||
|
|
8717525fbc | ||
|
|
2f2563314a | ||
|
|
b79c41d252 | ||
|
|
31a28eb5ba | ||
|
|
f71df80522 | ||
|
|
f6d0cc6cc3 | ||
|
|
db743e4918 | ||
|
|
462a056210 | ||
|
|
fe25ed214e | ||
|
|
3fa9658b39 | ||
|
|
c20224688c | ||
|
|
a012411d5e | ||
|
|
bcb9fa42be | ||
|
|
3bd47a95a8 | ||
|
|
e17f628a75 | ||
|
|
7edcfabf51 | ||
|
|
e7ae306aa1 | ||
|
|
04afd114bb | ||
|
|
c499ef1a6b | ||
|
|
f84d031d38 | ||
|
|
3048ad4731 | ||
|
|
ceb3092493 | ||
|
|
babd25c6b7 | ||
|
|
bfc798bd0b | ||
|
|
32f89760e3 | ||
|
|
11017c93cf | ||
|
|
643dea2455 | ||
|
|
6789fe248b | ||
|
|
10d2f41c83 | ||
|
|
b1fc55fded | ||
|
|
8962d35264 | ||
|
|
d41907a5cb | ||
|
|
b8dccbf310 | ||
|
|
949012797b | ||
|
|
1aef36b60d | ||
|
|
1e5641ba82 | ||
|
|
0916a19cb5 | ||
|
|
1d5f01500d | ||
|
|
625713091e | ||
|
|
d9e999cf4f | ||
|
|
81b239dc98 | ||
|
|
7f05ea60fa | ||
|
|
8ec9bfb31e | ||
|
|
4e1f59010e | ||
|
|
25eef1203a | ||
|
|
d656cda46d | ||
|
|
5479b6b32c | ||
|
|
36755e4057 | ||
|
|
6bd19bffd6 | ||
|
|
31de033590 | ||
|
|
e064cc98f0 | ||
|
|
0a42afae3a | ||
|
|
da1ccd3077 | ||
|
|
2ab08c8a19 | ||
|
|
c635f0087e | ||
|
|
3b7d01b63f | ||
|
|
8f612787a8 | ||
|
|
eefa6ecea0 | ||
|
|
9518d12e13 | ||
|
|
65ea6fdb49 | ||
|
|
af3d9333aa | ||
|
|
cd42df45d6 | ||
|
|
3485a907d1 | ||
|
|
6cac228a0c | ||
|
|
b73d01f13b | ||
|
|
bb8aa0cfe2 | ||
|
|
1fc92ddfb1 | ||
|
|
284dcc51b8 | ||
|
|
6cfb62d5aa | ||
|
|
bc0def52af | ||
|
|
ce63b9ca46 | ||
|
|
8da06d46f8 | ||
|
|
e44b915dbf | ||
|
|
df4aac8f96 | ||
|
|
c04bbd3cbb | ||
|
|
fe89243c3b | ||
|
|
6db2ee6583 | ||
|
|
ca7349b585 | ||
|
|
dd4c68b525 | ||
|
|
89987df3d5 | ||
|
|
b97b5d6f22 | ||
|
|
aa39107261 | ||
|
|
117d2c9f2e | ||
|
|
f5ebe63ecd | ||
|
|
2d231cef27 | ||
|
|
e4cee2eb69 | ||
|
|
5b418c3c4f | ||
|
|
c722ae6a65 | ||
|
|
5d4a8b0072 | ||
|
|
5496c0d5b7 | ||
|
|
1ee0d51e92 | ||
|
|
4935e24c7a | ||
|
|
aac216d699 | ||
|
|
3b8ac38ae9 | ||
|
|
78644bc6de | ||
|
|
3da9027770 | ||
|
|
256377c029 | ||
|
|
7c5222a195 | ||
|
|
78eb92e622 | ||
|
|
faa443a452 | ||
|
|
61ae9b7193 | ||
|
|
3fd64a281f | ||
|
|
c95ccf43c1 | ||
|
|
6a41a54212 | ||
|
|
983064f5f8 | ||
|
|
bce56bacc7 | ||
|
|
e774b25b2f | ||
|
|
3ce922437f | ||
|
|
97ed9b2d82 | ||
|
|
5923d9e807 | ||
|
|
a504cd0190 | ||
|
|
f3361dc928 | ||
|
|
52370c5998 | ||
|
|
0b55317494 | ||
|
|
3225ec43c8 | ||
|
|
23446a248b | ||
|
|
9431d18aaf | ||
|
|
74860e93fd | ||
|
|
8046b5e462 | ||
|
|
5214f16e29 | ||
|
|
b5c3379097 | ||
|
|
dc7fab4dc5 | ||
|
|
b10b946b12 | ||
|
|
72f50dd127 | ||
|
|
d08f68dee7 | ||
|
|
25dd30d656 | ||
|
|
c654f1f811 | ||
|
|
6395117142 | ||
|
|
26da4edbe1 | ||
|
|
c5071a8061 | ||
|
|
207fb9951d | ||
|
|
b8ea6097d9 | ||
|
|
4d2e708726 | ||
|
|
6602884b06 | ||
|
|
b85259c443 | ||
|
|
49adf206e8 | ||
|
|
d7d1fba74b | ||
|
|
cf571c1b58 | ||
|
|
cffae53b43 | ||
|
|
6a23e26a27 | ||
|
|
308b39efd5 | ||
|
|
f30c9eff76 | ||
|
|
7c4f607572 | ||
|
|
ebd829cffd | ||
|
|
8237d165e2 | ||
|
|
609187f5f6 | ||
|
|
b7d3b74f1c | ||
|
|
439e396262 | ||
|
|
1f59031373 | ||
|
|
8a406528b4 | ||
|
|
d4484158d9 | ||
|
|
b55d9fa466 | ||
|
|
433bafa55b | ||
|
|
af6f75f78c | ||
|
|
e9a9434842 | ||
|
|
58bfefbad3 | ||
|
|
c3e5d85ce1 | ||
|
|
ddd79e51ba | ||
|
|
652af36d17 | ||
|
|
2d3f3de235 | ||
|
|
1d6e5f7a3e | ||
|
|
58591c37a4 | ||
|
|
64ba5e2ae3 | ||
|
|
f441a569ea | ||
|
|
83549774cd | ||
|
|
48fb4bade8 | ||
|
|
ab719c2f82 | ||
|
|
d540512d00 | ||
|
|
babd48b6cd | ||
|
|
a6497b1759 | ||
|
|
150bebcd0c | ||
|
|
63724ddcfd | ||
|
|
10315ce215 | ||
|
|
2a67a7f65e | ||
|
|
0513b285ef | ||
|
|
418d7afb33 | ||
|
|
4d8aec8210 | ||
|
|
a40499b21a | ||
|
|
210c7c1b85 | ||
|
|
51cd3cddeb | ||
|
|
402b0df3b6 | ||
|
|
f4cb20300f | ||
|
|
6e5042cd62 | ||
|
|
18ca285ed6 | ||
|
|
1c28e4a0bb | ||
|
|
0b8ee4616d | ||
|
|
6160e7a411 | ||
|
|
53d007bc87 | ||
|
|
218156447c | ||
|
|
ab3d61813a | ||
|
|
23344fdb61 | ||
|
|
756379b11d | ||
|
|
c4c0b65b80 | ||
|
|
ec998d1e95 | ||
|
|
07c5e2465b | ||
|
|
743cbc2f13 | ||
|
|
66cce180c3 | ||
|
|
f3327ac30b | ||
|
|
2fe39ce949 | ||
|
|
fb3aa155be | ||
|
|
2e983e47df | ||
|
|
488a3d8e52 | ||
|
|
5ac6b600de | ||
|
|
4785a073d6 | ||
|
|
d00e9eba65 | ||
|
|
96c0309db9 | ||
|
|
714f62f976 | ||
|
|
3cef9a65d3 | ||
|
|
f1381b5312 | ||
|
|
b17e77a22b | ||
|
|
05bef5db20 | ||
|
|
3cf296185f | ||
|
|
70df2b8fe2 | ||
|
|
1c5c72ea24 | ||
|
|
3b7181a38b | ||
|
|
4e34c1aa47 | ||
|
|
f2cbe9ecc5 | ||
|
|
dfead3198d | ||
|
|
852f88f1e7 | ||
|
|
694ca50e97 | ||
|
|
3de5979bdc | ||
|
|
d1fdd6e186 | ||
|
|
f91de52f0d | ||
|
|
0a9f7afb66 | ||
|
|
c796b96d34 | ||
|
|
dd8aa1dcce | ||
|
|
ce31fc91e1 | ||
|
|
af42260440 | ||
|
|
96fba91b3a | ||
|
|
5f8e1dd399 | ||
|
|
b4d148e21a | ||
|
|
97df0bc55a | ||
|
|
ca4672458c | ||
|
|
0832343197 | ||
|
|
74a809a1cd | ||
|
|
1bc45af1db | ||
|
|
46f7750c63 | ||
|
|
68c77bb55d | ||
|
|
26c7da2d02 | ||
|
|
b7572cc384 | ||
|
|
65e37017b6 | ||
|
|
9f46e9cb85 | ||
|
|
bd326cdd5e | ||
|
|
8b0b4ea82f | ||
|
|
af021aac8d | ||
|
|
47ba73de27 | ||
|
|
86ee352138 | ||
|
|
ecdad948b5 | ||
|
|
9741bbe2c1 | ||
|
|
635f6e0d0e | ||
|
|
b150e23a60 | ||
|
|
2e3c49b161 | ||
|
|
f20054ba79 | ||
|
|
27e5d49fe5 | ||
|
|
1e8e004361 | ||
|
|
85f9276624 | ||
|
|
49ccd4e080 | ||
|
|
7c89fb455b | ||
|
|
09a490de17 | ||
|
|
a15559b8d0 | ||
|
|
392b23c01e | ||
|
|
2cae5e92a6 | ||
|
|
16c5d5b87b | ||
|
|
453019a2c7 | ||
|
|
fe19d9fea8 | ||
|
|
a661b7b04c | ||
|
|
db6bb6d329 | ||
|
|
c115b126d2 | ||
|
|
48c8aa11f8 | ||
|
|
aae173a1c9 | ||
|
|
62adbe0b80 | ||
|
|
80190249ec | ||
|
|
95901042d4 | ||
|
|
6c92324c5f | ||
|
|
060768ef75 | ||
|
|
a9215ef2d4 | ||
|
|
578d723b48 | ||
|
|
b1db684aca | ||
|
|
5904e2027f | ||
|
|
06b8cd4565 | ||
|
|
2e6d34d2c9 | ||
|
|
321c996af8 | ||
|
|
0fa30e790c | ||
|
|
0eeafbdce2 | ||
|
|
7c3e4dbf94 | ||
|
|
3a44d88d09 | ||
|
|
383ebe723b | ||
|
|
5bb2c5e454 | ||
|
|
f02b715f2b | ||
|
|
898a5aae21 | ||
|
|
c1700a5c9f | ||
|
|
ffb2dcc2e6 | ||
|
|
1d331bcfc5 | ||
|
|
6cd2059749 | ||
|
|
33655ee290 | ||
|
|
faaa7efef0 | ||
|
|
5ef9207813 | ||
|
|
c8ddf71989 | ||
|
|
6c2e0bdc0c | ||
|
|
06e04291e0 | ||
|
|
b693173e0d | ||
|
|
f7e9d80536 | ||
|
|
a7ae889ae5 | ||
|
|
a6e4132a8c | ||
|
|
b3ef14dbfc | ||
|
|
5ca95b2012 | ||
|
|
ba17ddaef3 | ||
|
|
99f81f10e3 | ||
|
|
9ac275a8bf | ||
|
|
1dc678393f | ||
|
|
4e1f92641c | ||
|
|
9d13203d15 | ||
|
|
6648950862 | ||
|
|
0f7b1c5414 | ||
|
|
6ad1c7f3b7 | ||
|
|
95a105334b | ||
|
|
25cda2b24c | ||
|
|
94aae40c28 | ||
|
|
64ff396593 | ||
|
|
bd4db5ee62 | ||
|
|
b9b7ffc8cd | ||
|
|
f2bd20351f | ||
|
|
abd45b1763 | ||
|
|
96752f9804 | ||
|
|
22411e17cb | ||
|
|
954407ab74 | ||
|
|
edce338e32 | ||
|
|
cf11d16b43 | ||
|
|
17fe41a4b9 | ||
|
|
e2ced4cb53 | ||
|
|
78e03e3004 | ||
|
|
b0c817ee9d | ||
|
|
27af4f618e | ||
|
|
52a02ab310 | ||
|
|
34ce85a83d | ||
|
|
a95a317425 | ||
|
|
40e8782400 | ||
|
|
d97f72633c | ||
|
|
49e34f2b7b | ||
|
|
f5be0d1406 | ||
|
|
984981832e | ||
|
|
718476ccc5 | ||
|
|
9ade25d985 | ||
|
|
62f9024361 | ||
|
|
1ae9d8227a | ||
|
|
f706d3c322 | ||
|
|
4a97da8b47 | ||
|
|
b6fcb63d75 | ||
|
|
4c7ce0ad91 | ||
|
|
81811e55a7 | ||
|
|
0bd3d7c2ed | ||
|
|
d29225fde4 | ||
|
|
57027e35db | ||
|
|
15481dab26 | ||
|
|
a197e31abb | ||
|
|
96b49f0b93 | ||
|
|
10663e4ba2 | ||
|
|
86dfc78d97 | ||
|
|
8ebb18416a | ||
|
|
e898ea67b4 | ||
|
|
23856156e2 | ||
|
|
21aaa9ed81 | ||
|
|
6b4333ae6b | ||
|
|
3bc433c179 | ||
|
|
87eb450047 | ||
|
|
cf68115e6e | ||
|
|
ef78fc9534 | ||
|
|
8879c671e3 | ||
|
|
56873b6065 | ||
|
|
7899b70c18 | ||
|
|
0b4f0129df | ||
|
|
63ee74109e | ||
|
|
4cf6971130 | ||
|
|
dd127ba0ba | ||
|
|
6932c66ffe | ||
|
|
7e1e4c5f78 | ||
|
|
6ced971cf1 | ||
|
|
b430270429 | ||
|
|
29ac94b0e4 | ||
|
|
bfbb9c6f9f | ||
|
|
a146bf03db | ||
|
|
bf8f5d991c | ||
|
|
50cb5ae089 | ||
|
|
6a589017ca | ||
|
|
3945fd5812 | ||
|
|
53032505bd | ||
|
|
b8727202a5 | ||
|
|
7de4c47da2 | ||
|
|
2a931b5906 | ||
|
|
be343e3134 | ||
|
|
0e3347cba6 | ||
|
|
dccb382283 | ||
|
|
134461723e | ||
|
|
4a6e46152a | ||
|
|
6de17fcb75 | ||
|
|
560679a2ad | ||
|
|
af21973ea2 | ||
|
|
07ffc2b955 | ||
|
|
2b5d9bb47c | ||
|
|
cd993f4584 | ||
|
|
210f606be8 | ||
|
|
93233b2c6b | ||
|
|
f513c3f0dd | ||
|
|
b21630d6d1 | ||
|
|
61268e8117 | ||
|
|
df9c9adeff | ||
|
|
87109e5fb5 | ||
|
|
a282bdc601 | ||
|
|
7bb2153fb2 | ||
|
|
30af076000 | ||
|
|
ba1f4271e8 | ||
|
|
a123cddb4b | ||
|
|
8fe196e28b | ||
|
|
d571f21c66 | ||
|
|
3d3a2399b5 | ||
|
|
5a10326612 | ||
|
|
934198b9a5 | ||
|
|
b25f657394 | ||
|
|
6715ac526e | ||
|
|
0b0539af17 | ||
|
|
5a2dfac674 | ||
|
|
6af82efcad | ||
|
|
54c633db36 | ||
|
|
73e0c51a5a | ||
|
|
832f91adbc | ||
|
|
8d50c3bc05 | ||
|
|
2e7036e85c | ||
|
|
f9157fcf82 | ||
|
|
5c67c93ac9 | ||
|
|
f141a086fc | ||
|
|
94fdc56b64 | ||
|
|
2c3582ad0b | ||
|
|
8fd0f9965e | ||
|
|
39bcd1e088 | ||
|
|
06c3318bba | ||
|
|
77f08d095d | ||
|
|
c27eca08e3 | ||
|
|
2597d6d6d4 | ||
|
|
6c918ca85f | ||
|
|
aba7652b76 | ||
|
|
5ce26962c5 | ||
|
|
30f09cc54c | ||
|
|
277ad4a71b | ||
|
|
d3b16ddec8 | ||
|
|
266e72149b | ||
|
|
27e47ae8e6 | ||
|
|
7f8ce2dfd6 | ||
|
|
1eeec9e183 | ||
|
|
54dd6e6da2 | ||
|
|
a820bda89d | ||
|
|
306b51011f | ||
|
|
cb93c0f8f5 | ||
|
|
290d3879eb | ||
|
|
9850cb5057 | ||
|
|
11919c8a97 | ||
|
|
4e8d9d26a9 | ||
|
|
14170f1be8 | ||
|
|
84f4bc6ca4 | ||
|
|
2ffad5c18a | ||
|
|
5592132f56 | ||
|
|
e643890176 | ||
|
|
3d93174c43 | ||
|
|
0d3b6d8ff6 | ||
|
|
8831754f5c | ||
|
|
2269ec727f | ||
|
|
3b361cf51c | ||
|
|
0c5bbdaad0 | ||
|
|
e68dfa511e | ||
|
|
17fa101c16 | ||
|
|
d643d64194 | ||
|
|
8560bd845c | ||
|
|
98e6519878 | ||
|
|
a840f3e775 | ||
|
|
ac7b406d9d | ||
|
|
bc6fd1fe79 | ||
|
|
39f6dc7108 | ||
|
|
33152f4162 | ||
|
|
3f590b4828 | ||
|
|
8748ce9f57 | ||
|
|
7b86539be2 | ||
|
|
89a8047e18 | ||
|
|
d377c78aa3 | ||
|
|
875dffca9e | ||
|
|
ecc48c6d86 | ||
|
|
6a9e1a7aad | ||
|
|
6912af6d83 | ||
|
|
4f7a6cebbc | ||
|
|
401d2c6acc | ||
|
|
ef44aa71d3 | ||
|
|
a6160b99d9 | ||
|
|
b26db874a3 | ||
|
|
bc92f73ffb | ||
|
|
02d2c1fee2 | ||
|
|
d7a6656913 | ||
|
|
ce712f503f | ||
|
|
09116464b6 | ||
|
|
69f7269e7d | ||
|
|
abc2f57768 | ||
|
|
0cd0e57e9c | ||
|
|
e202231d56 | ||
|
|
65c056d7e2 | ||
|
|
45b0c92b3a | ||
|
|
5ca5c59de2 | ||
|
|
20ec18807c | ||
|
|
06045b578a | ||
|
|
23edebbaed | ||
|
|
f57c500034 | ||
|
|
b52a36d6e0 | ||
|
|
58cebd63c1 | ||
|
|
53342ee5f3 | ||
|
|
6afe7f12b1 | ||
|
|
596f7a5cda | ||
|
|
2cd936366b | ||
|
|
36088884ac | ||
|
|
3cfbc9e234 | ||
|
|
c31874fe87 | ||
|
|
6405977c03 | ||
|
|
48e4a38655 | ||
|
|
06d15572d3 | ||
|
|
6a14c838ca | ||
|
|
92c84bbee4 | ||
|
|
280d7931c1 | ||
|
|
5db2bf75c4 | ||
|
|
f6d093e75e | ||
|
|
9831b28dd7 | ||
|
|
9eed27a9b8 | ||
|
|
47f31dd15d | ||
|
|
d534c15b0a | ||
|
|
936989a88b | ||
|
|
01fc684502 | ||
|
|
32103aa3fd | ||
|
|
da23f4572d | ||
|
|
a08a38d4d9 | ||
|
|
e566e817fb | ||
|
|
efc3e072a4 | ||
|
|
a4514860e5 | ||
|
|
a5f5504b0d | ||
|
|
3688c47f1f | ||
|
|
85eb8d47d4 | ||
|
|
e1f24786f4 | ||
|
|
12f36b9a5a | ||
|
|
a37c201776 | ||
|
|
d4998d7b88 | ||
|
|
38e1d68d7a | ||
|
|
3375473e2b | ||
|
|
b3260b1d15 | ||
|
|
889c1d7c15 | ||
|
|
eeecb48775 | ||
|
|
7159dab78d | ||
|
|
2edb8dede8 | ||
|
|
b8a27720f2 | ||
|
|
1f02abe381 | ||
|
|
f1ddaf46c7 | ||
|
|
130b9bb2c1 | ||
|
|
ff7beea3da | ||
|
|
cb613022ff | ||
|
|
4c663056d9 | ||
|
|
6b77b8d978 | ||
|
|
a20d253819 | ||
|
|
a06b735f32 | ||
|
|
52ce64302f | ||
|
|
1296211c2e | ||
|
|
6b0b29fd04 | ||
|
|
a6886d3fc4 | ||
|
|
4ce9ff7845 | ||
|
|
d1ddb68d37 | ||
|
|
91bae4baa5 | ||
|
|
d8111ea65b | ||
|
|
48013bb259 | ||
|
|
00a01d1a39 | ||
|
|
597c69036d | ||
|
|
9287aa5ef7 | ||
|
|
b2da3e0a02 | ||
|
|
950831d6d1 | ||
|
|
81a3de1d9d | ||
|
|
ccf58b069d | ||
|
|
4f1549a6ae | ||
|
|
cbf6d5cfe2 | ||
|
|
4fdd71d186 | ||
|
|
74bbac994d | ||
|
|
661aa45eeb | ||
|
|
fc09643aa2 | ||
|
|
784b6cf2da | ||
|
|
e0f6f78b02 | ||
|
|
7cb596c807 | ||
|
|
b8bbe0e1e4 | ||
|
|
46aa0ff9d0 | ||
|
|
8f37126df7 | ||
|
|
919c8f1da5 | ||
|
|
191e9b68d7 | ||
|
|
a3fda2ada9 | ||
|
|
ac3aaad70c | ||
|
|
6ace272a4b | ||
|
|
9b4568c78f | ||
|
|
1c85eff9b1 | ||
|
|
e2965b5f96 | ||
|
|
0e9ebe0a92 | ||
|
|
dfd63a2145 | ||
|
|
cc6da043bd | ||
|
|
b7e0decf0c | ||
|
|
e3a4280788 | ||
|
|
73ae1decdf | ||
|
|
0adc4ed0e7 | ||
|
|
90ea70b63b | ||
|
|
50f38cc0de | ||
|
|
f507f52e19 | ||
|
|
a5668efb7a | ||
|
|
f9b3733f77 | ||
|
|
c456e6fa97 | ||
|
|
e3916e2606 | ||
|
|
d0b3b1e1a4 | ||
|
|
b7741b6f8c | ||
|
|
f537b06e8d | ||
|
|
4aedf91028 | ||
|
|
9307df225d | ||
|
|
e7f6dfa925 | ||
|
|
e17e881653 | ||
|
|
a9f34bf1f5 | ||
|
|
5e44f53c5b | ||
|
|
e64a9a0507 | ||
|
|
caa7b8a81d | ||
|
|
dd923d9752 | ||
|
|
5ad3f7adaa | ||
|
|
28b19f4c66 | ||
|
|
38bab38ce7 | ||
|
|
bd8e986bb2 | ||
|
|
6014aa5688 | ||
|
|
019dd6282e | ||
|
|
b5290d0692 | ||
|
|
45f0f4a5ea | ||
|
|
c36089c417 | ||
|
|
05ea6054e3 | ||
|
|
bf16038b5c | ||
|
|
367c600849 | ||
|
|
068e6e0291 | ||
|
|
832ab4787c | ||
|
|
60f53f5d58 | ||
|
|
a32c2e6cf5 | ||
|
|
856b3efeba | ||
|
|
c2b46b1331 | ||
|
|
0904357eb9 | ||
|
|
97c8944eeb | ||
|
|
02ddaafd99 | ||
|
|
00c99972df | ||
|
|
ccd51b40dd | ||
|
|
963207d265 | ||
|
|
8504029d73 | ||
|
|
e92806278d | ||
|
|
74171908cb | ||
|
|
d06f0a0ee7 | ||
|
|
31ea86bf7d | ||
|
|
003782a521 | ||
|
|
3356aafe3a | ||
|
|
532fea836b | ||
|
|
ccb9c68487 | ||
|
|
48eb4e39b2 | ||
|
|
5f5a0cf916 | ||
|
|
945d46c2b7 | ||
|
|
e7c9e84a42 |
159
.agents/skills/clawdtributor/SKILL.md
Normal file
159
.agents/skills/clawdtributor/SKILL.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: clawdtributor
|
||||
description: "Use for OpenClaw clawtributors PR/issue triage: Discrawl discovery, live-open rechecks, deep review, topic grouping, and compact @handle/LOC/type/blast/verification summaries."
|
||||
---
|
||||
|
||||
# Clawdtributor
|
||||
|
||||
Use for the `#clawtributors` queue: Discord-discovered OpenClaw PRs/issues that need live GitHub status plus maintainer-quality review.
|
||||
|
||||
## Compose with other skills
|
||||
|
||||
- `$discrawl`: local Discord archive sync/search.
|
||||
- `$openclaw-pr-maintainer`: live GitHub PR/issue review, duplicate search, close/land rules.
|
||||
- `$gitcrawl`: related issue/PR and current-main/stale-proof search.
|
||||
- `$openclaw-testing` / `$crabbox`: proof choice when a candidate needs real validation.
|
||||
|
||||
## Archive flow
|
||||
|
||||
Local archive first; verify freshness for current questions.
|
||||
|
||||
```bash
|
||||
discrawl status --json
|
||||
discrawl sync
|
||||
```
|
||||
|
||||
Resolve channel if needed:
|
||||
|
||||
```bash
|
||||
sqlite3 "$HOME/.discrawl/discrawl.db" \
|
||||
"select id,name from channels where name like '%clawtributor%' order by name;"
|
||||
```
|
||||
|
||||
Current known channel id from prior work: `1458141495701012561`. Re-resolve if it stops matching.
|
||||
|
||||
Extract recent refs:
|
||||
|
||||
```bash
|
||||
sqlite3 "$HOME/.discrawl/discrawl.db" "
|
||||
select m.created_at, coalesce(nullif(mm.username,''), m.author_id), m.content
|
||||
from messages m
|
||||
left join members mm on mm.guild_id=m.guild_id and mm.user_id=m.author_id
|
||||
where m.channel_id='1458141495701012561'
|
||||
and m.created_at >= '<ISO cutoff>'
|
||||
order by m.created_at desc;" |
|
||||
perl -nE 'while(m{github\.com/openclaw/openclaw/(pull|issues)/(\d+)}g){say "$1\t$2\t$_"}'
|
||||
```
|
||||
|
||||
Map a PR/issue back to the Discord handle:
|
||||
|
||||
```bash
|
||||
sqlite3 -separator $'\t' "$HOME/.discrawl/discrawl.db" "
|
||||
select m.created_at,
|
||||
coalesce(nullif(mm.username,''), nullif(mm.global_name,''), m.author_id)
|
||||
from messages m
|
||||
left join members mm on mm.guild_id=m.guild_id and mm.user_id=m.author_id
|
||||
where m.channel_id='1458141495701012561'
|
||||
and m.content like '%github.com/openclaw/openclaw/<pull-or-issues>/<number>%'
|
||||
order by m.created_at desc
|
||||
limit 1;"
|
||||
```
|
||||
|
||||
Show only `@handle` in the final list. Do not write the word Discord unless the user asks for source details.
|
||||
|
||||
## Live GitHub recheck
|
||||
|
||||
Always recheck live state before listing, closing, or saying "open".
|
||||
|
||||
```bash
|
||||
GITHUB_TOKEN= GITHUB_TOKEN_NODIFF= GH_TOKEN= \
|
||||
gh api repos/openclaw/openclaw/pulls/<number> \
|
||||
--jq '. | {number,title,state,merged,mergeable,draft,author:.user.login,url:.html_url,updatedAt:.updated_at,additions,deletions,changedFiles:.changed_files}'
|
||||
```
|
||||
|
||||
For issues:
|
||||
|
||||
```bash
|
||||
GITHUB_TOKEN= GITHUB_TOKEN_NODIFF= GH_TOKEN= \
|
||||
gh api repos/openclaw/openclaw/issues/<number> \
|
||||
--jq '. | {number,title,state,author:.user.login,url:.html_url,updatedAt:.updated_at,pull_request}'
|
||||
```
|
||||
|
||||
If `gh` says bad credentials, clear env vars with empty assignments as above. Use `--jq '. | {...}'` for object projections.
|
||||
|
||||
## Review depth
|
||||
|
||||
For each open item, inspect enough to classify risk:
|
||||
|
||||
- PR body, linked issue, comments, files, additions/deletions, checks.
|
||||
- Current `origin/main` code path and adjacent tests.
|
||||
- Related threads with `gitcrawl neighbors/search`.
|
||||
- Whether main already fixed it, the PR is obsolete, or the idea is invalid.
|
||||
- Blast radius: touched runtime surfaces, config/schema, plugin/core boundary, user-visible behavior, release/package surface.
|
||||
- Verification: say if local unit/docs proof is enough, live/provider proof is needed, or it is not directly verifiable.
|
||||
|
||||
Do not close from title alone. If closing as done on main or nonsensical, prove it against current main and comment first when mutation is requested. Bulk close/reopen above 5 requires explicit scope.
|
||||
|
||||
## Candidate selection
|
||||
|
||||
When asked for `5 new`, exclude refs already surfaced in the session and refill from the archive until there are 5 live-open candidates. If fewer than 5 remain open, list all open ones and say how many short.
|
||||
|
||||
When asked to `update`, `refresh`, `recheck`, `check again`, or similar, return an updated live-open candidate list. Do not fill the main list with items that merely merged/closed since the last pass; put those numbers in a short bottom line.
|
||||
|
||||
Prefer:
|
||||
|
||||
- Fresh, open, external contributor work.
|
||||
- Small, high-confidence bugfixes.
|
||||
- Clear repro, tests, or obvious code-path proof.
|
||||
|
||||
Demote:
|
||||
|
||||
- Broad product/features without owner decision.
|
||||
- Large rewrites with unclear contract.
|
||||
- PRs already in progress, merged, closed, duplicate, or fixed on main.
|
||||
|
||||
## Topic grouping
|
||||
|
||||
Group only when useful or requested:
|
||||
|
||||
- Agents/tooling
|
||||
- Providers/auth/models
|
||||
- Channels/messaging
|
||||
- UI/web
|
||||
- Gateway/protocol/runtime
|
||||
- Config/memory/cache
|
||||
- Docker/install/release
|
||||
- Docs/tests/chore
|
||||
- Closed/obsolete
|
||||
|
||||
Infer topic from labels, touched files, title/body, and actual code path.
|
||||
|
||||
## Output format
|
||||
|
||||
No Markdown tables. Compact bullets. Use color/risk markers:
|
||||
|
||||
- 🟢 low/narrow
|
||||
- 🟡 medium or needs targeted proof
|
||||
- 🔴 broad/high runtime risk
|
||||
- 🟣 security/policy/owner-boundary slow review
|
||||
- ✅ merged
|
||||
- ⚪ closed unmerged
|
||||
|
||||
Required line shape:
|
||||
|
||||
```markdown
|
||||
- **PR #81244** `@whatsskill.` `+118/-1` `bug` 🟢 verifiable: yes. This prevents chat action buttons from overlapping short assistant replies. Blast: web chat rendering, low.
|
||||
- **Issue #81245** `@alice` `LOC n/a` `bug` 🟡 verifiable: partial. This reports duplicate Telegram replies when reconnecting after gateway restart. Blast: Telegram channel runtime, medium.
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Bold the `PR #n` or `Issue #n` marker.
|
||||
- Use `@handle`, not author bio text.
|
||||
- PR LOC is `+additions/-deletions`; issue LOC is `LOC n/a`.
|
||||
- Type: `bug`, `feature`, `perf`, `security`, `docs`, `test`, `chore`, or `refactor`.
|
||||
- Write a full sentence for what it does.
|
||||
- Always include blast radius in one phrase.
|
||||
- Always include `verifiable: yes|partial|no` plus the shortest proof hint when helpful.
|
||||
- If status is not open, still show it only when the user asked for all surfaced refs; use ✅ or ⚪ and state merged/closed.
|
||||
- For refresh-style asks, bottom line: `Merged/closed since last pass: #81016 merged, #81026 closed.` Omit if none.
|
||||
103
.agents/skills/codex-review/SKILL.md
Normal file
103
.agents/skills/codex-review/SKILL.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: codex-review
|
||||
description: "Codex code review closeout: local dirty changes, PR branch vs main, parallel tests."
|
||||
---
|
||||
|
||||
# Codex Review
|
||||
|
||||
Run Codex's built-in code review as a closeout check. This is code review (`codex review`), not Guardian `auto_review` approval routing.
|
||||
|
||||
Use when:
|
||||
- user asks for Codex review / autoreview / second-model review
|
||||
- after non-trivial code edits, before final/commit/ship
|
||||
- reviewing a local branch or PR branch after fixes
|
||||
|
||||
## Contract
|
||||
|
||||
- Treat review output as advisory. Never blindly apply it.
|
||||
- Verify every finding by reading the real code path and adjacent files.
|
||||
- Read dependency docs/source/types when the finding depends on external behavior.
|
||||
- Reject unrealistic edge cases, speculative risks, broad rewrites, and fixes that over-complicate the codebase.
|
||||
- Prefer small fixes at the right ownership boundary; no refactor unless it clearly improves the bug class.
|
||||
- Keep going until Codex review returns no accepted/actionable findings.
|
||||
- If a review-triggered fix changes code, rerun focused tests and rerun Codex review.
|
||||
- If rejecting a finding as intentional/not worth fixing, add a brief inline code comment only when it explains a real invariant or ownership decision that future reviewers should know.
|
||||
- Do not push just to review. Push only when the user requested push/ship/PR update.
|
||||
|
||||
## Pick Target
|
||||
|
||||
Dirty local work:
|
||||
|
||||
```bash
|
||||
codex review --uncommitted
|
||||
```
|
||||
|
||||
Branch/PR work:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
codex review --base origin/main
|
||||
```
|
||||
|
||||
Do not pass an inline prompt with `--base`; current CLI rejects `--base` + `[PROMPT]` even though help text is ambiguous. If custom instructions are needed, run the plain base review first, then do a local/manual follow-up pass.
|
||||
|
||||
If an open PR exists, use its actual base:
|
||||
|
||||
```bash
|
||||
base=$(gh pr view --json baseRefName --jq .baseRefName)
|
||||
codex review --base "origin/$base"
|
||||
```
|
||||
|
||||
Committed single change:
|
||||
|
||||
```bash
|
||||
codex review --commit HEAD
|
||||
```
|
||||
|
||||
## Parallel Closeout
|
||||
|
||||
Format first if formatting can change line locations. Then it is OK to run tests and review in parallel:
|
||||
|
||||
```bash
|
||||
scripts/codex-review --parallel-tests "<focused test command>"
|
||||
```
|
||||
|
||||
Tradeoff: tests may force code changes that stale the review. If tests or review lead to code edits, rerun the affected tests and rerun review until no accepted/actionable findings remain.
|
||||
|
||||
## Context Efficiency
|
||||
|
||||
Codex review is usually noisy. Default to a subagent filter when subagents are available. Ask it to run the review and return only:
|
||||
- actionable findings it accepts
|
||||
- findings it rejects, with one-line reason
|
||||
- exact files/tests to rerun
|
||||
|
||||
Run inline only for tiny changes or when subagents are unavailable.
|
||||
|
||||
## Helper
|
||||
|
||||
Bundled helper:
|
||||
|
||||
```bash
|
||||
~/.codex/skills/codex-review/scripts/codex-review --help
|
||||
```
|
||||
|
||||
If installed from `agent-scripts`, path is:
|
||||
|
||||
```bash
|
||||
/Users/steipete/Projects/agent-scripts/skills/codex-review/scripts/codex-review --help
|
||||
```
|
||||
|
||||
The helper:
|
||||
- chooses dirty `--uncommitted` first
|
||||
- otherwise uses current PR base if `gh pr view` works
|
||||
- otherwise uses `origin/main` for non-main branches
|
||||
- writes only to stdout unless `--output` or `CODEX_REVIEW_OUTPUT` is set
|
||||
- supports `--dry-run` and `--parallel-tests`
|
||||
|
||||
## Final Report
|
||||
|
||||
Include:
|
||||
- review command used
|
||||
- tests/proof run
|
||||
- findings accepted/rejected, briefly why
|
||||
- final clean review command, or why a remaining finding was consciously rejected
|
||||
188
.agents/skills/codex-review/scripts/codex-review
Executable file
188
.agents/skills/codex-review/scripts/codex-review
Executable file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: codex-review [options]
|
||||
|
||||
Options:
|
||||
--mode auto|local|branch Target selection. Default: auto.
|
||||
--base REF Base ref for branch review. Default: PR base or origin/main.
|
||||
--codex-bin PATH Codex binary. Default: codex.
|
||||
--output FILE Also save output to file.
|
||||
--parallel-tests CMD Run review and test command concurrently.
|
||||
--dry-run Print selected commands, do not run.
|
||||
-h, --help Show help.
|
||||
|
||||
Modes:
|
||||
local codex review --uncommitted
|
||||
branch codex review --base <base>
|
||||
auto dirty tree -> local, else PR/current branch -> branch
|
||||
EOF
|
||||
}
|
||||
|
||||
mode=auto
|
||||
base_ref=
|
||||
codex_bin=${CODEX_BIN:-codex}
|
||||
output=${CODEX_REVIEW_OUTPUT:-}
|
||||
parallel_tests=
|
||||
dry_run=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--mode)
|
||||
mode=${2:-}
|
||||
shift 2
|
||||
;;
|
||||
--base)
|
||||
base_ref=${2:-}
|
||||
shift 2
|
||||
;;
|
||||
--codex-bin)
|
||||
codex_bin=${2:-}
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
output=${2:-}
|
||||
shift 2
|
||||
;;
|
||||
--parallel-tests)
|
||||
parallel_tests=${2:-}
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$mode" in
|
||||
auto|local|branch) ;;
|
||||
*)
|
||||
echo "invalid --mode: $mode" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
git rev-parse --show-toplevel >/dev/null
|
||||
|
||||
current_branch=$(git branch --show-current 2>/dev/null || true)
|
||||
dirty=false
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
dirty=true
|
||||
fi
|
||||
|
||||
pr_url=
|
||||
if [[ -z "$base_ref" && "$mode" != local ]] && command -v gh >/dev/null 2>&1; then
|
||||
if pr_lines=$(gh pr view --json baseRefName,url --jq '[.baseRefName, .url] | @tsv' 2>/dev/null); then
|
||||
base_name=${pr_lines%%$'\t'*}
|
||||
pr_url=${pr_lines#*$'\t'}
|
||||
if [[ -n "$base_name" ]]; then
|
||||
base_ref="origin/$base_name"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$base_ref" ]]; then
|
||||
base_ref=origin/main
|
||||
fi
|
||||
|
||||
review_kind=
|
||||
if [[ "$mode" == local || ( "$mode" == auto && "$dirty" == true ) ]]; then
|
||||
review_kind=local
|
||||
elif [[ "$mode" == branch || ( "$mode" == auto && -n "$current_branch" && "$current_branch" != "main" ) ]]; then
|
||||
review_kind=branch
|
||||
else
|
||||
echo "no review target: clean main checkout and no forced mode" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$review_kind" == local ]]; then
|
||||
review_cmd=("$codex_bin" review --uncommitted)
|
||||
else
|
||||
review_cmd=("$codex_bin" review --base "$base_ref")
|
||||
fi
|
||||
|
||||
printf 'codex-review target: %s\n' "$review_kind"
|
||||
printf 'branch: %s\n' "${current_branch:-detached}"
|
||||
if [[ -n "$pr_url" ]]; then
|
||||
printf 'pr: %s\n' "$pr_url"
|
||||
fi
|
||||
printf 'review:'
|
||||
printf ' %q' "${review_cmd[@]}"
|
||||
printf '\n'
|
||||
if [[ -n "$parallel_tests" ]]; then
|
||||
printf 'tests: %s\n' "$parallel_tests"
|
||||
fi
|
||||
if [[ "$review_kind" == branch ]]; then
|
||||
printf 'fetch: git fetch origin --quiet\n'
|
||||
fi
|
||||
if [[ -n "$output" ]]; then
|
||||
printf 'output: %s\n' "$output"
|
||||
fi
|
||||
|
||||
if [[ "$dry_run" == true ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$review_kind" == branch ]]; then
|
||||
git fetch origin --quiet || {
|
||||
echo "warning: git fetch origin failed; reviewing with existing refs" >&2
|
||||
}
|
||||
fi
|
||||
|
||||
run_review() {
|
||||
if [[ -n "$output" ]]; then
|
||||
mkdir -p "$(dirname "$output")"
|
||||
"${review_cmd[@]}" 2>&1 | tee "$output"
|
||||
else
|
||||
"${review_cmd[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -z "$parallel_tests" ]]; then
|
||||
run_review
|
||||
exit $?
|
||||
fi
|
||||
|
||||
review_status_file=$(mktemp)
|
||||
tests_status_file=$(mktemp)
|
||||
|
||||
(
|
||||
set +e
|
||||
run_review
|
||||
status=$?
|
||||
printf '%s\n' "$status" > "$review_status_file"
|
||||
) &
|
||||
review_pid=$!
|
||||
|
||||
(
|
||||
set +e
|
||||
bash -lc "$parallel_tests"
|
||||
status=$?
|
||||
printf '%s\n' "$status" > "$tests_status_file"
|
||||
) &
|
||||
tests_pid=$!
|
||||
|
||||
wait "$review_pid" || true
|
||||
wait "$tests_pid" || true
|
||||
|
||||
review_status=$(cat "$review_status_file")
|
||||
tests_status=$(cat "$tests_status_file")
|
||||
rm -f "$review_status_file" "$tests_status_file"
|
||||
|
||||
printf 'codex-review exit: %s\n' "$review_status"
|
||||
printf 'tests exit: %s\n' "$tests_status"
|
||||
|
||||
if [[ "$review_status" != 0 || "$tests_status" != 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: crabbox
|
||||
description: Use Crabbox for OpenClaw remote Linux validation. Default to Blacksmith Testbox; includes direct Blacksmith and owned AWS/Hetzner fallback notes when Crabbox fails.
|
||||
description: Use Crabbox for OpenClaw remote validation across Linux, macOS, Windows, and WSL2. Default to the repo Crabbox config, use brokered AWS for normal broad proof, and keep Blacksmith Testbox as an explicit opt-in or outage diagnostic path.
|
||||
---
|
||||
|
||||
# Crabbox
|
||||
@@ -9,9 +9,15 @@ Use Crabbox when OpenClaw needs remote Linux proof for broad tests, CI-parity
|
||||
checks, secrets, hosted services, Docker/E2E/package lanes, warmed reusable
|
||||
boxes, sync timing, logs/results, cache inspection, or lease cleanup.
|
||||
|
||||
Default backend: `blacksmith-testbox`. The separate `blacksmith-testbox` skill
|
||||
has been removed; this skill owns both the normal Crabbox path and the direct
|
||||
Blacksmith fallback playbook.
|
||||
Default backend: the repo `.crabbox.yaml`, currently brokered AWS. Do not
|
||||
override it to Blacksmith unless the user explicitly asks for Blacksmith proof,
|
||||
the task is specifically about Testbox behavior, or AWS/brokered Crabbox is the
|
||||
broken layer.
|
||||
|
||||
Blacksmith Testbox is a delegated fallback, not the default router. If a
|
||||
Blacksmith run queues, fails capacity, fails auth, or cannot allocate, stop
|
||||
after one real attempt and switch to the repo default or report the blocker.
|
||||
Do not retry Blacksmith in a loop.
|
||||
|
||||
## First Checks
|
||||
|
||||
@@ -28,16 +34,20 @@ pnpm crabbox:run -- --help | sed -n '1,120p'
|
||||
|
||||
- OpenClaw scripts prefer `../crabbox/bin/crabbox` when present. The user PATH
|
||||
shim can be stale.
|
||||
- Check `.crabbox.yaml` for repo defaults, but override provider explicitly.
|
||||
Even if config still says AWS, maintainer validation should normally pass
|
||||
`--provider blacksmith-testbox`.
|
||||
- For live/provider bugs, check keys on the local Mac before downgrading to
|
||||
mocks: source local `~/.profile` and test only presence/length. If Crabbox
|
||||
does not already have the key, copy only the exact needed key into the remote
|
||||
process environment for that one command. Do not print it, do not sync it as a
|
||||
repo file, and do not leave it in remote shell history or logs. If no
|
||||
secret-safe injection path is available, say true live provider auth is
|
||||
blocked instead of silently using a fake key.
|
||||
- Check `.crabbox.yaml` for repo defaults and honor them. For normal Linux
|
||||
validation, omit `--provider` so the wrapper uses brokered AWS.
|
||||
- Pass `--provider blacksmith-testbox` only for explicit Blacksmith/Testbox
|
||||
work or a deliberate comparison.
|
||||
- If a warm direct-provider lease smells stale, retry with `--full-resync`
|
||||
(alias `--fresh-sync`) before replacing the lease. This resets the remote
|
||||
workdir, skips the fingerprint fast path, reseeds Git when possible, and
|
||||
uploads the checkout from scratch.
|
||||
- For live/provider bugs, use the configured secret workflow before downgrading
|
||||
to mocks. Copy only the exact needed key into the remote process environment
|
||||
for that one command. Do not print it, do not sync it as a repo file, and do
|
||||
not leave it in remote shell history or logs. If no secret-safe injection path
|
||||
is available, say true live provider auth is blocked instead of silently using
|
||||
a fake key.
|
||||
- Prefer local targeted tests for tight edit loops. Broad gates belong remote.
|
||||
- Do not treat inherited shell env as operator intent. In particular,
|
||||
`OPENCLAW_LOCAL_CHECK_MODE=throttled` from the local shell is not permission
|
||||
@@ -51,7 +61,8 @@ pnpm crabbox:run -- --help | sed -n '1,120p'
|
||||
## macOS And Windows Targets
|
||||
|
||||
Use these only when the task needs an existing non-Linux host. OpenClaw broad
|
||||
validation still defaults to `blacksmith-testbox`.
|
||||
Linux validation uses the repo Crabbox config unless a provider is explicitly
|
||||
requested.
|
||||
|
||||
Crabbox supports static SSH targets:
|
||||
|
||||
@@ -64,14 +75,15 @@ Crabbox supports static SSH targets:
|
||||
- `target=macos` and `target=windows --windows-mode wsl2` use the POSIX SSH,
|
||||
bash, Git, rsync, and tar contract.
|
||||
- Native Windows uses OpenSSH, PowerShell, Git, and tar; sync is manifest tar
|
||||
archive transfer into `static.workRoot`.
|
||||
archive transfer into `static.workRoot`. Direct native Windows runs support
|
||||
`--script*`, `--env-from-profile`, `--preflight`, and PowerShell `--shell`.
|
||||
- `crabbox actions hydrate/register` are Linux-only today; use plain
|
||||
`crabbox run` loops for static macOS and Windows hosts.
|
||||
- Live proof needs a reachable, operator-managed SSH host. Without one, verify
|
||||
with `../crabbox/bin/crabbox run --help`, config/flag tests, and the Crabbox
|
||||
Go test suite.
|
||||
|
||||
## Default Blacksmith Backend
|
||||
## Default Brokered AWS Backend
|
||||
|
||||
Use this for `pnpm check`, `pnpm check:changed`, `pnpm test`,
|
||||
`pnpm test:changed`, Docker/E2E/live/package gates, or anything likely to fan
|
||||
@@ -80,11 +92,7 @@ out across many Vitest projects.
|
||||
Changed gate:
|
||||
|
||||
```sh
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox \
|
||||
--blacksmith-org openclaw \
|
||||
--blacksmith-workflow .github/workflows/ci-check-testbox.yml \
|
||||
--blacksmith-job check \
|
||||
--blacksmith-ref main \
|
||||
pnpm crabbox:run -- \
|
||||
--idle-timeout 90m \
|
||||
--ttl 240m \
|
||||
--timing-json \
|
||||
@@ -95,11 +103,7 @@ pnpm crabbox:run -- --provider blacksmith-testbox \
|
||||
Full suite:
|
||||
|
||||
```sh
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox \
|
||||
--blacksmith-org openclaw \
|
||||
--blacksmith-workflow .github/workflows/ci-check-testbox.yml \
|
||||
--blacksmith-job check \
|
||||
--blacksmith-ref main \
|
||||
pnpm crabbox:run -- \
|
||||
--idle-timeout 90m \
|
||||
--ttl 240m \
|
||||
--timing-json \
|
||||
@@ -110,11 +114,7 @@ pnpm crabbox:run -- --provider blacksmith-testbox \
|
||||
Focused rerun:
|
||||
|
||||
```sh
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox \
|
||||
--blacksmith-org openclaw \
|
||||
--blacksmith-workflow .github/workflows/ci-check-testbox.yml \
|
||||
--blacksmith-job check \
|
||||
--blacksmith-ref main \
|
||||
pnpm crabbox:run -- \
|
||||
--idle-timeout 90m \
|
||||
--ttl 240m \
|
||||
--timing-json \
|
||||
@@ -124,19 +124,18 @@ pnpm crabbox:run -- --provider blacksmith-testbox \
|
||||
|
||||
Read the JSON summary. Useful fields:
|
||||
|
||||
- `provider`: should be `blacksmith-testbox`
|
||||
- `leaseId`: `tbx_...`
|
||||
- `syncDelegated`: should be `true`
|
||||
- `provider`: should normally be `aws`
|
||||
- `leaseId`: `cbx_...`
|
||||
- `syncDelegated`: should normally be `false`
|
||||
- `commandPhases`: populated when the command prints `CRABBOX_PHASE:<name>`
|
||||
- `commandMs` / `totalMs`
|
||||
- `exitCode`
|
||||
|
||||
Crabbox should stop one-shot Blacksmith Testboxes automatically after the run.
|
||||
Verify cleanup when a run fails, is interrupted, or the command output is
|
||||
unclear:
|
||||
Crabbox should stop one-shot AWS leases automatically after the run. Verify
|
||||
cleanup when a run fails, is interrupted, or the command output is unclear:
|
||||
|
||||
```sh
|
||||
blacksmith testbox list
|
||||
../crabbox/bin/crabbox list --provider aws
|
||||
```
|
||||
|
||||
## Observability Flags
|
||||
@@ -144,8 +143,16 @@ blacksmith testbox list
|
||||
Use these on debugging runs before inventing ad hoc logging:
|
||||
|
||||
- `--preflight`: prints run context, workspace mode, SSH target, remote user/cwd,
|
||||
sudo/apt, Node, pnpm, Docker, and bubblewrap. On `blacksmith-testbox`, this
|
||||
prints a delegated-unsupported note because the workflow owns setup.
|
||||
and target-specific tool probes. Defaults cover `git`, `tar`, `node`, `npm`,
|
||||
`corepack`, `pnpm`, `yarn`, `bun`, `docker`, plus POSIX
|
||||
`sudo`/`apt`/`bubblewrap` and native Windows
|
||||
`powershell`/`execution_policy`/`longpaths`/`temp`/`pwsh`. Add
|
||||
`--preflight-tools node,bun,docker`, `CRABBOX_PREFLIGHT_TOOLS`, or repo
|
||||
`run.preflightTools` to replace the list. `default` expands built-ins; `none`
|
||||
prints only the workspace summary. Preflight is diagnostic only; install
|
||||
toolchains through Actions hydration, images, devcontainer/Nix/mise/asdf, or
|
||||
the run script. On `blacksmith-testbox`, this prints a delegated-unsupported
|
||||
note because the workflow owns setup.
|
||||
- `CRABBOX_ENV_ALLOW=NAME,...`: forwards only listed local env vars for direct
|
||||
providers and prints `set len=N secret=true` style summaries. On
|
||||
`blacksmith-testbox`, env forwarding is unsupported; put secrets in the
|
||||
@@ -154,21 +161,36 @@ Use these on debugging runs before inventing ad hoc logging:
|
||||
`export NAME=value` / `NAME=value` lines from a local profile without
|
||||
executing it, then forwards only allowlisted names. `--allow-env` is
|
||||
repeatable and comma-separated. Profile values override ambient allowlisted
|
||||
env values for that run.
|
||||
env values for that run. Direct POSIX, WSL2, and native Windows runs are
|
||||
supported; delegated providers are not. Crabbox probes the uploaded profile
|
||||
remotely and prints redacted presence/length metadata before the command.
|
||||
- `--env-helper <name>`: with `--env-from-profile` on POSIX SSH targets,
|
||||
persists `.crabbox/env/<name>` and `.crabbox/env/<name>.env` so follow-up
|
||||
commands on the same lease can run through `./.crabbox/env/<name> <command>`.
|
||||
Use only on leases you control; the profile stays until cleanup, lease reset,
|
||||
or `--full-resync`.
|
||||
- `--script <file>` / `--script-stdin`: upload a local script into
|
||||
`.crabbox/scripts/` and execute it on the remote box. Shebang scripts execute
|
||||
directly; scripts without a shebang run through `bash`. Arguments after `--`
|
||||
become script args.
|
||||
directly on POSIX; scripts without a shebang run through `bash`. Native
|
||||
Windows uploads run through Windows PowerShell, and Crabbox appends `.ps1`
|
||||
when needed. Arguments after `--` become script args.
|
||||
- `--fresh-pr owner/repo#123|URL|number`: skip dirty local sync and create a
|
||||
fresh remote checkout of the GitHub PR. Bare numbers use the current repo's
|
||||
GitHub origin. Add `--apply-local-patch` only when the current local
|
||||
`git diff --binary HEAD` should be applied on top of that PR checkout.
|
||||
- `--full-resync` / `--fresh-sync`: reset a stale direct-provider workdir
|
||||
before syncing. Use after sync fingerprints look wrong, SSH times out before
|
||||
sync, or rsync watchdog output suggests it. It is redundant with
|
||||
`--fresh-pr`, incompatible with `--no-sync`, and unsupported by delegated
|
||||
providers.
|
||||
- `--capture-stdout <path>` / `--capture-stderr <path>`: write remote streams to
|
||||
local files and keep binary/noisy output out of retained logs. Parent
|
||||
directories must already exist. These are direct-provider only.
|
||||
- `--capture-on-fail`: on non-zero direct-provider exits, downloads
|
||||
`.crabbox/captures/*.tar.gz` with `test-results`, `playwright-report`,
|
||||
`coverage`, JUnit XML, and nearby logs. Treat as secret-bearing until reviewed.
|
||||
- `--keep-on-failure`: leave a failed one-shot lease alive for live debugging
|
||||
until idle/TTL expiry. Useful on direct providers and delegated one-shots.
|
||||
- `--timing-json`: final machine-readable timing. Add
|
||||
`echo CRABBOX_PHASE:install`, `CRABBOX_PHASE:test`, etc. in long shell
|
||||
commands; direct providers and Blacksmith Testbox both report them as
|
||||
@@ -180,7 +202,6 @@ Live-provider debug template for direct AWS/Hetzner leases:
|
||||
mkdir -p .crabbox/logs
|
||||
pnpm crabbox:run -- --provider aws \
|
||||
--preflight \
|
||||
--env-from-profile ~/.profile \
|
||||
--allow-env OPENAI_API_KEY,OPENAI_BASE_URL \
|
||||
--timing-json \
|
||||
--capture-stdout .crabbox/logs/live-provider.stdout.log \
|
||||
@@ -191,9 +212,10 @@ pnpm crabbox:run -- --provider aws \
|
||||
```
|
||||
|
||||
Do not pass `--capture-*`, `--download`, `--checksum`, `--force-sync-large`, or
|
||||
`--sync-only` to delegated providers. Also do not pass `--script*` or
|
||||
`--fresh-pr` there. Crabbox rejects these because the provider owns sync or
|
||||
command transport.
|
||||
`--sync-only` to delegated providers. Also do not pass `--script*`,
|
||||
`--fresh-pr`, `--full-resync`, or `--env-helper` there. Crabbox rejects these
|
||||
because the provider owns sync or command transport. `--keep-on-failure` is OK
|
||||
for delegated one-shots when you need to inspect a failed lease.
|
||||
|
||||
## Efficient Bug E2E Verification
|
||||
|
||||
@@ -206,8 +228,8 @@ Pick the lane by symptom:
|
||||
- Docker/setup/install bug: build a package tarball and run the matching
|
||||
`scripts/e2e/*-docker.sh` or package script. This proves npm packaging,
|
||||
install paths, runtime deps, config writes, and container behavior.
|
||||
- Provider/model/auth bug: prefer true live E2E. First source local Mac
|
||||
`~/.profile`, then inject the single needed key into Crabbox if needed. Scrub
|
||||
- Provider/model/auth bug: prefer true live E2E. Use the configured secret
|
||||
workflow, then inject the single needed key into Crabbox if needed. Scrub
|
||||
unrelated provider env vars in the child command so interactive defaults do
|
||||
not drift to another provider. If only a dummy key is used, label the proof
|
||||
narrowly, e.g. "UI/install path only; live provider auth not exercised."
|
||||
@@ -241,6 +263,8 @@ Keep it efficient:
|
||||
- Use `--fresh-pr <pr>` when validating an upstream PR in isolation from the
|
||||
local dirty tree. Add `--apply-local-patch` only when testing a local fixup on
|
||||
top of that PR.
|
||||
- Use `--full-resync` before replacing a warmed direct-provider lease when the
|
||||
remote workdir or sync fingerprint appears stale.
|
||||
- Use one-shot Crabbox for a single proof; use a reusable Testbox only when
|
||||
several commands must share built images, installed packages, or live state.
|
||||
- Prefer `OPENCLAW_CURRENT_PACKAGE_TGZ` with Docker/package lanes when testing a
|
||||
@@ -302,13 +326,13 @@ Interactive CLI/onboarding:
|
||||
|
||||
## Reuse And Keepalive
|
||||
|
||||
For most Blacksmith-backed Crabbox calls, one-shot is enough. Use reuse only
|
||||
when you need multiple manual commands on the same hydrated box.
|
||||
For most Crabbox calls, one-shot is enough. Use reuse only when you need
|
||||
multiple manual commands on the same hydrated box.
|
||||
|
||||
If Crabbox returns a reusable id or you intentionally keep a lease:
|
||||
|
||||
```sh
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox --id <tbx_id> --no-sync --timing-json --shell -- "pnpm test <path>"
|
||||
pnpm crabbox:run -- --id <cbx_id-or-slug> --no-sync --timing-json --shell -- "pnpm test <path>"
|
||||
```
|
||||
|
||||
Stop boxes you created before handoff:
|
||||
@@ -336,10 +360,17 @@ Useful WebVNC commands:
|
||||
|
||||
```sh
|
||||
../crabbox/bin/crabbox webvnc --provider hetzner --id <cbx_id-or-slug> --open
|
||||
../crabbox/bin/crabbox webvnc --provider hetzner --id <cbx_id-or-slug> --daemon --open
|
||||
../crabbox/bin/crabbox webvnc --provider hetzner --id <cbx_id-or-slug> --status
|
||||
../crabbox/bin/crabbox webvnc --provider hetzner --id <cbx_id-or-slug> --stop
|
||||
../crabbox/bin/crabbox screenshot --provider hetzner --id <cbx_id-or-slug> --output desktop.png
|
||||
../crabbox/bin/crabbox webvnc daemon start --provider hetzner --id <cbx_id-or-slug> --open
|
||||
../crabbox/bin/crabbox webvnc daemon status --provider hetzner --id <cbx_id-or-slug>
|
||||
../crabbox/bin/crabbox webvnc daemon stop --provider hetzner --id <cbx_id-or-slug>
|
||||
../crabbox/bin/crabbox webvnc status --provider hetzner --id <cbx_id-or-slug>
|
||||
../crabbox/bin/crabbox webvnc reset --provider hetzner --id <cbx_id-or-slug> --open
|
||||
../crabbox/bin/crabbox desktop doctor --provider hetzner --id <cbx_id-or-slug>
|
||||
../crabbox/bin/crabbox desktop click --provider hetzner --id <cbx_id-or-slug> --x 640 --y 420
|
||||
../crabbox/bin/crabbox desktop paste --provider hetzner --id <cbx_id-or-slug> --text "user@example.com"
|
||||
../crabbox/bin/crabbox desktop key --provider hetzner --id <cbx_id-or-slug> ctrl+l
|
||||
../crabbox/bin/crabbox artifacts collect --id <cbx_id-or-slug> --all --output artifacts/<slug>
|
||||
../crabbox/bin/crabbox artifacts publish --dir artifacts/<slug> --pr <number>
|
||||
```
|
||||
|
||||
`desktop launch --webvnc --open` is usually the nicest one-shot: it starts the
|
||||
@@ -350,14 +381,16 @@ WebVNC portal, and opens the portal. Keep browsers windowed for human QA; use
|
||||
## If Crabbox Fails
|
||||
|
||||
Keep the fallback narrow. First decide whether the failure is Crabbox itself,
|
||||
Blacksmith/Testbox, repo hydration, sync, or the test command.
|
||||
the brokered AWS lease, Blacksmith/Testbox, repo hydration, sync, or the test
|
||||
command.
|
||||
|
||||
Fast checks:
|
||||
|
||||
```sh
|
||||
command -v crabbox
|
||||
../crabbox/bin/crabbox --version
|
||||
crabbox run --provider blacksmith-testbox --help | sed -n '1,140p'
|
||||
pnpm crabbox:run -- --help | sed -n '1,140p'
|
||||
../crabbox/bin/crabbox doctor
|
||||
command -v blacksmith
|
||||
blacksmith --version
|
||||
blacksmith testbox list
|
||||
@@ -367,34 +400,36 @@ Common Crabbox-only failures:
|
||||
|
||||
- Provider missing or old CLI: use `../crabbox/bin/crabbox` from the sibling
|
||||
repo, or update/install Crabbox before retrying.
|
||||
- Bad local config: pass `--provider blacksmith-testbox` plus explicit
|
||||
`--blacksmith-*` flags instead of relying on `.crabbox.yaml`.
|
||||
- Slug/claim confusion: use the raw `tbx_...` id, or run one-shot without
|
||||
`--id`.
|
||||
- Bad local config: inspect `.crabbox.yaml`, `crabbox config show`, and
|
||||
`crabbox whoami`; normal OpenClaw proof should use brokered AWS without
|
||||
asking for cloud keys.
|
||||
- Slug/claim confusion: use the raw `cbx_...` / `tbx_...` id, or run one-shot
|
||||
without `--id`.
|
||||
- Sync/timing bug: add `--debug --timing-json`; capture the final JSON and the
|
||||
printed Actions URL. Large sync warnings now include top source directories
|
||||
by file count and a hint to update `.crabboxignore` / `sync.exclude`; inspect
|
||||
those before reaching for `--force-sync-large`.
|
||||
- Cleanup uncertainty: run `blacksmith testbox list` and stop only boxes you
|
||||
those before reaching for `--force-sync-large`. Quiet rsync watchdogs and SSH
|
||||
timeouts now print `next_action=` hints; follow them, usually `--full-resync`
|
||||
first and a fresh lease second.
|
||||
- Cleanup uncertainty: run `crabbox list --provider aws`; for explicit
|
||||
Blacksmith runs, use `blacksmith testbox list` and stop only boxes you
|
||||
created.
|
||||
- Testbox queued/capacity pressure: do not convert a broad changed gate or full
|
||||
suite into local `OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm ...`. Leave the
|
||||
remote lane queued, switch to a narrower targeted local check, or stop and
|
||||
report the capacity blocker.
|
||||
- Testbox queued/capacity pressure: do not retry Blacksmith repeatedly. Rerun
|
||||
once without `--provider` so `.crabbox.yaml` routes to brokered AWS, or report
|
||||
the Blacksmith blocker if Testbox itself is the requested proof.
|
||||
|
||||
If Crabbox cannot dispatch, sync, attach, or stop but Blacksmith itself works,
|
||||
first try the same command through the repo wrapper with `--debug` and
|
||||
`--timing-json`:
|
||||
If brokered AWS cannot dispatch, sync, attach, or stop, retry once with
|
||||
`--debug` and `--timing-json`:
|
||||
|
||||
```sh
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox --debug --timing-json -- \
|
||||
pnpm crabbox:run -- --debug --timing-json -- \
|
||||
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed
|
||||
```
|
||||
|
||||
Full suite:
|
||||
|
||||
```sh
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox --debug --timing-json -- \
|
||||
pnpm crabbox:run -- --debug --timing-json -- \
|
||||
CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test
|
||||
```
|
||||
|
||||
@@ -413,9 +448,10 @@ Raw Blacksmith footguns:
|
||||
- Treat `blacksmith testbox list` as cleanup diagnostics, not a shared reusable
|
||||
queue.
|
||||
|
||||
Escalate to owned AWS/Hetzner only when Blacksmith is down, quota-limited,
|
||||
missing the needed environment, or owned capacity is the explicit goal. Use the
|
||||
Owned Cloud Fallback section below.
|
||||
Use Blacksmith only when the task is specifically about Testbox, brokered AWS
|
||||
is unavailable, or an explicit comparison is needed. If Blacksmith is down or
|
||||
quota-limited, do not keep probing it; stay on brokered AWS and note the
|
||||
delegated-provider outage.
|
||||
|
||||
## Blacksmith Backend Notes
|
||||
|
||||
@@ -451,13 +487,14 @@ Important Blacksmith footguns:
|
||||
blacksmith auth login --non-interactive --organization openclaw
|
||||
```
|
||||
|
||||
## Owned Cloud Fallback
|
||||
## Brokered AWS
|
||||
|
||||
Use AWS/Hetzner only when Blacksmith is down, quota-limited, missing the needed
|
||||
environment, or owned capacity is explicitly the goal.
|
||||
Use AWS for normal OpenClaw remote proof. The repo `.crabbox.yaml` already
|
||||
selects brokered AWS, so omit `--provider` unless you are testing a different
|
||||
provider deliberately.
|
||||
|
||||
```sh
|
||||
pnpm crabbox:warmup -- --provider aws --class beast --market on-demand --idle-timeout 90m
|
||||
pnpm crabbox:warmup -- --class beast --market on-demand --idle-timeout 90m
|
||||
pnpm crabbox:hydrate -- --id <cbx_id-or-slug>
|
||||
pnpm crabbox:run -- --id <cbx_id-or-slug> --timing-json --shell -- "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed"
|
||||
pnpm crabbox:stop -- <cbx_id-or-slug>
|
||||
@@ -481,8 +518,8 @@ crabbox whoami
|
||||
- If broker auth is missing, run `crabbox login --url https://crabbox.openclaw.ai --provider aws`.
|
||||
- If the CLI asks for `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, or AWS
|
||||
profile setup during normal OpenClaw validation, assume the agent selected
|
||||
the wrong path. Use brokered `crabbox login`, `--provider blacksmith-testbox`,
|
||||
or an existing brokered lease before asking the user for cloud credentials.
|
||||
the wrong path. Use brokered `crabbox login` or an existing brokered lease
|
||||
before asking the user for cloud credentials.
|
||||
- Ask for AWS keys only for explicit direct-provider/account administration,
|
||||
not for normal brokered OpenClaw proof.
|
||||
- Trusted automation may still use
|
||||
@@ -495,8 +532,7 @@ macOS config lives at:
|
||||
```
|
||||
|
||||
It should include `broker.url`, `broker.token`, and usually `provider: aws`
|
||||
for owned-cloud lanes. Do not let that config override the OpenClaw default
|
||||
when Blacksmith proof is requested; pass `--provider blacksmith-testbox`.
|
||||
for OpenClaw lanes. Let that config drive normal validation.
|
||||
|
||||
### Interactive Desktop / WebVNC
|
||||
|
||||
@@ -516,7 +552,10 @@ crabbox run --id <lease> --shell -- 'DISPLAY=:99 xdotool search --onlyvisible --
|
||||
crabbox status --id <id-or-slug> --wait
|
||||
crabbox inspect --id <id-or-slug> --json
|
||||
crabbox sync-plan
|
||||
crabbox history --limit 20
|
||||
crabbox history --lease <id-or-slug>
|
||||
crabbox attach <run_id>
|
||||
crabbox events <run_id> --json
|
||||
crabbox logs <run_id>
|
||||
crabbox results <run_id>
|
||||
crabbox cache stats --id <id-or-slug>
|
||||
@@ -531,14 +570,15 @@ Use `--market spot|on-demand` only on AWS warmup/one-shot runs.
|
||||
## Failure Triage
|
||||
|
||||
- Crabbox cannot find provider: verify `../crabbox/bin/crabbox --help` lists
|
||||
`blacksmith-testbox`; update Crabbox before falling back.
|
||||
the provider selected by `.crabbox.yaml`; update Crabbox before falling back.
|
||||
- Hydration stuck or failed: open the printed GitHub Actions run URL and inspect
|
||||
the hydration step.
|
||||
- Sync failed: rerun with `--debug`; check changed-file count and whether the
|
||||
checkout is dirty.
|
||||
- Command failed: rerun only the failing shard/file first. Do not rerun a full
|
||||
suite until the focused failure is understood.
|
||||
- Cleanup uncertain: `blacksmith testbox list`; stop owned `tbx_...` leases you
|
||||
- Cleanup uncertain: `crabbox list --provider aws`; for explicit Blacksmith
|
||||
runs, use `blacksmith testbox list` and stop owned `tbx_...` leases you
|
||||
created.
|
||||
- Crabbox broken but Blacksmith works: use the direct Blacksmith fallback above,
|
||||
then file/fix the Crabbox issue.
|
||||
|
||||
@@ -73,8 +73,9 @@ openclaw logs --follow
|
||||
tool execution.
|
||||
- **Worker/dist:** run `pnpm build` when touching workers, dynamic imports,
|
||||
package exports, lazy runtime boundaries, or published paths.
|
||||
- **Live keys:** check local `~/.profile` for key presence/length before saying
|
||||
live proof is blocked. Never print secrets.
|
||||
- **Live keys:** use the configured secret workflow for missing provider keys
|
||||
before saying live proof is blocked. Env checks are presence-only; never print
|
||||
secrets.
|
||||
|
||||
## Code Pointers
|
||||
|
||||
|
||||
@@ -42,16 +42,20 @@ Choose the page type before writing:
|
||||
Use this default topic page structure:
|
||||
|
||||
1. Title: name the major entity or surface.
|
||||
2. Overview: explain what it is, what it owns, and what it does not own.
|
||||
2. Opening overview: start with a few unheaded sentences that explain what it
|
||||
is, what it owns, and what it does not own. Do not add a `## Overview`
|
||||
heading unless the page is itself an overview index.
|
||||
3. Requirements: include only when setup needs specific accounts, versions,
|
||||
permissions, plugins, operating systems, or credentials.
|
||||
4. Quickstart: show the recommended setup path and smallest reliable verification.
|
||||
5. Configuration: show the minimum configuration needed to use the surface,
|
||||
common variants users must choose between, and where each option is set:
|
||||
CLI, config file, environment variable, plugin manifest, dashboard, or API.
|
||||
6. Subtopics: organize the entity's major concepts, workflows, and decisions by
|
||||
reader intent.
|
||||
7. Troubleshooting: diagnose common observable failures.
|
||||
6. Major subtopics: organize the entity's major concepts, workflows, and
|
||||
decisions by reader intent. Put each major subtopic under its own heading;
|
||||
do not wrap them in a generic `## Subtopics` section.
|
||||
7. Troubleshooting: diagnose common observable failures under an explicit
|
||||
`## Troubleshooting` heading.
|
||||
8. Related: link to guides, references, commands, concepts, and adjacent topics.
|
||||
|
||||
Topic pages may be longer than quickstarts, but they should not become exhaustive
|
||||
|
||||
@@ -56,7 +56,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
|
||||
- For unpublished targets, pack the candidate on the host, serve the `.tgz` over the harness HTTP server, and point the guest updater at that served package. Prefer `openclaw update --tag http://<host-ip>:<port>/openclaw-<version>.tgz --yes --json`; when channel persistence also matters, pass `--channel <stable|beta>` and set `OPENCLAW_UPDATE_PACKAGE_SPEC` to the same served URL in the guest update environment. The command under test must still be `openclaw update`, not direct npm.
|
||||
- For unpublished local-fix validation, remember the old baseline updater code still controls the first hop. A fix that lives only in the new updater code cannot change that already-running old process; the served candidate must either keep package/plugin metadata compatible with the baseline host or the baseline itself must include the updater fix.
|
||||
- For beta/stable verification, resolve the tag immediately before the run (`npm view openclaw@beta version dist.tarball` or `npm view openclaw@latest ...`). Tags can move while a long VM matrix is already running; restart the matrix when the intended prerelease appears after an earlier registry 404/tag-lag check.
|
||||
- Source Peter's profile in the host shell (`set -a; source "$HOME/.profile"; set +a`) before OpenAI/Anthropic lanes. Do not print profile contents or env dumps; pass provider secrets through the guest exec environment.
|
||||
- Use the configured secret workflow to inject only the provider keys needed by OpenAI/Anthropic lanes. Do not print secrets or env dumps; pass provider secrets through the guest exec environment.
|
||||
- Same-guest update verification should set the default model explicitly to `openai/gpt-5.4` before the agent turn and use a fresh explicit `--session-id` so old session model state does not leak into the check.
|
||||
- The aggregate npm-update wrapper must resolve the Linux VM with the same Ubuntu fallback policy as `parallels-linux-smoke.sh` before both fresh and update lanes. Treat any Ubuntu guest with major version `>= 24` as acceptable when the exact default VM is missing, preferring the closest version match. On Peter's current host today, missing `Ubuntu 24.04.3 ARM64` should fall back to `Ubuntu 25.10`.
|
||||
- On macOS same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; launchd can otherwise report a loaded service while the old process has exited and the fresh process is not RPC-ready yet.
|
||||
|
||||
@@ -138,7 +138,9 @@ Output only qualifying candidates, with: ref, surface, proof, cause, fix sketch,
|
||||
|
||||
- Start every PR review with 1-3 plain sentences explaining what the change does and why it matters. Put this before `Findings`.
|
||||
- Then list findings first. If none, say `No blocking findings` or `No findings`.
|
||||
- Always answer: bug/behavior being fixed, PR/issue URL and affected surface, and best-fix verdict.
|
||||
- Always answer: bug/behavior being fixed, PR/issue URL and affected surface, provenance for regressions when traceable, and best-fix verdict.
|
||||
- For bug/regression fixes, include a compact `Provenance:` line after cause/root-cause when a bounded history pass can identify it. Use `git log -S/-G`, `git blame`, linked PRs/issues, and tests; separate author, committer/merger, and current PR author when they differ.
|
||||
- Phrase provenance as `introduced by`, `made visible by`, or `carried forward by`, with confidence (`clear`, `likely`, `unknown`). If unclear, say what evidence is missing instead of guessing. For features, docs, and refactors, use `Provenance: N/A` or omit it when no broken behavior is being fixed.
|
||||
- Keep summaries compact, but include enough proof that the verdict is auditable without rereading the PR.
|
||||
|
||||
## Read beyond the diff
|
||||
@@ -160,8 +162,9 @@ Output only qualifying candidates, with: ref, surface, proof, cause, fix sketch,
|
||||
- Before landing, require:
|
||||
1. symptom evidence such as a repro, logs, or a failing test
|
||||
2. a verified root cause in code with file/line
|
||||
3. a fix that touches the implicated code path
|
||||
4. a regression test when feasible, or explicit manual verification plus a reason no test was added
|
||||
3. provenance for regressions when traceable by bounded git/PR history
|
||||
4. a fix that touches the implicated code path
|
||||
5. a regression test when feasible, or explicit manual verification plus a reason no test was added
|
||||
- If the claim is unsubstantiated or likely wrong, request evidence or changes instead of merging.
|
||||
- If the linked issue appears outdated or incorrect, correct triage first. Do not merge a speculative fix.
|
||||
- If Crabbox/E2E proof is blocked, say exactly why and use the closest available
|
||||
|
||||
@@ -227,7 +227,9 @@ pnpm openclaw qa manual \
|
||||
- Treat the concrete Codex model name as user/config input; do not hardcode it in source, docs examples, or scenarios.
|
||||
- Live QA preserves `CODEX_HOME` so Codex CLI auth/config works while keeping `HOME` and `OPENCLAW_HOME` sandboxed.
|
||||
- Mock QA should scrub `CODEX_HOME`.
|
||||
- If Codex returns fallback/auth text every turn, first check `CODEX_HOME`, `~/.profile`, and gateway child logs before changing scenario assertions.
|
||||
- If Codex returns fallback/auth text every turn, first check `CODEX_HOME`,
|
||||
relevant secret-backed auth, and gateway child logs before changing
|
||||
scenario assertions.
|
||||
- For model comparison, include `codex-cli/<codex-model>` as another candidate in `qa character-eval`; the report should label it as an opaque model name.
|
||||
|
||||
## Repo facts
|
||||
|
||||
90
.agents/skills/openclaw-release-ci/SKILL.md
Normal file
90
.agents/skills/openclaw-release-ci/SKILL.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: openclaw-release-ci
|
||||
description: "Run, watch, debug, and summarize OpenClaw full release CI, release checks, live provider gates, install/update proofs, and release-secret preflights."
|
||||
---
|
||||
|
||||
# OpenClaw Release CI
|
||||
|
||||
Use this with `$openclaw-release-maintainer` and `$openclaw-testing` when a release candidate needs full validation, install/update proof, live provider checks, or CI recovery.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- No version bump, tag, npm publish, GitHub release, or release promotion without explicit operator approval.
|
||||
- Validate provider secrets before dispatching expensive full release matrices.
|
||||
- Do not set GitHub secrets from unvalidated 1Password candidates. If a candidate returns 401/403, leave the existing secret alone and report the exact missing provider.
|
||||
- Use `$one-password` for secret reads/writes: one persistent tmux session, targeted items only, no secret output.
|
||||
- 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.
|
||||
|
||||
## Preflight
|
||||
|
||||
Before full release validation:
|
||||
|
||||
```bash
|
||||
node .agents/skills/openclaw-release-ci/scripts/verify-provider-secrets.mjs --required openai,anthropic,fireworks
|
||||
gh api rate_limit --jq '.resources.core'
|
||||
git status --short --branch
|
||||
git rev-parse HEAD
|
||||
```
|
||||
|
||||
If env lacks keys, use `$one-password` to inject or set them, then rerun the script. The script prints only provider status and HTTP class, never tokens.
|
||||
|
||||
## Dispatch
|
||||
|
||||
Prefer the trusted workflow on `main`, target the exact release SHA:
|
||||
|
||||
```bash
|
||||
gh workflow run full-release-validation.yml \
|
||||
--repo openclaw/openclaw \
|
||||
--ref main \
|
||||
-f ref=<release-sha> \
|
||||
-f provider=openai \
|
||||
-f mode=both \
|
||||
-f release_profile=full \
|
||||
-f rerun_group=all
|
||||
```
|
||||
|
||||
Use `release_profile=stable` unless the operator explicitly asks for the broad advisory provider/media matrix. Use narrow `rerun_group` after focused fixes.
|
||||
|
||||
## Watch
|
||||
|
||||
Use the summary helper instead of repeated raw polling:
|
||||
|
||||
```bash
|
||||
node .agents/skills/openclaw-release-ci/scripts/release-ci-summary.mjs <full-release-run-id>
|
||||
```
|
||||
|
||||
Then watch only when useful:
|
||||
|
||||
```bash
|
||||
gh run watch <full-release-run-id> --repo openclaw/openclaw --exit-status
|
||||
```
|
||||
|
||||
Stop watchers before ending the turn or switching strategy.
|
||||
|
||||
## Failure Triage
|
||||
|
||||
1. Confirm parent SHA and child run IDs.
|
||||
2. List failed jobs only:
|
||||
```bash
|
||||
gh run view <child-run-id> --repo openclaw/openclaw --json jobs \
|
||||
--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.
|
||||
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.
|
||||
|
||||
## Evidence
|
||||
|
||||
Record:
|
||||
|
||||
- release SHA
|
||||
- full parent run URL
|
||||
- child run IDs and conclusions: CI, Release Checks, Plugin Prerelease, NPM Telegram
|
||||
- targeted local proof commands
|
||||
- provider-secret preflight result
|
||||
- known gaps or unrelated failures
|
||||
|
||||
For lessons and recovery patterns, read `references/release-ci-notes.md`.
|
||||
4
.agents/skills/openclaw-release-ci/agents/openai.yaml
Normal file
4
.agents/skills/openclaw-release-ci/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "OpenClaw Release CI"
|
||||
short_description: "Verify and debug OpenClaw release validation runs"
|
||||
default_prompt: "Use $openclaw-release-ci to preflight provider secrets, watch full release validation, summarize child runs, and triage only failing release lanes."
|
||||
@@ -0,0 +1,41 @@
|
||||
# Release CI Notes
|
||||
|
||||
## What Went Wrong
|
||||
|
||||
- Full validation was started before all provider keys were proven valid.
|
||||
- GitHub secret presence was confused with key validity.
|
||||
- Repeated `gh run view` and log fetches exhausted REST quota.
|
||||
- Parent run state was less useful than child run evidence.
|
||||
- Live-cache failures needed structured classification: invalid key, empty provider output, timeout, or real cache regression.
|
||||
- Background watchers accumulated and made interruption recovery harder.
|
||||
|
||||
## Better Defaults
|
||||
|
||||
- Run provider-secret preflight first. Require real `/models` or equivalent endpoint checks for release-blocking providers.
|
||||
- Keep one watcher open. Use child summaries every few minutes, not every few seconds.
|
||||
- Fetch failed-job logs only after a job reaches a terminal failing state.
|
||||
- Prefer narrow `rerun_group` recovery after a focused fix.
|
||||
- Leave bad secrets unset. A 401 candidate from 1Password should not overwrite GitHub.
|
||||
- Make the final release evidence note durable: parent URL, child run URLs, SHA, command proof, and gaps.
|
||||
|
||||
## Secret Handling Pattern
|
||||
|
||||
- Use `$one-password`; never run broad env dumps.
|
||||
- Search exact item titles or known ids.
|
||||
- Validate candidates without printing values.
|
||||
- Set GitHub secrets only after endpoint validation succeeds.
|
||||
- After setting, verify metadata with `gh secret list`, not value output.
|
||||
|
||||
## Live Cache Pattern
|
||||
|
||||
- Empty text with token usage is a provider/output issue until proven otherwise.
|
||||
- Retry lane-level mismatches once with a fresh session id.
|
||||
- Keep cache baselines strict, but log enough structured usage to distinguish cache miss from response mismatch.
|
||||
- If a provider key validates locally but fails in Actions, inspect whether the workflow reads the expected secret name.
|
||||
|
||||
## Quota-Safe GitHub Pattern
|
||||
|
||||
- Check `gh api rate_limit --jq '.resources.core'` before log-heavy work.
|
||||
- Use one child-run listing call, then inspect failed jobs only.
|
||||
- If remaining quota is low, pause until reset; do not keep polling.
|
||||
- Prefer GraphQL only for metadata when REST is exhausted; logs still need REST.
|
||||
79
.agents/skills/openclaw-release-ci/scripts/release-ci-summary.mjs
Executable file
79
.agents/skills/openclaw-release-ci/scripts/release-ci-summary.mjs
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync } from "node:child_process";
|
||||
import process from "node:process";
|
||||
|
||||
const runId = process.argv[2];
|
||||
const repo = process.env.OPENCLAW_RELEASE_REPO || "openclaw/openclaw";
|
||||
|
||||
if (!runId) {
|
||||
console.error("usage: release-ci-summary.mjs <full-release-run-id>");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function gh(args) {
|
||||
return execFileSync("gh", args, {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
}
|
||||
|
||||
function jsonGh(args) {
|
||||
return JSON.parse(gh(args));
|
||||
}
|
||||
|
||||
function rate() {
|
||||
try {
|
||||
return jsonGh(["api", "rate_limit"]).resources.core;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const core = rate();
|
||||
if (core) {
|
||||
const reset = new Date(core.reset * 1000).toISOString();
|
||||
console.log(`rate: remaining=${core.remaining}/${core.limit} reset=${reset}`);
|
||||
if (core.remaining < 20) {
|
||||
console.error("rate too low for CI summary; wait for reset before polling");
|
||||
process.exit(3);
|
||||
}
|
||||
}
|
||||
|
||||
const parent = jsonGh([
|
||||
"run",
|
||||
"view",
|
||||
runId,
|
||||
"--repo",
|
||||
repo,
|
||||
"--json",
|
||||
"status,conclusion,createdAt,headSha,url,jobs",
|
||||
]);
|
||||
|
||||
console.log(`parent: ${runId} ${parent.status}/${parent.conclusion || "none"}`);
|
||||
console.log(`sha: ${parent.headSha}`);
|
||||
console.log(`url: ${parent.url}`);
|
||||
|
||||
for (const job of parent.jobs ?? []) {
|
||||
const marker = job.conclusion || job.status;
|
||||
console.log(`parent-job: ${marker} ${job.name}`);
|
||||
}
|
||||
|
||||
const since = parent.createdAt;
|
||||
const runList = gh([
|
||||
"api",
|
||||
`repos/${repo}/actions/runs?per_page=100`,
|
||||
"--jq",
|
||||
`.workflow_runs[] | select(.created_at >= "${since}") | select(.name=="CI" or .name=="OpenClaw Release Checks" or .name=="Plugin Prerelease" or .name=="NPM Telegram Beta E2E" or .name=="Full Release Validation") | [.id,.name,.status,.conclusion,.head_sha,.html_url] | @tsv`,
|
||||
]).trim();
|
||||
|
||||
if (!runList) {
|
||||
console.log("children: none found yet");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log("children:");
|
||||
for (const line of runList.split("\n")) {
|
||||
const [id, name, status, conclusion, sha, url] = line.split("\t");
|
||||
console.log(`child: ${id} ${name} ${status}/${conclusion || "none"} sha=${sha}`);
|
||||
console.log(`child-url: ${url}`);
|
||||
}
|
||||
113
.agents/skills/openclaw-release-ci/scripts/verify-provider-secrets.mjs
Executable file
113
.agents/skills/openclaw-release-ci/scripts/verify-provider-secrets.mjs
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env node
|
||||
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;
|
||||
const [key, inlineValue] = arg.slice(2).split("=", 2);
|
||||
const value = inlineValue ?? process.argv[index + 1];
|
||||
if (inlineValue === undefined) index += 1;
|
||||
args.set(key, value);
|
||||
}
|
||||
|
||||
const requiredInput = String(args.get("required") ?? "openai,anthropic").trim();
|
||||
const required = new Set(
|
||||
(requiredInput.toLowerCase() === "none" ? "" : requiredInput)
|
||||
.split(",")
|
||||
.map((entry) => entry.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
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 };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function checkProvider(id, config) {
|
||||
const secret = envFirst(config.env);
|
||||
if (!secret) {
|
||||
return { id, ok: false, status: "missing", env: config.env.join("|") };
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const headers = config.headers(secret.value);
|
||||
const response = await fetch(config.url, {
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
return {
|
||||
id,
|
||||
ok: response.ok,
|
||||
status: response.ok ? "ok" : `http_${response.status}`,
|
||||
env: secret.name,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
id,
|
||||
ok: false,
|
||||
status: error?.name === "AbortError" ? "timeout" : "error",
|
||||
env: secret.name,
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
const providers = {
|
||||
openai: {
|
||||
env: ["OPENAI_API_KEY"],
|
||||
url: "https://api.openai.com/v1/models",
|
||||
headers: (token) => ({ authorization: `Bearer ${token}` }),
|
||||
},
|
||||
anthropic: {
|
||||
env: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_TOKEN"],
|
||||
url: "https://api.anthropic.com/v1/models",
|
||||
headers: (token) => ({
|
||||
"anthropic-version": "2023-06-01",
|
||||
"x-api-key": token,
|
||||
}),
|
||||
},
|
||||
fireworks: {
|
||||
env: ["FIREWORKS_API_KEY"],
|
||||
url: "https://api.fireworks.ai/inference/v1/models",
|
||||
headers: (token) => ({ authorization: `Bearer ${token}` }),
|
||||
},
|
||||
openrouter: {
|
||||
env: ["OPENROUTER_API_KEY"],
|
||||
url: "https://openrouter.ai/api/v1/models",
|
||||
headers: (token) => ({ authorization: `Bearer ${token}` }),
|
||||
},
|
||||
};
|
||||
|
||||
const unknown = [...required].filter((id) => !providers[id]);
|
||||
if (unknown.length > 0) {
|
||||
console.error(`unknown providers: ${unknown.join(",")}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const id of Object.keys(providers)) {
|
||||
if (required.has(id) || envFirst(providers[id].env)) {
|
||||
results.push(await checkProvider(id, providers[id]));
|
||||
}
|
||||
}
|
||||
|
||||
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 (failed) {
|
||||
console.error("release provider secret preflight failed");
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -65,8 +65,8 @@ Use this skill for release and publish-time workflow. Keep ordinary development
|
||||
stable base version section, for example `v2026.4.20-beta.1` uses
|
||||
`## 2026.4.20` release notes.
|
||||
- When any beta or stable release is live, make a best-effort Discord
|
||||
announcement using Peter's bot token from `.profile`; do not block or roll
|
||||
back the release if the announcement fails.
|
||||
announcement using the configured secret workflow; do not block or roll back
|
||||
the release if the announcement fails.
|
||||
- When asked to announce on X, use `~/Projects/bird/bird` and follow the
|
||||
release tweet style below.
|
||||
|
||||
@@ -288,13 +288,11 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
## Check all relevant release builds
|
||||
|
||||
- Always validate the OpenClaw npm release path before creating the tag.
|
||||
- Source Peter's profile before live release validation so OpenAI and Anthropic
|
||||
credentials are available without printing secrets:
|
||||
`set -a; source "$HOME/.profile"; set +a`.
|
||||
- Use the configured secret workflow before live release validation so OpenAI
|
||||
and Anthropic credentials are available without printing secrets.
|
||||
- Parallels validation and any local live model QA for this train must use both
|
||||
`OPENAI_API_KEY` and `ANTHROPIC_API_KEY`. If either is missing after sourcing
|
||||
`.profile`, stop before starting those local long lanes and report the
|
||||
missing key.
|
||||
`OPENAI_API_KEY` and `ANTHROPIC_API_KEY`. If either cannot be injected, stop
|
||||
before starting those local long lanes and report the missing key.
|
||||
- Live credentialed channel QA is the GitHub Actions workflow
|
||||
`QA-Lab - All Lanes` (`.github/workflows/qa-live-telegram-convex.yml`), not a
|
||||
local substitute. Dispatch it from Actions against the release tag and wait
|
||||
@@ -592,8 +590,7 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
If a pre-npm lane fails before any tag/package leaves the machine, fix and
|
||||
rerun the same intended beta attempt. Repeat up to the operator's
|
||||
authorized beta-attempt limit, normally 4.
|
||||
24. Announce the beta/stable release on Discord best-effort using Peter's bot
|
||||
token from `.profile`.
|
||||
24. Announce the beta/stable release on Discord best-effort using the configured secret workflow.
|
||||
25. If the operator requested beta only, stop after beta verification and the
|
||||
announcement.
|
||||
26. If the stable release was published to `beta`, use the light stable
|
||||
|
||||
@@ -581,6 +581,8 @@ function cmdNotify(target, author, locationType, secretTypes, replyToNodeId) {
|
||||
}
|
||||
|
||||
const body = [
|
||||
`> **Note:** This is an automated message sent by the OpenClaw maintainer team. **NO_REPLY.**`,
|
||||
"",
|
||||
`@${author} :warning: **Security Notice: Secret Leakage Detected**`,
|
||||
"",
|
||||
`GitHub Secret Scanning detected the following exposed secret types in ${locationDesc}:`,
|
||||
|
||||
@@ -19,9 +19,13 @@ or validating a change without wasting hours.
|
||||
Prove the touched surface first. Do not reflexively run the whole suite.
|
||||
|
||||
1. Inspect the diff and classify the touched surface:
|
||||
- source: `pnpm changed:lanes --json`, then `pnpm check:changed`
|
||||
- tests only: `pnpm test:changed`
|
||||
- one failing file: `pnpm test <path-or-filter> -- --reporter=verbose`
|
||||
- normal source checkout, source change: `pnpm changed:lanes --json`, then `pnpm check:changed`
|
||||
- normal source checkout, tests only: `pnpm test:changed`
|
||||
- normal source checkout, one failing file: `pnpm test <path-or-filter> -- --reporter=verbose`
|
||||
- Codex worktree or linked/sparse checkout, one/few explicit files: `node scripts/run-vitest.mjs <path-or-filter>`
|
||||
- Codex worktree or linked/sparse checkout, changed gates or anything broad:
|
||||
`node scripts/crabbox-wrapper.mjs run ... --shell -- "pnpm check:changed"`
|
||||
and let `.crabbox.yaml` choose the provider
|
||||
- workflow-only: `git diff --check`, workflow syntax/lint (`actionlint` when available)
|
||||
- docs-only: `pnpm docs:list`, docs formatter/lint only if docs tooling changed or requested
|
||||
2. Reproduce narrowly before fixing.
|
||||
@@ -36,11 +40,19 @@ Prove the touched surface first. Do not reflexively run the whole suite.
|
||||
- Prefer GitHub Actions for release/Docker proof when the workflow already has the prepared image and secrets.
|
||||
- Use `scripts/committer "<msg>" <paths...>` when committing; stage only your files.
|
||||
- If deps are missing, run `pnpm install`, retry once, then report the first actionable error.
|
||||
- For Blacksmith Testbox proof, use Crabbox first. `pnpm crabbox:run -- --provider
|
||||
blacksmith-testbox --timing-json -- <command...>` warms, claims, syncs, runs,
|
||||
reports, and cleans up one-shot boxes. Reuse only an id/slug created in this
|
||||
operator session; `blacksmith testbox list` is diagnostics only, not a shared
|
||||
work queue.
|
||||
- In a Codex worktree or linked/sparse checkout, do not run direct local
|
||||
`pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, or `scripts/committer` until
|
||||
you have verified pnpm will not reconcile or reinstall dependencies. Use
|
||||
`node scripts/run-vitest.mjs` for tiny local proof, `node
|
||||
scripts/crabbox-wrapper.mjs` for Testbox, and `git commit --no-verify` only
|
||||
after the relevant remote or node-wrapper proof is already clean.
|
||||
- For remote proof, use Crabbox first and omit `--provider` unless a specific
|
||||
provider is being tested. The repo Crabbox config routes normal broad proof to
|
||||
brokered AWS. Blacksmith Testbox is explicit opt-in; if it queues, fails
|
||||
capacity, or cannot allocate, retry once through the default Crabbox route or
|
||||
report the Testbox blocker. Reuse only an id/slug created in this operator
|
||||
session; `blacksmith testbox list` is diagnostics only, not a shared work
|
||||
queue.
|
||||
|
||||
## Local Test Shortcuts
|
||||
|
||||
@@ -55,6 +67,14 @@ OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test <path-or-filter>
|
||||
|
||||
Use targeted file paths whenever possible. Avoid raw `vitest`; use the repo
|
||||
`pnpm test` wrapper so project routing, workers, and setup stay correct.
|
||||
When the checkout is a Codex worktree, prefer the direct node harness instead:
|
||||
|
||||
```bash
|
||||
node scripts/run-vitest.mjs <path-or-filter>
|
||||
```
|
||||
|
||||
That keeps the test scoped without giving pnpm a chance to run dependency
|
||||
status checks or install reconciliation in a linked worktree.
|
||||
|
||||
## Command Semantics
|
||||
|
||||
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -11,6 +11,8 @@
|
||||
/.github/workflows/codeql.yml @openclaw/openclaw-secops
|
||||
/.github/workflows/codeql-android-critical-security.yml @openclaw/openclaw-secops
|
||||
/.github/workflows/codeql-critical-quality.yml @openclaw/openclaw-secops
|
||||
/.github/workflows/dependency-change-awareness.yml @openclaw/openclaw-secops
|
||||
/test/scripts/dependency-change-awareness-workflow.test.ts @openclaw/openclaw-secops
|
||||
/src/security/ @openclaw/openclaw-secops
|
||||
/src/secrets/ @openclaw/openclaw-secops
|
||||
/src/config/*secret*.ts @openclaw/openclaw-secops
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
You are Mantis running native Telegram Desktop visual proof for an OpenClaw PR.
|
||||
|
||||
Goal: inspect the pull request, decide the best Telegram-visible behavior to
|
||||
prove, run before/after native Telegram Desktop sessions, iterate until the GIFs
|
||||
are visually good, and leave a Mantis evidence manifest for the workflow to
|
||||
publish.
|
||||
Goal: inspect the pull request, decide whether it has an honest
|
||||
Telegram-visible before/after behavior, then either run native Telegram Desktop
|
||||
proof or leave a no-visual-proof manifest for the workflow to publish.
|
||||
|
||||
Hard limits:
|
||||
|
||||
@@ -16,6 +15,9 @@ Hard limits:
|
||||
- Do not use fixed `/status` proof unless it genuinely proves the PR.
|
||||
- Do not finish with tiny, cropped-wrong, off-bottom, or sidebar-heavy GIFs.
|
||||
- Do not invent a generic proof. The proof must match the PR behavior.
|
||||
- Do not force GIFs for internal-only, workflow-only, test-only, docs-only, or
|
||||
otherwise non-visual PRs. A no-visual-proof manifest is a successful outcome
|
||||
when GIFs would be misleading.
|
||||
|
||||
Inputs are provided as environment variables:
|
||||
|
||||
@@ -36,10 +38,45 @@ Required workflow:
|
||||
1. Read `.agents/skills/telegram-crabbox-e2e-proof/SKILL.md`.
|
||||
2. Inspect the PR with `gh pr view "$MANTIS_PR_NUMBER"` and
|
||||
`gh pr diff "$MANTIS_PR_NUMBER"`.
|
||||
3. Decide what Telegram message, mock model response, command, callback, button,
|
||||
3. Decide whether the PR has a visibly reproducible Telegram Desktop
|
||||
before/after. If it does not, write
|
||||
`${MANTIS_OUTPUT_DIR}/mantis-evidence.json` with `comparison.pass: true`, no
|
||||
artifacts, and a summary that starts with
|
||||
`Mantis did not generate before/after GIFs because`. Include the concrete
|
||||
reason in the summary. Use this manifest shape and do not create worktrees
|
||||
or start Crabbox for this case:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "telegram-desktop-proof",
|
||||
"title": "Mantis Telegram Desktop Proof",
|
||||
"summary": "Mantis did not generate before/after GIFs because <reason>.",
|
||||
"scenario": "telegram-desktop-proof",
|
||||
"comparison": {
|
||||
"baseline": {
|
||||
"ref": "<BASELINE_REF>",
|
||||
"sha": "<BASELINE_SHA>",
|
||||
"expected": "no visible Telegram Desktop delta",
|
||||
"status": "skipped"
|
||||
},
|
||||
"candidate": {
|
||||
"ref": "<CANDIDATE_REF>",
|
||||
"sha": "<CANDIDATE_SHA>",
|
||||
"expected": "no visible Telegram Desktop delta",
|
||||
"status": "skipped",
|
||||
"fixed": true
|
||||
},
|
||||
"pass": true
|
||||
},
|
||||
"artifacts": []
|
||||
}
|
||||
```
|
||||
|
||||
4. Decide what Telegram message, mock model response, command, callback, button,
|
||||
media, or sequence best proves the PR. Use `MANTIS_INSTRUCTIONS` as extra
|
||||
maintainer guidance, not as a replacement for reading the PR.
|
||||
4. Create detached worktrees under
|
||||
5. Create detached worktrees under
|
||||
`.artifacts/qa-e2e/mantis/telegram-desktop-proof-worktrees/baseline` and
|
||||
`.artifacts/qa-e2e/mantis/telegram-desktop-proof-worktrees/candidate`, then
|
||||
install and build each worktree with the repo's normal `pnpm` commands.
|
||||
@@ -49,7 +86,7 @@ Required workflow:
|
||||
runtime commands. The candidate SUT may receive only the proof runner's
|
||||
short-lived Telegram bot token, generated local config/state paths, and mock
|
||||
model key needed for this isolated proof.
|
||||
5. In each worktree, run the real-user Telegram Crabbox proof flow from the
|
||||
6. In each worktree, run the real-user Telegram Crabbox proof flow from the
|
||||
skill with `$OPENCLAW_TELEGRAM_USER_PROOF_CMD`; do not run
|
||||
`pnpm qa:telegram-user:crabbox` directly. The proof command comes from the
|
||||
trusted workflow checkout while the current directory controls which
|
||||
@@ -59,11 +96,11 @@ Required workflow:
|
||||
install, or patch replacement proof tooling during the run. Use the same
|
||||
proof idea for baseline and candidate. You may iterate and rerun if the
|
||||
visual result is not convincing.
|
||||
6. Open Telegram Desktop directly to the newest relevant message with the
|
||||
7. Open Telegram Desktop directly to the newest relevant message with the
|
||||
runner `view` command before finishing each recording. Keep the chat scrolled
|
||||
to the bottom so new proof messages appear in-frame.
|
||||
7. Finish each session with `--preview-crop telegram-window`.
|
||||
8. Build `${MANTIS_OUTPUT_DIR}/mantis-evidence.json` with:
|
||||
8. Finish each session with `--preview-crop telegram-window`.
|
||||
9. Build `${MANTIS_OUTPUT_DIR}/mantis-evidence.json` with:
|
||||
|
||||
```bash
|
||||
node scripts/mantis/build-telegram-desktop-proof-evidence.mjs \
|
||||
@@ -93,6 +130,8 @@ Visual acceptance:
|
||||
Expected final state:
|
||||
|
||||
- `${MANTIS_OUTPUT_DIR}/mantis-evidence.json` exists.
|
||||
- The manifest contains paired `motionPreview` artifacts labeled `Main` and
|
||||
`This PR`.
|
||||
- Visual proof manifests contain paired `motionPreview` artifacts labeled
|
||||
`Main` and `This PR`.
|
||||
- No-visual-proof manifests contain no artifacts and have `comparison.pass:
|
||||
true`.
|
||||
- The worktree can be dirty only under `.artifacts/`.
|
||||
|
||||
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
@@ -244,6 +244,10 @@
|
||||
- "docs/gateway/security.md"
|
||||
- "security/**"
|
||||
|
||||
"extensions: admin-http-rpc":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/admin-http-rpc/**"
|
||||
"extensions: copilot-proxy":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
1
.github/workflows/ci-check-testbox.yml
vendored
1
.github/workflows/ci-check-testbox.yml
vendored
@@ -124,5 +124,6 @@ jobs:
|
||||
- name: Run Testbox
|
||||
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
@@ -452,7 +452,7 @@ jobs:
|
||||
contents: read
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_build_artifacts == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
channels-result: ${{ steps.built_artifact_checks.outputs['channels-result'] }}
|
||||
@@ -655,7 +655,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_fast_core == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -750,7 +750,7 @@ jobs:
|
||||
name: ${{ matrix.checkName }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_plugin_contracts_shards == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -856,7 +856,7 @@ jobs:
|
||||
name: ${{ matrix.checkName }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_fast == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1059,7 +1059,7 @@ jobs:
|
||||
name: checks-node-compat-node22
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1136,7 +1136,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_node_core_nondist == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && (matrix.runner || 'ubuntu-24.04') || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'ubuntu-24.04') || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1304,7 +1304,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && matrix.runner || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && matrix.runner || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1398,6 +1398,7 @@ jobs:
|
||||
pnpm tool-display:check
|
||||
pnpm check:host-env-policy:swift
|
||||
pnpm dup:check:coverage
|
||||
pnpm deps:patches:check
|
||||
;;
|
||||
prod-types)
|
||||
pnpm tsgo:prod
|
||||
@@ -1465,7 +1466,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1794,7 +1795,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_windows == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-windows-2025' || 'windows-2025' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'windows-2025' || (github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-windows-2025' || 'windows-2025') }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
@@ -1907,7 +1908,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_macos_node == 'true' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-6vcpu-macos-latest' || 'macos-latest' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-latest' || (github.repository == 'openclaw/openclaw' && 'blacksmith-6vcpu-macos-latest' || 'macos-latest') }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1951,7 +1952,7 @@ jobs:
|
||||
name: "macos-swift"
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_macos_swift == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || 'macos-latest' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || 'macos-26') }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -2048,7 +2049,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_android_job == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -137,8 +137,10 @@ jobs:
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENCLAW_CONTROL_UI_I18N_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
|
||||
OPENCLAW_CONTROL_UI_I18N_PROVIDER: ${{ secrets.ANTHROPIC_API_KEY != '' && 'anthropic' || 'openai' }}
|
||||
OPENCLAW_CONTROL_UI_I18N_MODEL: ${{ secrets.ANTHROPIC_API_KEY != '' && 'claude-opus-4-6' || vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
|
||||
OPENCLAW_CONTROL_UI_I18N_THINKING: low
|
||||
OPENCLAW_CONTROL_UI_I18N_AUTH_OPTIONAL: "1"
|
||||
LOCALE: ${{ matrix.locale }}
|
||||
run: node --import tsx scripts/control-ui-i18n.ts sync --locale "${LOCALE}" --write
|
||||
|
||||
|
||||
171
.github/workflows/dependency-change-awareness.yml
vendored
Normal file
171
.github/workflows/dependency-change-awareness.yml
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
name: Dependency Change Awareness
|
||||
|
||||
on:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] metadata-only workflow; no checkout or untrusted code execution
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: dependency-change-awareness-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
dependency-change-awareness:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Label and comment on dependency changes
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
const marker = "<!-- openclaw:dependency-change-awareness -->";
|
||||
const labelName = "dependencies-changed";
|
||||
const maxListedFiles = 25;
|
||||
const pullRequest = context.payload.pull_request;
|
||||
|
||||
if (!pullRequest) {
|
||||
core.info("No pull_request payload found; skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
const isDependencyFile = (filename) =>
|
||||
filename === "package.json" ||
|
||||
filename === "pnpm-lock.yaml" ||
|
||||
filename === "pnpm-workspace.yaml" ||
|
||||
filename === "ui/package.json" ||
|
||||
filename.startsWith("patches/") ||
|
||||
/^packages\/[^/]+\/package\.json$/u.test(filename) ||
|
||||
/^extensions\/[^/]+\/package\.json$/u.test(filename);
|
||||
|
||||
const sanitizeDisplayValue = (value) =>
|
||||
String(value)
|
||||
.replace(/[\u0000-\u001f\u007f]/gu, "?")
|
||||
.slice(0, 240);
|
||||
const markdownCode = (value) =>
|
||||
`\`${sanitizeDisplayValue(value).replaceAll("`", "\\`")}\``;
|
||||
const ignoreUnavailableWritePermission = (action) => (error) => {
|
||||
if (error?.status === 403) {
|
||||
core.warning(
|
||||
`Skipping dependency change ${action}; token does not have issue write permission.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (error?.status === 404 || error?.status === 422) {
|
||||
core.warning(`Dependency change ${action} is unavailable.`);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
};
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pullRequest.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const dependencyFiles = files
|
||||
.map((file) => file.filename)
|
||||
.filter((filename) => typeof filename === "string" && isDependencyFile(filename))
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const existingComment = comments.find(
|
||||
(comment) =>
|
||||
comment.user?.login === "github-actions[bot]" && comment.body?.includes(marker),
|
||||
);
|
||||
|
||||
const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const hasLabel = labels.some((label) => label.name === labelName);
|
||||
|
||||
if (dependencyFiles.length === 0) {
|
||||
if (hasLabel) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
name: labelName,
|
||||
}).catch(ignoreUnavailableWritePermission("label removal"));
|
||||
}
|
||||
if (existingComment) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existingComment.id,
|
||||
}).catch(ignoreUnavailableWritePermission("comment deletion"));
|
||||
}
|
||||
await core.summary
|
||||
.addHeading("Dependency Change Awareness")
|
||||
.addRaw("No dependency-related file changes detected.")
|
||||
.write();
|
||||
core.info("No dependency-related file changes detected.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
labels: [labelName],
|
||||
}).catch(ignoreUnavailableWritePermission(`label "${labelName}" update`));
|
||||
}
|
||||
|
||||
const listedFiles = dependencyFiles.slice(0, maxListedFiles);
|
||||
const omittedCount = dependencyFiles.length - listedFiles.length;
|
||||
const fileLines = listedFiles.map((filename) => `- ${markdownCode(filename)}`);
|
||||
if (omittedCount > 0) {
|
||||
fileLines.push(`- ${omittedCount} additional dependency-related files not shown`);
|
||||
}
|
||||
|
||||
const body = [
|
||||
marker,
|
||||
"",
|
||||
"### Dependency Changes Detected",
|
||||
"",
|
||||
"This PR changes dependency-related files. Maintainers should confirm these changes are intentional.",
|
||||
"",
|
||||
"Changed files:",
|
||||
...fileLines,
|
||||
"",
|
||||
"Maintainer follow-up:",
|
||||
"- Review whether the dependency changes are intentional.",
|
||||
"- Inspect resolved package deltas when lockfile or workspace dependency policy changes are present.",
|
||||
"- Run `pnpm deps:changes:report -- --base-ref origin/main --markdown /tmp/dependency-changes.md --json /tmp/dependency-changes.json` locally for detailed release-style evidence.",
|
||||
].join("\n");
|
||||
|
||||
if (existingComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existingComment.id,
|
||||
body,
|
||||
}).catch(ignoreUnavailableWritePermission("comment update"));
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
body,
|
||||
}).catch(ignoreUnavailableWritePermission("comment creation"));
|
||||
}
|
||||
|
||||
await core.summary
|
||||
.addHeading("Dependency Change Awareness")
|
||||
.addRaw(`Detected ${dependencyFiles.length} dependency-related file change(s).`)
|
||||
.addList(dependencyFiles.map((filename) => markdownCode(filename)))
|
||||
.write();
|
||||
core.notice(`Detected ${dependencyFiles.length} dependency-related file change(s).`);
|
||||
18
.github/workflows/docs-sync-publish.yml
vendored
18
.github/workflows/docs-sync-publish.yml
vendored
@@ -16,29 +16,37 @@ permissions:
|
||||
jobs:
|
||||
sync-publish-repo:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
OPENCLAW_DOCS_SYNC_TOKEN: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN }}
|
||||
steps:
|
||||
- name: Skip publish sync without token
|
||||
if: env.OPENCLAW_DOCS_SYNC_TOKEN == ''
|
||||
run: echo "OPENCLAW_DOCS_SYNC_TOKEN is not configured; skipping docs publish repo sync."
|
||||
|
||||
- name: Checkout source repo
|
||||
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout ClawHub docs source
|
||||
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: openclaw/clawhub
|
||||
path: clawhub-source
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
token: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN || github.token }}
|
||||
token: ${{ env.OPENCLAW_DOCS_SYNC_TOKEN || github.token }}
|
||||
|
||||
- name: Setup Node
|
||||
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22.18.0"
|
||||
|
||||
- name: Clone publish repo
|
||||
env:
|
||||
OPENCLAW_DOCS_SYNC_TOKEN: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN }}
|
||||
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3 4 5; do
|
||||
@@ -56,6 +64,7 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Sync docs into publish repo
|
||||
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
|
||||
run: |
|
||||
clawhub_sha="$(git -C "$GITHUB_WORKSPACE/clawhub-source" rev-parse HEAD)"
|
||||
node scripts/docs-sync-publish.mjs \
|
||||
@@ -67,13 +76,16 @@ jobs:
|
||||
--clawhub-source-sha "$clawhub_sha"
|
||||
|
||||
- name: Install docs MDX checker dependency
|
||||
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
|
||||
working-directory: publish
|
||||
run: npm install --no-save --package-lock=false @mdx-js/mdx@3.1.1
|
||||
|
||||
- name: Check publish docs MDX
|
||||
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
|
||||
run: node "$GITHUB_WORKSPACE/publish/.openclaw-sync/check-docs-mdx.mjs" "$GITHUB_WORKSPACE/publish/docs"
|
||||
|
||||
- name: Commit publish repo sync
|
||||
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
|
||||
working-directory: publish
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
230
.github/workflows/full-release-validation.yml
vendored
230
.github/workflows/full-release-validation.yml
vendored
@@ -297,6 +297,7 @@ jobs:
|
||||
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$conclusion" != "success" ]]; then
|
||||
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -396,6 +397,7 @@ jobs:
|
||||
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$conclusion" != "success" ]]; then
|
||||
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -504,6 +506,7 @@ jobs:
|
||||
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$conclusion" != "success" ]]; then
|
||||
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -726,6 +729,7 @@ jobs:
|
||||
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$conclusion" != "success" ]]; then
|
||||
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
summary:
|
||||
@@ -735,62 +739,6 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Request private evidence update
|
||||
env:
|
||||
RELEASE_PRIVATE_DISPATCH_TOKEN: ${{ secrets.OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN }}
|
||||
TARGET_REF: ${{ inputs.ref }}
|
||||
PACKAGE_SPEC: ${{ inputs.evidence_package_spec || inputs.npm_telegram_package_spec }}
|
||||
GITHUB_RUN_ID_VALUE: ${{ github.run_id }}
|
||||
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" ]]; then
|
||||
echo "Release checks were skipped by rerun group; skipping automatic private evidence update."
|
||||
exit 0
|
||||
fi
|
||||
if [[ -z "${RELEASE_PRIVATE_DISPATCH_TOKEN// }" ]]; then
|
||||
echo "OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN is not configured; skipping automatic private evidence update."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
release_id="${TARGET_REF#refs/tags/}"
|
||||
release_id="${release_id#v}"
|
||||
if [[ "$PACKAGE_SPEC" =~ ^openclaw@(.+)$ ]]; then
|
||||
release_id="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
release_id="$(printf '%s' "$release_id" | tr '/:@ ' '----' | tr -cd 'A-Za-z0-9._-')"
|
||||
if [[ -z "$release_id" ]]; then
|
||||
echo "::error::Could not derive release evidence id from target ref '${TARGET_REF}'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
payload="$(
|
||||
jq -cn \
|
||||
--arg full_validation_run_id "$GITHUB_RUN_ID_VALUE" \
|
||||
--arg release_id "$release_id" \
|
||||
--arg release_ref "$TARGET_REF" \
|
||||
--arg package_spec "$PACKAGE_SPEC" \
|
||||
--arg notes "Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE} after child workflows completed; the parent summary re-checks current child run conclusions." \
|
||||
'{
|
||||
event_type: "openclaw_full_release_validation_completed",
|
||||
client_payload: {
|
||||
full_validation_run_id: $full_validation_run_id,
|
||||
release_id: $release_id,
|
||||
release_ref: $release_ref,
|
||||
package_spec: $package_spec,
|
||||
notes: $notes
|
||||
}
|
||||
}'
|
||||
)"
|
||||
|
||||
curl --fail-with-body \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${RELEASE_PRIVATE_DISPATCH_TOKEN}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/openclaw/releases-private/dispatches \
|
||||
-d "$payload"
|
||||
|
||||
- name: Verify child workflow results
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -935,6 +883,54 @@ jobs:
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
summarize_failed_child() {
|
||||
local label="$1"
|
||||
local run_id="$2"
|
||||
if [[ -z "${run_id// }" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local run_json status conclusion artifacts_json
|
||||
run_json="$(gh run view "$run_id" --json status,conclusion,url,jobs)"
|
||||
status="$(jq -r '.status' <<< "$run_json")"
|
||||
conclusion="$(jq -r '.conclusion' <<< "$run_json")"
|
||||
if [[ "$status" == "completed" && "$conclusion" == "success" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
{
|
||||
echo
|
||||
echo "### Failed child detail: ${label}"
|
||||
echo
|
||||
jq -r '
|
||||
"- Run: " + (.url // ""),
|
||||
"- Result: `" + (.status // "") + "/" + (.conclusion // "") + "`",
|
||||
"",
|
||||
"Failed jobs:",
|
||||
(.jobs[]
|
||||
| select(.conclusion != "success" and .conclusion != "skipped")
|
||||
| "- `" + (.name | gsub("`"; "\\`")) + "`: `" + ((.conclusion // .status // "") | tostring) + "` " + (.url // ""))
|
||||
' <<< "$run_json" || true
|
||||
echo
|
||||
echo "Artifacts:"
|
||||
artifacts_json="$(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts?per_page=100" 2>/dev/null || true
|
||||
)"
|
||||
if [[ -n "${artifacts_json// }" ]]; then
|
||||
jq -r '
|
||||
if ((.artifacts // []) | length) == 0 then
|
||||
"- none"
|
||||
else
|
||||
(.artifacts[]
|
||||
| "- `" + (.name | gsub("`"; "\\`")) + "` (" + ((.size_in_bytes // 0) | tostring) + " bytes)")
|
||||
end
|
||||
' <<< "$artifacts_json" || echo "- unable to list artifacts"
|
||||
else
|
||||
echo "- unable to list artifacts"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
failed=0
|
||||
|
||||
append_child_overview
|
||||
@@ -968,4 +964,126 @@ jobs:
|
||||
summarize_child_timing "release_checks" "$RELEASE_CHECKS_RUN_ID"
|
||||
summarize_child_timing "npm_telegram" "$NPM_TELEGRAM_RUN_ID"
|
||||
|
||||
if [[ "$failed" != "0" ]]; then
|
||||
summarize_failed_child "normal_ci" "$NORMAL_CI_RUN_ID"
|
||||
summarize_failed_child "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID"
|
||||
summarize_failed_child "release_checks" "$RELEASE_CHECKS_RUN_ID"
|
||||
summarize_failed_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID"
|
||||
fi
|
||||
|
||||
exit "$failed"
|
||||
|
||||
- name: Request private evidence update
|
||||
env:
|
||||
RELEASE_PRIVATE_DISPATCH_TOKEN: ${{ secrets.OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN }}
|
||||
TARGET_REF: ${{ inputs.ref }}
|
||||
PACKAGE_SPEC: ${{ inputs.evidence_package_spec || inputs.npm_telegram_package_spec }}
|
||||
GITHUB_RUN_ID_VALUE: ${{ github.run_id }}
|
||||
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" ]]; then
|
||||
echo "Release checks were skipped by rerun group; skipping automatic private evidence update."
|
||||
exit 0
|
||||
fi
|
||||
if [[ -z "${RELEASE_PRIVATE_DISPATCH_TOKEN// }" ]]; then
|
||||
echo "OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN is not configured; skipping automatic private evidence update."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
release_id="${TARGET_REF#refs/tags/}"
|
||||
release_id="${release_id#v}"
|
||||
if [[ "$PACKAGE_SPEC" =~ ^openclaw@(.+)$ ]]; then
|
||||
release_id="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
release_id="$(printf '%s' "$release_id" | tr '/:@ ' '----' | tr -cd 'A-Za-z0-9._-')"
|
||||
if [[ -z "$release_id" ]]; then
|
||||
echo "::warning::Could not derive release evidence id from target ref '${TARGET_REF}'; skipping automatic private evidence update."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
payload="$(
|
||||
jq -cn \
|
||||
--arg full_validation_run_id "$GITHUB_RUN_ID_VALUE" \
|
||||
--arg release_id "$release_id" \
|
||||
--arg release_ref "$TARGET_REF" \
|
||||
--arg package_spec "$PACKAGE_SPEC" \
|
||||
--arg notes "Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE} after child workflows completed; the parent summary re-checks current child run conclusions." \
|
||||
'{
|
||||
event_type: "openclaw_full_release_validation_completed",
|
||||
client_payload: {
|
||||
full_validation_run_id: $full_validation_run_id,
|
||||
release_id: $release_id,
|
||||
release_ref: $release_ref,
|
||||
package_spec: $package_spec,
|
||||
notes: $notes
|
||||
}
|
||||
}'
|
||||
)"
|
||||
|
||||
if ! curl --fail-with-body \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${RELEASE_PRIVATE_DISPATCH_TOKEN}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/openclaw/releases-private/dispatches \
|
||||
-d "$payload"; then
|
||||
echo "::warning::Automatic private release evidence dispatch failed; child workflow validation remains authoritative."
|
||||
fi
|
||||
|
||||
- name: Write release validation manifest
|
||||
if: ${{ success() }}
|
||||
env:
|
||||
TARGET_REF: ${{ inputs.ref }}
|
||||
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
|
||||
RELEASE_PROFILE: ${{ inputs.release_profile }}
|
||||
RERUN_GROUP: ${{ inputs.rerun_group }}
|
||||
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'full' }}
|
||||
NORMAL_CI_RUN_ID: ${{ needs.normal_ci.outputs.run_id }}
|
||||
PLUGIN_PRERELEASE_RUN_ID: ${{ needs.plugin_prerelease.outputs.run_id }}
|
||||
RELEASE_CHECKS_RUN_ID: ${{ needs.release_checks.outputs.run_id }}
|
||||
NPM_TELEGRAM_RUN_ID: ${{ needs.npm_telegram.outputs.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
manifest_dir="${RUNNER_TEMP}/full-release-validation"
|
||||
mkdir -p "$manifest_dir"
|
||||
jq -n \
|
||||
--arg workflowName "Full Release Validation" \
|
||||
--arg runId "$GITHUB_RUN_ID" \
|
||||
--arg runAttempt "$GITHUB_RUN_ATTEMPT" \
|
||||
--arg workflowRef "$GITHUB_REF_NAME" \
|
||||
--arg targetRef "$TARGET_REF" \
|
||||
--arg targetSha "$TARGET_SHA" \
|
||||
--arg releaseProfile "$RELEASE_PROFILE" \
|
||||
--arg rerunGroup "$RERUN_GROUP" \
|
||||
--arg runReleaseSoak "$RUN_RELEASE_SOAK" \
|
||||
--arg normalCiRunId "$NORMAL_CI_RUN_ID" \
|
||||
--arg pluginPrereleaseRunId "$PLUGIN_PRERELEASE_RUN_ID" \
|
||||
--arg releaseChecksRunId "$RELEASE_CHECKS_RUN_ID" \
|
||||
--arg npmTelegramRunId "$NPM_TELEGRAM_RUN_ID" \
|
||||
'{
|
||||
version: 1,
|
||||
workflowName: $workflowName,
|
||||
runId: $runId,
|
||||
runAttempt: $runAttempt,
|
||||
workflowRef: $workflowRef,
|
||||
targetRef: $targetRef,
|
||||
targetSha: $targetSha,
|
||||
releaseProfile: $releaseProfile,
|
||||
rerunGroup: $rerunGroup,
|
||||
runReleaseSoak: $runReleaseSoak,
|
||||
childRuns: {
|
||||
normalCi: $normalCiRunId,
|
||||
pluginPrerelease: $pluginPrereleaseRunId,
|
||||
releaseChecks: $releaseChecksRunId,
|
||||
npmTelegram: $npmTelegramRunId
|
||||
}
|
||||
}' > "${manifest_dir}/full-release-validation-manifest.json"
|
||||
|
||||
- name: Upload release validation manifest
|
||||
if: ${{ success() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: full-release-validation-${{ github.run_id }}
|
||||
path: ${{ runner.temp }}/full-release-validation
|
||||
if-no-files-found: error
|
||||
|
||||
14
.github/workflows/install-smoke.yml
vendored
14
.github/workflows/install-smoke.yml
vendored
@@ -100,7 +100,7 @@ jobs:
|
||||
install-smoke-fast:
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_fast_install_smoke == 'true' && needs.preflight.outputs.run_full_install_smoke != 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: "false"
|
||||
DOCKER_BUILD_RECORD_UPLOAD: "false"
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
root_dockerfile_image:
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_full_install_smoke == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
image_ref: ${{ steps.image.outputs.image_ref }}
|
||||
env:
|
||||
@@ -284,7 +284,7 @@ jobs:
|
||||
qr_package_install_smoke:
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_full_install_smoke == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout CLI
|
||||
uses: actions/checkout@v6
|
||||
@@ -299,7 +299,7 @@ jobs:
|
||||
root_dockerfile_smokes:
|
||||
needs: [preflight, root_dockerfile_image]
|
||||
if: needs.preflight.outputs.run_full_install_smoke == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout CLI
|
||||
uses: actions/checkout@v6
|
||||
@@ -401,7 +401,7 @@ jobs:
|
||||
installer_smoke:
|
||||
needs: [preflight, root_dockerfile_image]
|
||||
if: needs.preflight.outputs.run_full_install_smoke == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: "false"
|
||||
DOCKER_BUILD_RECORD_UPLOAD: "false"
|
||||
@@ -471,7 +471,7 @@ jobs:
|
||||
bun_global_install_smoke:
|
||||
needs: [preflight, root_dockerfile_image]
|
||||
if: needs.preflight.outputs.run_full_install_smoke == 'true' && needs.preflight.outputs.run_bun_global_install_smoke == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout CLI
|
||||
uses: actions/checkout@v6
|
||||
@@ -505,7 +505,7 @@ jobs:
|
||||
docker-e2e-fast:
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_fast_install_smoke == 'true' || needs.preflight.outputs.run_full_install_smoke == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 12
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: "false"
|
||||
|
||||
@@ -21,7 +21,7 @@ on:
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
@@ -538,7 +538,6 @@ jobs:
|
||||
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
permission-contents: write
|
||||
permission-issues: write
|
||||
permission-pull-requests: write
|
||||
|
||||
@@ -546,9 +545,15 @@ jobs:
|
||||
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' && steps.run_mantis.outputs.output_dir != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }}
|
||||
TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }}
|
||||
ARTIFACT_URL: ${{ steps.upload_artifact.outputs.artifact-url }}
|
||||
MANTIS_ARTIFACT_R2_ACCESS_KEY_ID: ${{ secrets.MANTIS_ARTIFACT_R2_ACCESS_KEY_ID }}
|
||||
MANTIS_ARTIFACT_R2_BUCKET: openclaw-crabbox-artifacts
|
||||
MANTIS_ARTIFACT_R2_ENDPOINT: ${{ vars.MANTIS_ARTIFACT_R2_ENDPOINT }}
|
||||
MANTIS_ARTIFACT_R2_PUBLIC_BASE_URL: https://artifacts.openclaw.ai
|
||||
MANTIS_ARTIFACT_R2_REGION: auto
|
||||
MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY: ${{ secrets.MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY }}
|
||||
REQUEST_SOURCE: ${{ needs.resolve_request.outputs.request_source }}
|
||||
TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -21,7 +21,7 @@ on:
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
@@ -546,7 +546,6 @@ jobs:
|
||||
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
permission-contents: write
|
||||
permission-issues: write
|
||||
permission-pull-requests: write
|
||||
|
||||
@@ -554,9 +553,15 @@ jobs:
|
||||
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' && steps.run_mantis.outputs.output_dir != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }}
|
||||
TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }}
|
||||
ARTIFACT_URL: ${{ steps.upload_artifact.outputs.artifact-url }}
|
||||
MANTIS_ARTIFACT_R2_ACCESS_KEY_ID: ${{ secrets.MANTIS_ARTIFACT_R2_ACCESS_KEY_ID }}
|
||||
MANTIS_ARTIFACT_R2_BUCKET: openclaw-crabbox-artifacts
|
||||
MANTIS_ARTIFACT_R2_ENDPOINT: ${{ vars.MANTIS_ARTIFACT_R2_ENDPOINT }}
|
||||
MANTIS_ARTIFACT_R2_PUBLIC_BASE_URL: https://artifacts.openclaw.ai
|
||||
MANTIS_ARTIFACT_R2_REGION: auto
|
||||
MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY: ${{ secrets.MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY }}
|
||||
REQUEST_SOURCE: ${{ needs.resolve_request.outputs.request_source }}
|
||||
TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
11
.github/workflows/mantis-slack-desktop-smoke.yml
vendored
11
.github/workflows/mantis-slack-desktop-smoke.yml
vendored
@@ -44,7 +44,7 @@ on:
|
||||
- prehydrated
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
@@ -368,7 +368,6 @@ jobs:
|
||||
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
permission-contents: write
|
||||
permission-issues: write
|
||||
permission-pull-requests: write
|
||||
|
||||
@@ -376,9 +375,15 @@ jobs:
|
||||
if: ${{ always() && inputs.pr_number != '' && steps.run_mantis.outputs.output_dir != '' && steps.upload_artifact.outputs.artifact-url != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }}
|
||||
TARGET_PR: ${{ inputs.pr_number }}
|
||||
ARTIFACT_URL: ${{ steps.upload_artifact.outputs.artifact-url }}
|
||||
MANTIS_ARTIFACT_R2_ACCESS_KEY_ID: ${{ secrets.MANTIS_ARTIFACT_R2_ACCESS_KEY_ID }}
|
||||
MANTIS_ARTIFACT_R2_BUCKET: openclaw-crabbox-artifacts
|
||||
MANTIS_ARTIFACT_R2_ENDPOINT: ${{ vars.MANTIS_ARTIFACT_R2_ENDPOINT }}
|
||||
MANTIS_ARTIFACT_R2_PUBLIC_BASE_URL: https://artifacts.openclaw.ai
|
||||
MANTIS_ARTIFACT_R2_REGION: auto
|
||||
MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY: ${{ secrets.MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY }}
|
||||
REQUEST_SOURCE: workflow_dispatch
|
||||
TARGET_PR: ${{ inputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -28,7 +28,7 @@ on:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
@@ -380,8 +380,9 @@ jobs:
|
||||
openai-api-key: ${{ secrets.OPENCLAW_MANTIS_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
|
||||
prompt-file: .github/codex/prompts/mantis-telegram-desktop-proof.md
|
||||
model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
|
||||
effort: high
|
||||
effort: medium
|
||||
sandbox: danger-full-access
|
||||
codex-args: '["-c","service_tier=\"fast\""]'
|
||||
codex-home: /tmp/mantis-codex-home-${{ github.run_id }}
|
||||
safety-strategy: unprivileged-user
|
||||
codex-user: codex
|
||||
@@ -421,7 +422,6 @@ jobs:
|
||||
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
permission-contents: write
|
||||
permission-issues: write
|
||||
permission-pull-requests: write
|
||||
|
||||
@@ -430,6 +430,12 @@ jobs:
|
||||
env:
|
||||
ARTIFACT_URL: ${{ steps.upload_artifact.outputs.artifact-url }}
|
||||
GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }}
|
||||
MANTIS_ARTIFACT_R2_ACCESS_KEY_ID: ${{ secrets.MANTIS_ARTIFACT_R2_ACCESS_KEY_ID }}
|
||||
MANTIS_ARTIFACT_R2_BUCKET: openclaw-crabbox-artifacts
|
||||
MANTIS_ARTIFACT_R2_ENDPOINT: ${{ vars.MANTIS_ARTIFACT_R2_ENDPOINT }}
|
||||
MANTIS_ARTIFACT_R2_PUBLIC_BASE_URL: https://artifacts.openclaw.ai
|
||||
MANTIS_ARTIFACT_R2_REGION: auto
|
||||
MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY: ${{ secrets.MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY }}
|
||||
REQUEST_SOURCE: ${{ needs.resolve_request.outputs.request_source }}
|
||||
TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }}
|
||||
shell: bash
|
||||
|
||||
11
.github/workflows/mantis-telegram-live.yml
vendored
11
.github/workflows/mantis-telegram-live.yml
vendored
@@ -34,7 +34,7 @@ on:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
@@ -480,7 +480,6 @@ jobs:
|
||||
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
permission-contents: write
|
||||
permission-issues: write
|
||||
permission-pull-requests: write
|
||||
|
||||
@@ -488,9 +487,15 @@ jobs:
|
||||
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' && steps.run_mantis.outputs.output_dir != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }}
|
||||
TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }}
|
||||
ARTIFACT_URL: ${{ steps.upload_artifact.outputs.artifact-url }}
|
||||
MANTIS_ARTIFACT_R2_ACCESS_KEY_ID: ${{ secrets.MANTIS_ARTIFACT_R2_ACCESS_KEY_ID }}
|
||||
MANTIS_ARTIFACT_R2_BUCKET: openclaw-crabbox-artifacts
|
||||
MANTIS_ARTIFACT_R2_ENDPOINT: ${{ vars.MANTIS_ARTIFACT_R2_ENDPOINT }}
|
||||
MANTIS_ARTIFACT_R2_PUBLIC_BASE_URL: https://artifacts.openclaw.ai
|
||||
MANTIS_ARTIFACT_R2_REGION: auto
|
||||
MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY: ${{ secrets.MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY }}
|
||||
REQUEST_SOURCE: ${{ needs.resolve_request.outputs.request_source }}
|
||||
TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -427,6 +427,7 @@ jobs:
|
||||
add_profile_suite live-cli-backend-docker "stable full"
|
||||
add_profile_suite live-acp-bind-docker "stable full"
|
||||
add_profile_suite live-codex-harness-docker "stable full"
|
||||
add_profile_suite live-subagent-announce-docker "stable full"
|
||||
|
||||
add_profile_suite native-live-extensions-a-k "full"
|
||||
add_profile_suite native-live-extensions-media-audio "full"
|
||||
@@ -454,7 +455,7 @@ jobs:
|
||||
validate_release_live_cache:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'live-cache')
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
@@ -504,7 +505,7 @@ jobs:
|
||||
validate_repo_e2e:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_repo_e2e && inputs.live_suite_filter == ''
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
|
||||
env:
|
||||
OPENCLAW_VITEST_MAX_WORKERS: "2"
|
||||
@@ -533,7 +534,7 @@ jobs:
|
||||
validate_special_e2e:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_repo_e2e && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'openshell-e2e')
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -607,7 +608,7 @@ jobs:
|
||||
needs: [validate_selected_ref, prepare_docker_e2e_image]
|
||||
if: inputs.include_release_path_suites && inputs.docker_lanes == ''
|
||||
name: Docker E2E (${{ matrix.label }})
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -875,7 +876,7 @@ jobs:
|
||||
plan_docker_lane_groups:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.docker_lanes != ''
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
groups_json: ${{ steps.groups.outputs.groups_json }}
|
||||
@@ -902,7 +903,7 @@ jobs:
|
||||
needs: [validate_selected_ref, prepare_docker_e2e_image, plan_docker_lane_groups]
|
||||
if: inputs.docker_lanes != ''
|
||||
name: Docker E2E targeted lanes (${{ matrix.group.label }})
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1111,7 +1112,7 @@ jobs:
|
||||
needs: [validate_selected_ref, prepare_docker_e2e_image]
|
||||
if: inputs.include_openwebui && !inputs.include_release_path_suites && inputs.docker_lanes == ''
|
||||
name: Docker E2E (openwebui)
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
@@ -1238,7 +1239,7 @@ jobs:
|
||||
prepare_docker_e2e_image:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_release_path_suites || inputs.include_openwebui || inputs.docker_lanes != ''
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
|
||||
permissions:
|
||||
actions: read
|
||||
@@ -1482,7 +1483,7 @@ jobs:
|
||||
prepare_live_test_image:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_live_suites && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'live-') || startsWith(inputs.live_suite_filter, 'docker-live-models'))
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -1555,7 +1556,7 @@ jobs:
|
||||
name: Docker live models (${{ matrix.provider_label }})
|
||||
needs: [validate_selected_ref, prepare_live_test_image]
|
||||
if: inputs.include_live_suites && inputs.live_model_providers == '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models')
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1707,7 +1708,7 @@ jobs:
|
||||
name: Docker live models (selected providers)
|
||||
needs: [validate_selected_ref, prepare_live_test_image]
|
||||
if: inputs.include_live_suites && inputs.live_model_providers != '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models')
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
@@ -1882,7 +1883,7 @@ jobs:
|
||||
validate_live_provider_suites:
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || (startsWith(inputs.live_suite_filter, 'native-live-') && !startsWith(inputs.live_suite_filter, 'native-live-extensions-media') && inputs.live_suite_filter != 'native-live-extensions-a-k'))
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -2153,27 +2154,11 @@ jobs:
|
||||
fi
|
||||
case "${{ matrix.suite_id }}" in
|
||||
live-cli-backend-docker)
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
|
||||
# Keep the release-blocking CI lane on Codex API-key auth. The
|
||||
# staged auth-file path remains supported for local maintainer
|
||||
# reruns, but it can hang on stale subscription/session state in
|
||||
# an otherwise healthy release run.
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
|
||||
# Replace the staged config.toml with a minimal CI-safe config so
|
||||
# the repo stays trusted for MCP/tool use without inheriting
|
||||
# maintainer-local provider/profile overrides that do not exist
|
||||
# inside CI.
|
||||
# Codex's workspace-write sandbox relies on user namespaces that
|
||||
# this Docker lane does not provide, so run Codex unsandboxed
|
||||
# inside the already-isolated container to keep MCP cron/tool
|
||||
# execution representative instead of failing on nested sandbox
|
||||
# setup.
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
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"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1" >> "$GITHUB_ENV"
|
||||
;;
|
||||
live-codex-harness-docker)
|
||||
# Keep CI on the API-key path for now. The staged Codex auth secret
|
||||
@@ -2219,7 +2204,7 @@ jobs:
|
||||
name: Docker live suites (${{ matrix.label }})
|
||||
needs: [validate_selected_ref, prepare_live_test_image]
|
||||
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'live-'))
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -2291,6 +2276,12 @@ jobs:
|
||||
timeout_minutes: 40
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
- suite_id: live-subagent-announce-docker
|
||||
label: Docker live subagent announce
|
||||
command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 20m bash .release-harness/scripts/test-live-subagent-announce-docker.sh
|
||||
timeout_minutes: 25
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
@@ -2388,14 +2379,11 @@ jobs:
|
||||
fi
|
||||
case "${{ matrix.suite_id }}" in
|
||||
live-cli-backend-docker)
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
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"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1" >> "$GITHUB_ENV"
|
||||
;;
|
||||
live-codex-harness-docker)
|
||||
echo "OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key" >> "$GITHUB_ENV"
|
||||
@@ -2435,7 +2423,7 @@ jobs:
|
||||
name: Live media suites (${{ matrix.label }})
|
||||
needs: validate_selected_ref
|
||||
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'native-live-extensions-media') || inputs.live_suite_filter == 'native-live-extensions-a-k')
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_call' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
container:
|
||||
image: ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04
|
||||
credentials:
|
||||
|
||||
93
.github/workflows/openclaw-npm-release.yml
vendored
93
.github/workflows/openclaw-npm-release.yml
vendored
@@ -16,6 +16,10 @@ on:
|
||||
description: Existing successful preflight workflow run id to promote without rebuilding
|
||||
required: false
|
||||
type: string
|
||||
full_release_validation_run_id:
|
||||
description: Successful Full Release Validation run id for this tag/SHA, required for real publish
|
||||
required: false
|
||||
type: string
|
||||
npm_dist_tag:
|
||||
description: npm dist-tag to publish to
|
||||
required: true
|
||||
@@ -169,12 +173,27 @@ jobs:
|
||||
- name: Verify release contents
|
||||
run: pnpm release:check
|
||||
|
||||
- name: Generate dependency release evidence
|
||||
id: dependency_evidence
|
||||
env:
|
||||
RELEASE_REF: ${{ inputs.tag }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node scripts/generate-dependency-release-evidence.mjs \
|
||||
--release-ref "$RELEASE_REF" \
|
||||
--npm-dist-tag "$RELEASE_NPM_DIST_TAG" \
|
||||
--output-dir "$RUNNER_TEMP/openclaw-release-dependency-evidence" \
|
||||
--github-output "$GITHUB_OUTPUT" \
|
||||
--github-step-summary "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Pack prepared npm tarball
|
||||
id: packed_tarball
|
||||
env:
|
||||
OPENCLAW_PREPACK_PREPARED: "1"
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
DEPENDENCY_EVIDENCE_DIR: ${{ steps.dependency_evidence.outputs.dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PACK_OUTPUT="$RUNNER_TEMP/npm-pack-output.txt"
|
||||
@@ -246,6 +265,7 @@ jobs:
|
||||
rm -rf "$ARTIFACT_DIR"
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
cp "$PACK_PATH" "$ARTIFACT_DIR/"
|
||||
cp -R "$DEPENDENCY_EVIDENCE_DIR" "$ARTIFACT_DIR/dependency-evidence"
|
||||
printf '%s\n' "$RELEASE_TAG" > "$ARTIFACT_DIR/release-tag.txt"
|
||||
printf '%s\n' "$RELEASE_SHA" > "$ARTIFACT_DIR/release-sha.txt"
|
||||
printf '%s\n' "$RELEASE_NPM_DIST_TAG" > "$ARTIFACT_DIR/release-npm-dist-tag.txt"
|
||||
@@ -261,6 +281,8 @@ jobs:
|
||||
packageVersion: process.env.PACKAGE_VERSION,
|
||||
tarballName: process.env.TARBALL_NAME,
|
||||
tarballSha256: process.env.TARBALL_SHA256,
|
||||
dependencyEvidenceDir: "dependency-evidence",
|
||||
dependencyEvidenceManifest: "dependency-evidence/dependency-evidence-manifest.json",
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(process.env.ARTIFACT_DIR, "preflight-manifest.json"),
|
||||
@@ -269,6 +291,27 @@ jobs:
|
||||
NODE
|
||||
echo "dir=$ARTIFACT_DIR" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify prepared npm tarball install
|
||||
env:
|
||||
PREFLIGHT_ARTIFACT_DIR: ${{ steps.packed_tarball.outputs.dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARBALL_PATH="$(find "$PREFLIGHT_ARTIFACT_DIR" -maxdepth 1 -type f -name '*.tgz' -print | sort | tail -n 1)"
|
||||
if [[ -z "$TARBALL_PATH" ]]; then
|
||||
echo "Prepared preflight tarball not found." >&2
|
||||
ls -la "$PREFLIGHT_ARTIFACT_DIR" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
|
||||
node --import tsx scripts/openclaw-npm-prepublish-verify.ts "$TARBALL_PATH" "$PACKAGE_VERSION"
|
||||
|
||||
- name: Upload dependency release evidence
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: openclaw-release-dependency-evidence-${{ inputs.tag }}
|
||||
path: ${{ steps.dependency_evidence.outputs.dir }}
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload prepared npm publish bundle
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -295,12 +338,17 @@ jobs:
|
||||
- name: Require preflight artifact promotion on real publish
|
||||
env:
|
||||
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${PREFLIGHT_RUN_ID}" ]]; then
|
||||
echo "Real publish requires preflight_run_id from a successful npm preflight run." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${FULL_RELEASE_VALIDATION_RUN_ID}" ]]; then
|
||||
echo "Real publish requires full_release_validation_run_id from a successful Full Release Validation run." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
publish_openclaw_npm:
|
||||
# KEEP THE REAL RELEASE/PUBLISH PATH ON A GITHUB-HOSTED RUNNER.
|
||||
@@ -368,6 +416,16 @@ jobs:
|
||||
RUN_JSON="$(gh run view "$PREFLIGHT_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,conclusion,url)"
|
||||
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw NPM Release"], ["headBranch", process.env.EXPECTED_PREFLIGHT_BRANCH], ["event", "workflow_dispatch"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced npm preflight run ${process.env.PREFLIGHT_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using npm preflight run ${process.env.PREFLIGHT_RUN_ID}: ${run.url}`);'
|
||||
|
||||
- name: Verify full release validation run metadata
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RUN_JSON="$(gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
|
||||
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "Full Release Validation"], ["headBranch", process.env.EXPECTED_WORKFLOW_BRANCH], ["event", "workflow_dispatch"], ["status", "completed"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID}: ${run.url}`);'
|
||||
|
||||
- name: Download prepared npm tarball
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
@@ -377,6 +435,15 @@ jobs:
|
||||
run-id: ${{ inputs.preflight_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Download full release validation manifest
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: full-release-validation-${{ inputs.full_release_validation_run_id }}
|
||||
path: full-release-validation
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ inputs.full_release_validation_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Validate release tag and package metadata
|
||||
if: ${{ inputs.preflight_run_id == '' }}
|
||||
env:
|
||||
@@ -433,6 +500,32 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify full release validation target
|
||||
run: |
|
||||
set -euo pipefail
|
||||
EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
|
||||
MANIFEST_FILE="full-release-validation/full-release-validation-manifest.json"
|
||||
if [[ ! -f "$MANIFEST_FILE" ]]; then
|
||||
echo "Full release validation manifest is missing." >&2
|
||||
ls -la full-release-validation >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
WORKFLOW_NAME="$(jq -r '.workflowName // ""' "$MANIFEST_FILE")"
|
||||
TARGET_SHA="$(jq -r '.targetSha // ""' "$MANIFEST_FILE")"
|
||||
RERUN_GROUP="$(jq -r '.rerunGroup // ""' "$MANIFEST_FILE")"
|
||||
if [[ "$WORKFLOW_NAME" != "Full Release Validation" ]]; then
|
||||
echo "Full release validation manifest workflow mismatch: $WORKFLOW_NAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$TARGET_SHA" != "$EXPECTED_RELEASE_SHA" ]]; then
|
||||
echo "Full release validation target SHA mismatch: expected $EXPECTED_RELEASE_SHA, got $TARGET_SHA" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$RERUN_GROUP" != "all" ]]; then
|
||||
echo "Full release validation must run rerun_group=all before npm publish; got $RERUN_GROUP" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Resolve publish tarball
|
||||
id: publish_tarball
|
||||
run: |
|
||||
|
||||
7
.github/workflows/openclaw-performance.yml
vendored
7
.github/workflows/openclaw-performance.yml
vendored
@@ -489,9 +489,7 @@ jobs:
|
||||
reports_root=".artifacts/clawgrit-reports"
|
||||
mkdir -p "$reports_root"
|
||||
git -C "$reports_root" init -b main
|
||||
git -C "$reports_root" remote add origin https://github.com/openclaw/clawgrit-reports.git
|
||||
auth_header="$(printf 'x-access-token:%s' "$CLAWGRIT_REPORTS_TOKEN" | base64 -w0)"
|
||||
git -C "$reports_root" config http.https://github.com/.extraheader "AUTHORIZATION: basic ${auth_header}"
|
||||
git -C "$reports_root" remote add origin "https://x-access-token:${CLAWGRIT_REPORTS_TOKEN}@github.com/openclaw/clawgrit-reports.git"
|
||||
if git -C "$reports_root" ls-remote --exit-code --heads origin main >/dev/null 2>&1; then
|
||||
git -C "$reports_root" fetch --depth=1 origin main
|
||||
git -C "$reports_root" checkout -B main FETCH_HEAD
|
||||
@@ -501,10 +499,13 @@ jobs:
|
||||
|
||||
- name: Publish to clawgrit reports
|
||||
if: ${{ steps.kova.outputs.report_json != '' && steps.clawgrit.outputs.present == 'true' }}
|
||||
env:
|
||||
CLAWGRIT_REPORTS_TOKEN: ${{ secrets.CLAWGRIT_REPORTS_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
reports_root=".artifacts/clawgrit-reports"
|
||||
git -C "$reports_root" remote set-url origin "https://x-access-token:${CLAWGRIT_REPORTS_TOKEN}@github.com/openclaw/clawgrit-reports.git"
|
||||
ref_slug="$(printf '%s' "${TESTED_REF}" | tr -c 'A-Za-z0-9._-' '-')"
|
||||
run_slug="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
dest="${reports_root}/openclaw-performance/${ref_slug}/${run_slug}/${LANE_ID}"
|
||||
|
||||
19
.github/workflows/openclaw-release-checks.yml
vendored
19
.github/workflows/openclaw-release-checks.yml
vendored
@@ -516,6 +516,9 @@ jobs:
|
||||
candidate_version: ${{ needs.prepare_release_package.outputs.package_version }}
|
||||
candidate_source_sha: ${{ needs.prepare_release_package.outputs.source_sha }}
|
||||
openai_model: openai/gpt-5.4
|
||||
ubuntu_runner: ubuntu-24.04
|
||||
windows_runner: windows-2025
|
||||
macos_runner: macos-26
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
@@ -626,7 +629,7 @@ jobs:
|
||||
artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}
|
||||
package_sha256: ${{ (needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.resolve_target.outputs.release_package_spec == '') && needs.prepare_release_package.outputs.package_sha256 || '' }}
|
||||
suite_profile: custom
|
||||
docker_lanes: doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update
|
||||
docker_lanes: doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins-offline plugin-update
|
||||
published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }}
|
||||
published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}
|
||||
telegram_mode: mock-openai
|
||||
@@ -685,7 +688,7 @@ jobs:
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
|
||||
continue-on-error: true
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -772,7 +775,7 @@ jobs:
|
||||
needs: [resolve_target, qa_lab_parity_lane_release_checks]
|
||||
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
|
||||
continue-on-error: true
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -831,7 +834,7 @@ jobs:
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_matrix_enabled == 'true'
|
||||
continue-on-error: true
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -911,7 +914,7 @@ jobs:
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_telegram_enabled == 'true'
|
||||
continue-on-error: true
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -1007,7 +1010,7 @@ jobs:
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_discord_enabled == 'true' && vars.OPENCLAW_RELEASE_QA_DISCORD_LIVE_CI_ENABLED == 'true'
|
||||
continue-on-error: true
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -1103,7 +1106,7 @@ jobs:
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_whatsapp_enabled == 'true' && vars.OPENCLAW_RELEASE_QA_WHATSAPP_LIVE_CI_ENABLED == 'true'
|
||||
continue-on-error: true
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -1199,7 +1202,7 @@ jobs:
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_slack_enabled == 'true' && vars.OPENCLAW_RELEASE_QA_SLACK_LIVE_CI_ENABLED == 'true'
|
||||
continue-on-error: true
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
93
.github/workflows/openclaw-release-publish.yml
vendored
93
.github/workflows/openclaw-release-publish.yml
vendored
@@ -11,6 +11,10 @@ on:
|
||||
description: Successful OpenClaw NPM Release preflight run id, required when publish_openclaw_npm=true
|
||||
required: false
|
||||
type: string
|
||||
full_release_validation_run_id:
|
||||
description: Successful Full Release Validation run id for this tag/SHA, required when publish_openclaw_npm=true
|
||||
required: false
|
||||
type: string
|
||||
npm_dist_tag:
|
||||
description: npm dist-tag for the OpenClaw package
|
||||
required: true
|
||||
@@ -77,6 +81,7 @@ jobs:
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
|
||||
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
|
||||
PLUGINS: ${{ inputs.plugins }}
|
||||
@@ -101,6 +106,10 @@ jobs:
|
||||
echo "publish_openclaw_npm=true requires preflight_run_id." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && -z "${FULL_RELEASE_VALIDATION_RUN_ID}" ]]; then
|
||||
echo "publish_openclaw_npm=true requires full_release_validation_run_id." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${WORKFLOW_REF}" != "refs/heads/main" && ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]]; then
|
||||
echo "publish_openclaw_npm=true requires dispatching this workflow from main or release/YYYY.M.D." >&2
|
||||
exit 1
|
||||
@@ -131,6 +140,16 @@ jobs:
|
||||
run-id: ${{ inputs.preflight_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Download full release validation manifest
|
||||
if: ${{ inputs.publish_openclaw_npm }}
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: full-release-validation-${{ inputs.full_release_validation_run_id }}
|
||||
path: ${{ runner.temp }}/full-release-validation-manifest
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ inputs.full_release_validation_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Checkout release tag
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -186,6 +205,46 @@ jobs:
|
||||
fi
|
||||
echo "sha=$release_sha" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate full release validation manifest
|
||||
if: ${{ inputs.publish_openclaw_npm }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
EXPECTED_SHA: ${{ steps.ref.outputs.sha }}
|
||||
EXPECTED_RELEASE_PROFILE: ${{ inputs.release_profile }}
|
||||
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RUN_JSON="$(gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
|
||||
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "Full Release Validation"], ["headBranch", process.env.EXPECTED_WORKFLOW_BRANCH], ["event", "workflow_dispatch"], ["status", "completed"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID}: ${run.url}`);'
|
||||
|
||||
manifest="${RUNNER_TEMP}/full-release-validation-manifest/full-release-validation-manifest.json"
|
||||
if [[ ! -f "$manifest" ]]; then
|
||||
echo "Full release validation manifest is missing." >&2
|
||||
ls -la "${RUNNER_TEMP}/full-release-validation-manifest" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
workflow_name="$(jq -r '.workflowName // ""' "$manifest")"
|
||||
target_sha="$(jq -r '.targetSha // ""' "$manifest")"
|
||||
release_profile="$(jq -r '.releaseProfile // ""' "$manifest")"
|
||||
rerun_group="$(jq -r '.rerunGroup // ""' "$manifest")"
|
||||
if [[ "$workflow_name" != "Full Release Validation" ]]; then
|
||||
echo "Full release validation manifest workflow mismatch: $workflow_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$target_sha" != "$EXPECTED_SHA" ]]; then
|
||||
echo "Full release validation target SHA mismatch: expected $EXPECTED_SHA, got $target_sha" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$release_profile" != "$EXPECTED_RELEASE_PROFILE" ]]; then
|
||||
echo "Full release validation profile mismatch: expected $EXPECTED_RELEASE_PROFILE, got $release_profile" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$rerun_group" != "all" ]]; then
|
||||
echo "Full release validation must run rerun_group=all before npm publish; got $rerun_group" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Validate release tag is reachable from main or release branch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -208,6 +267,7 @@ jobs:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
TARGET_SHA: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
|
||||
RELEASE_PROFILE: ${{ inputs.release_profile }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
run: |
|
||||
{
|
||||
echo "### Release target"
|
||||
@@ -215,6 +275,9 @@ jobs:
|
||||
echo "- Tag: \`${RELEASE_TAG}\`"
|
||||
echo "- SHA: \`${TARGET_SHA}\`"
|
||||
echo "- Release profile: \`${RELEASE_PROFILE}\`"
|
||||
if [[ -n "${FULL_RELEASE_VALIDATION_RUN_ID// }" ]]; then
|
||||
echo "- Full release validation: \`${FULL_RELEASE_VALIDATION_RUN_ID}\`"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
publish:
|
||||
@@ -237,6 +300,7 @@ jobs:
|
||||
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
|
||||
PLUGINS: ${{ inputs.plugins }}
|
||||
@@ -401,6 +465,33 @@ jobs:
|
||||
echo "- GitHub release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
upload_dependency_evidence_release_asset() {
|
||||
local release_version download_dir asset_path asset_name
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
download_dir="${RUNNER_TEMP}/openclaw-release-dependency-evidence-asset"
|
||||
asset_name="openclaw-${release_version}-dependency-evidence.zip"
|
||||
asset_path="${RUNNER_TEMP}/${asset_name}"
|
||||
|
||||
rm -rf "${download_dir}" "${asset_path}"
|
||||
mkdir -p "${download_dir}"
|
||||
gh run download "${PREFLIGHT_RUN_ID}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--name "openclaw-npm-preflight-${RELEASE_TAG}" \
|
||||
--dir "${download_dir}"
|
||||
|
||||
if [[ ! -d "${download_dir}/dependency-evidence" ]]; then
|
||||
echo "Dependency evidence is missing from OpenClaw npm preflight artifact." >&2
|
||||
find "${download_dir}" -maxdepth 2 -type f -print >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(cd "${download_dir}" && zip -qr "${asset_path}" dependency-evidence)
|
||||
gh release upload "${RELEASE_TAG}" "${asset_path}#${asset_name}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--clobber
|
||||
echo "- Dependency evidence asset: \`${asset_name}\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
{
|
||||
echo "### Publish sequence"
|
||||
echo
|
||||
@@ -446,6 +537,7 @@ jobs:
|
||||
-f tag="${RELEASE_TAG}" \
|
||||
-f preflight_only=false \
|
||||
-f preflight_run_id="${PREFLIGHT_RUN_ID}" \
|
||||
-f full_release_validation_run_id="${FULL_RELEASE_VALIDATION_RUN_ID}" \
|
||||
-f npm_dist_tag="${RELEASE_NPM_DIST_TAG}")"
|
||||
echo "- OpenClaw npm run ID: \`${openclaw_npm_run_id}\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
@@ -491,4 +583,5 @@ jobs:
|
||||
|
||||
if [[ -n "${openclaw_npm_run_id}" ]]; then
|
||||
create_or_update_github_release
|
||||
upload_dependency_evidence_release_asset
|
||||
fi
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: write
|
||||
pull-requests: read
|
||||
@@ -20,6 +21,7 @@ env:
|
||||
jobs:
|
||||
live_and_openwebui_checks:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: write
|
||||
pull-requests: read
|
||||
|
||||
8
.github/workflows/package-acceptance.yml
vendored
8
.github/workflows/package-acceptance.yml
vendored
@@ -284,7 +284,7 @@ env:
|
||||
jobs:
|
||||
resolve_package:
|
||||
name: Resolve package candidate
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
outputs:
|
||||
docker_lanes: ${{ steps.profile.outputs.docker_lanes }}
|
||||
@@ -386,10 +386,10 @@ jobs:
|
||||
docker_lanes="npm-onboard-channel-agent gateway-network config-reload"
|
||||
;;
|
||||
package)
|
||||
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins-offline plugin-update"
|
||||
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins-offline plugin-update"
|
||||
;;
|
||||
product)
|
||||
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor update-restart-auth plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui"
|
||||
docker_lanes="npm-onboard-channel-agent doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui"
|
||||
include_openwebui=true
|
||||
;;
|
||||
full)
|
||||
@@ -588,7 +588,7 @@ jobs:
|
||||
name: Verify package acceptance
|
||||
needs: [resolve_package, docker_acceptance, package_telegram]
|
||||
if: always()
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Verify package acceptance results
|
||||
|
||||
12
.github/workflows/plugin-clawhub-release.yml
vendored
12
.github/workflows/plugin-clawhub-release.yml
vendored
@@ -444,10 +444,14 @@ jobs:
|
||||
async function fetchWithRetry(url, options = {}) {
|
||||
let lastStatus = "unknown";
|
||||
for (let attempt = 1; attempt <= 12; attempt += 1) {
|
||||
const response = await fetch(url, { redirect: "manual", ...options });
|
||||
lastStatus = response.status;
|
||||
if (response.status !== 429 && response.status < 500) {
|
||||
return response;
|
||||
try {
|
||||
const response = await fetch(url, { redirect: "manual", ...options });
|
||||
lastStatus = response.status;
|
||||
if (response.status !== 429 && response.status < 500) {
|
||||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
lastStatus = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, attempt * 5000));
|
||||
}
|
||||
|
||||
6
.github/workflows/plugin-prerelease.yml
vendored
6
.github/workflows/plugin-prerelease.yml
vendored
@@ -209,7 +209,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_plugin_prerelease_static == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_plugin_prerelease_node == 'true'
|
||||
runs-on: ${{ matrix.runner || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (matrix.runner || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_plugin_prerelease_extensions == 'true'
|
||||
runs-on: ${{ matrix.runner }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
17
.github/workflows/website-installer-sync.yml
vendored
17
.github/workflows/website-installer-sync.yml
vendored
@@ -134,20 +134,29 @@ jobs:
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.sync_website)
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
OPENCLAW_GH_TOKEN: ${{ secrets.OPENCLAW_GH_TOKEN }}
|
||||
steps:
|
||||
- name: Skip website sync without token
|
||||
if: env.OPENCLAW_GH_TOKEN == ''
|
||||
run: echo "OPENCLAW_GH_TOKEN is not configured; installer verification passed, skipping website sync."
|
||||
|
||||
- name: Checkout OpenClaw
|
||||
if: env.OPENCLAW_GH_TOKEN != ''
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: openclaw
|
||||
|
||||
- name: Checkout openclaw.ai
|
||||
if: env.OPENCLAW_GH_TOKEN != ''
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: openclaw/openclaw.ai
|
||||
token: ${{ secrets.OPENCLAW_GH_TOKEN }}
|
||||
token: ${{ env.OPENCLAW_GH_TOKEN }}
|
||||
path: openclaw.ai
|
||||
|
||||
- name: Sync installer scripts
|
||||
if: env.OPENCLAW_GH_TOKEN != ''
|
||||
run: |
|
||||
cp openclaw/scripts/install.sh openclaw.ai/public/install.sh
|
||||
cp openclaw/scripts/install-cli.sh openclaw.ai/public/install-cli.sh
|
||||
@@ -156,6 +165,7 @@ jobs:
|
||||
chmod +x openclaw.ai/public/install.sh openclaw.ai/public/install-cli.sh
|
||||
|
||||
- name: Check for changes
|
||||
if: env.OPENCLAW_GH_TOKEN != ''
|
||||
id: changes
|
||||
working-directory: openclaw.ai
|
||||
run: |
|
||||
@@ -196,7 +206,10 @@ jobs:
|
||||
run: |
|
||||
git config user.name "openclaw-installer-sync[bot]"
|
||||
git config user.email "openclaw-installer-sync[bot]@users.noreply.github.com"
|
||||
git add public/install.sh public/install-cli.sh public/install.ps1 public/install.cmd
|
||||
git add public/install.sh public/install-cli.sh public/install.ps1
|
||||
if git ls-files --error-unmatch public/install.cmd >/dev/null 2>&1; then
|
||||
git add -u -- public/install.cmd
|
||||
fi
|
||||
git commit -m "chore: sync installers from openclaw ${GITHUB_SHA::12}"
|
||||
git pull --rebase origin main
|
||||
git push origin HEAD:main
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -41,6 +41,7 @@ apps/macos/.build/
|
||||
apps/macos-mlx-tts/.build/
|
||||
apps/shared/MoltbotKit/.build/
|
||||
apps/shared/OpenClawKit/.build/
|
||||
apps/shared/*/.build/
|
||||
apps/shared/OpenClawKit/Package.resolved
|
||||
**/ModuleCache/
|
||||
bin/
|
||||
@@ -50,6 +51,7 @@ apps/macos/.build-local/
|
||||
apps/macos/.swiftpm/
|
||||
apps/shared/MoltbotKit/.swiftpm/
|
||||
apps/shared/OpenClawKit/.swiftpm/
|
||||
apps/shared/*/.swiftpm/
|
||||
Core/
|
||||
apps/ios/*.xcodeproj/
|
||||
apps/ios/*.xcworkspace/
|
||||
@@ -108,6 +110,9 @@ USER.md
|
||||
# local tooling
|
||||
.serena/
|
||||
|
||||
# local QA evidence mirrors; CI publishes canonical Mantis files as Actions artifacts
|
||||
mantis/
|
||||
|
||||
# Local project-agent skill installs. Only repo-owned skills are visible by
|
||||
# default; promoting a new repo skill should require an intentional `git add -f`.
|
||||
.agents/skills/*
|
||||
@@ -115,6 +120,8 @@ USER.md
|
||||
!.agents/skills/blacksmith-testbox/**
|
||||
!.agents/skills/crabbox/
|
||||
!.agents/skills/crabbox/**
|
||||
!.agents/skills/clawdtributor/
|
||||
!.agents/skills/clawdtributor/**
|
||||
!.agents/skills/gitcrawl/
|
||||
!.agents/skills/gitcrawl/**
|
||||
!.agents/skills/openclaw-docs/**
|
||||
@@ -132,6 +139,8 @@ USER.md
|
||||
!.agents/skills/openclaw-refactor-docs/**
|
||||
!.agents/skills/openclaw-qa-testing/
|
||||
!.agents/skills/openclaw-qa-testing/**
|
||||
!.agents/skills/openclaw-release-ci/
|
||||
!.agents/skills/openclaw-release-ci/**
|
||||
!.agents/skills/openclaw-release-maintainer/
|
||||
!.agents/skills/openclaw-release-maintainer/**
|
||||
!.agents/skills/openclaw-secret-scanning-maintainer/
|
||||
|
||||
28
AGENTS.md
28
AGENTS.md
@@ -10,12 +10,12 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Docs/user-visible work: `pnpm docs:list`, then read relevant docs only.
|
||||
- Fix/triage answers need source, tests, current/shipped behavior, and dependency contract proof.
|
||||
- Dependency-backed behavior: read upstream docs/source/types first. No API/default/error/timing guesses.
|
||||
- Live-verify when feasible. Check env/`~/.profile` for keys before saying blocked; never print secrets.
|
||||
- Live-verify when feasible. Never print secrets.
|
||||
- Missing deps: `pnpm install`, retry once, then report first actionable error.
|
||||
- CODEOWNERS: maint/refactor/tests ok. Larger behavior/product/security/ownership: owner ask/review.
|
||||
- Product/docs/UI/changelog wording: "plugin/plugins"; `extensions/` is internal.
|
||||
- New channel/plugin/app/doc surface: update `.github/labeler.yml` + GH labels.
|
||||
- New `AGENTS.md`: add sibling `CLAUDE.md` symlink.
|
||||
- New `AGENTS.md`: add sibling `CLAUDE.md` symlink; edit `AGENTS.md` only.
|
||||
|
||||
## Map
|
||||
|
||||
@@ -31,11 +31,15 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Core/tests: no deep plugin internals (`extensions/*/src/**`, `onboard.js`). Use public barrels, SDK facade, generic contracts.
|
||||
- Owner boundary: owner-specific repair/detection/onboarding/auth/defaults/provider behavior lives in owner plugin. Shared/core gets generic seams only.
|
||||
- Dependency ownership follows runtime ownership: plugin-only deps stay plugin-local; root deps only for core imports or intentionally internalized bundled plugin runtime.
|
||||
- Internal bundled plugins ship in core dist; bundled-only facade loader ok only for them.
|
||||
- External official plugins own package/deps and are excluded from core dist; core uses registry-aware `facade-runtime` or generic contracts.
|
||||
- Externalizing a bundled plugin: update package excludes, official catalogs, docs, tests, and prove core runtime paths resolve installed plugin roots before root-dep removal.
|
||||
- Legacy config repair belongs in `openclaw doctor --fix`, not startup/load-time core migrations. Runtime paths use canonical contracts.
|
||||
- New seams: backward-compatible, documented, versioned. Third-party plugins exist.
|
||||
- Channels are implementation under `src/channels/**`; plugin authors get SDK seams. Providers own auth/catalog/runtime hooks; core owns generic loop.
|
||||
- Hot paths should carry prepared facts forward: provider id, model ref, channel id, target, capability family, attachment class. Do not rediscover with broad plugin/provider/channel/capability loaders.
|
||||
- Do not fix repeated request-time discovery with scattered caches. Move the canonical fact earlier; reuse prepared runtime objects; delete duplicate lookup branches.
|
||||
- Inline code comments: brief notes for tricky, bug-prone, or previously buggy logic.
|
||||
- Gateway protocol changes: additive first; incompatible needs versioning/docs/client follow-through.
|
||||
- Config contract: exported types, schema/help, metadata, baselines, docs aligned. Retired public keys stay retired; compat in raw migration/doctor only.
|
||||
- Prompt cache: deterministic ordering for maps/sets/registries/plugin lists/files/network results before model/tool payloads. Preserve old transcript bytes when possible.
|
||||
@@ -45,9 +49,12 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Runtime: Node 22+. Keep Node + Bun paths working.
|
||||
- Package manager/runtime: repo defaults only. No swaps without approval.
|
||||
- Install: `pnpm install` (keep Bun lock/patches aligned if touched).
|
||||
- Sharp/Homebrew libvips source-build fail: `SHARP_IGNORE_GLOBAL_LIBVIPS=1 pnpm install`.
|
||||
- CLI: `pnpm openclaw ...` or `pnpm dev`; build: `pnpm build`.
|
||||
- Tests: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
|
||||
- Checks: `pnpm check:changed`; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
|
||||
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
|
||||
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
|
||||
- Checks in a normal source checkout: `pnpm check:changed`; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
|
||||
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... --shell -- "pnpm check:changed"` so pnpm runs inside Testbox, not locally.
|
||||
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
|
||||
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); never add `tsc --noEmit`, `typecheck`, `check:types`.
|
||||
- Formatting: `oxfmt`, not Prettier. Use repo wrappers (`pnpm format:*`, `pnpm lint:*`, `scripts/run-oxlint.mjs`).
|
||||
@@ -56,10 +63,12 @@ Skills own workflows; root owns hard policy and routing.
|
||||
## Validation
|
||||
|
||||
- Use `$openclaw-testing` for test/CI choice and `$crabbox` for remote/full/E2E proof.
|
||||
- Small/narrow tests, lints, format checks, and type probes are fine locally.
|
||||
- Small/narrow tests, lints, format checks, and type probes are fine locally only in a healthy normal checkout.
|
||||
- In Codex worktrees, direct local `pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, and `scripts/committer` can trigger pnpm dependency reconciliation or install prompts. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated proof.
|
||||
- Full suites, broad changed gates, Docker/package/E2E/live/cross-OS proof, or anything that bogs down the Mac: Crabbox/Testbox.
|
||||
- One/few files local. If a local command fans out, stop and move broad proof to Crabbox/Testbox.
|
||||
- Before handoff/push: prove touched surface. Before landing to `main`: issue proof plus appropriate full/broad proof unless scope is clearly narrow.
|
||||
- Pre-land/pre-commit code changes: use `$codex-review` until no accepted/actionable findings remain, unless equivalent manual review already done, trivial/docs-only, or user opts out.
|
||||
- If proof is blocked, say exactly what is missing and why.
|
||||
- Do not land related failing format/lint/type/build/tests. If unrelated on latest `origin/main`, say so with scoped proof.
|
||||
- Docs/changelog-only and CI/workflow metadata-only: `git diff --check` plus relevant docs/workflow sanity; escalate only if scripts/config/generated/package/runtime behavior changed.
|
||||
@@ -70,18 +79,22 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- PR refs: `gh pr view/diff` or `gh api`, not web search. Prefer `gitcrawl` for maintainer discovery; missing/stale `gitcrawl` falls through to live `gh`, not contributor setup. Verify live with `gh` before mutation.
|
||||
- Bare issue/PR URL/number means review/report in chat. Suggest comment/close/merge when appropriate; mutate only when asked.
|
||||
- No unsolicited PR comments/reviews/labels/retitles/rebases/fixups/landing. Exception: close/duplicate action that needs a reason comment after explicit close/sweep/landing request.
|
||||
- PR review answer: bug/behavior, URL(s), affected surface, best-fix judgment, evidence from code/tests/CI/current or shipped behavior.
|
||||
- Maintainer decision closes the cluster: if deciding reported behavior/proposed fix is not planned, comment+close all directly associated open issues/PRs unless explicitly told to keep one open. Associated means linked PRs/issues, duplicates, companion workaround PRs, and the canonical issue for the rejected behavior.
|
||||
- Do not leave associated issues open for hypothetical future repros. Close with rationale; ask for a new issue or reopen only if concrete new evidence appears. Close comment states: decision, why, supported alternative, and what evidence would change the decision.
|
||||
- PR review answer: bug/behavior, URL(s), affected surface, provenance for regressions when traceable, best-fix judgment, evidence from code/tests/CI/current or shipped behavior.
|
||||
- Issue/PR final answer: last line is the full GitHub URL.
|
||||
- Changelog: PR landings/fixes need one unless pure test/internal. Do not mention missing changelog as a review finding; Codex handles it during fix/landing.
|
||||
- PR verification: before merge, post exact local commands, CI/Testbox run IDs, before/after proof when used, and known proof gaps.
|
||||
- Issue fixed on `main` with proof: comment proof + commit/PR, then close.
|
||||
- After landing or requested close/sweep: search duplicates; comment proof + canonical commit/PR/release before closing.
|
||||
- After landing/ship final: include 2-5 sentence recap of what landed: behavior change, key files/surface, proof run, issue/PR state. Do not answer with only status/links.
|
||||
- `ship` that fixes an issue: after push, comment proof + commit link, then close the issue.
|
||||
- GH comments with backticks, `$`, or shell snippets: use heredoc/body file, not inline double-quoted `--body`.
|
||||
- PR create: real body required. Include Summary + Verification; mention refs, behavior, and proof.
|
||||
- Real behavior proof section is parsed. Use exact `field: value` labels: `Behavior addressed`, `Real environment tested`, `Exact steps or command run after this patch`, `Evidence after fix`, `Observed result after fix`, `What was not tested`.
|
||||
- PR artifacts/screenshots: attach to PR/comment/external artifact store. 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.
|
||||
- Maintainers: ignore `Real behavior proof` failures that only say PR body lacks real after-fix evidence.
|
||||
- Maintainers: may skip/ignore `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`.
|
||||
|
||||
## Code
|
||||
@@ -134,7 +147,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
|
||||
- Never commit real phone numbers, videos, credentials, live config.
|
||||
- Secrets: channel/provider creds in `~/.openclaw/credentials/`; model auth profiles in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`.
|
||||
- Env keys: check `~/.profile`; redact output.
|
||||
- Dependency patches/overrides/vendor changes need explicit approval. `pnpm-workspace.yaml` patched dependencies use exact versions only.
|
||||
- Carbon pins owner-only: do not change `@buape/carbon` unless Shadow (`@thewilloftheshadow`, verified by `gh`) asks.
|
||||
- Releases/publish/version bumps need explicit approval. Use `$openclaw-release-maintainer`.
|
||||
|
||||
980
CHANGELOG.md
980
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
837
appcast.xml
837
appcast.xml
@@ -2,6 +2,480 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>OpenClaw</title>
|
||||
<item>
|
||||
<title>2026.5.12</title>
|
||||
<pubDate>Fri, 15 May 2026 13:25:16 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026051290</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.5.12</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.5.12</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Amazon Bedrock: externalize the Bedrock and Bedrock Mantle provider packages so core installs no longer pull AWS SDK dependencies unless those providers are installed.</li>
|
||||
<li>Plugins: externalize Slack, OpenShell sandbox, and Anthropic Vertex so their runtime dependency cones install only when those plugins are installed.</li>
|
||||
<li>Control UI/WebChat: add a persisted auto-scroll mode selector so users can keep the current near-bottom behavior, always follow streaming output, or turn automatic streaming scroll off and use the New messages button manually. Fixes #7648 and #81287. Thanks @BunsDev.</li>
|
||||
<li>ACP: add <code>acp.fallbacks</code> so ACP turns can try configured backup runtime backends when the primary backend is unavailable before any output is emitted. (#69542) Thanks @kaseonedge.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Doctor/Codex: stop warning that the message tool is unavailable for source-reply paths where OpenClaw grants <code>message</code> at runtime, keeping update and doctor output aligned with the OpenAI happy path. Thanks @pashpashpash.</li>
|
||||
<li>Channels/Weixin: bump the external Weixin catalog entry to <code>@tencent-weixin/openclaw-weixin@2.4.3</code> with the matching package integrity. (#81730) Thanks @scotthuang.</li>
|
||||
<li>Agents/subagents: apply <code>agents.defaults.subagents.model</code> before target agent primary models during <code>sessions_spawn</code>, so model-scoped runtimes such as <code>claude-cli</code> stay attached to default child runs. Fixes #81395. (#81783) Thanks @joshavant.</li>
|
||||
<li>Telegram: keep Bot API polling alive during main event-loop stalls by moving ingress to an isolated worker with a durable local spool. Fixes #81132. (#81746) Thanks @joshavant.</li>
|
||||
<li>Telegram: preserve rendered HTML formatting through lazy cron announce delivery so Markdown links stay clickable instead of falling back to literal anchor tags. Fixes #81742. (#81758)</li>
|
||||
<li>Telegram: skip unmentioned group media before download when <code>requireMention</code> is active, avoiding failed media-download replies for messages that should be ignored. Fixes #81181. (#81785) Thanks @joshavant.</li>
|
||||
<li>CLI/plugins: keep bare plugin and parent-command help on the lightweight path, avoiding plugin registry discovery before rendering help.</li>
|
||||
<li>Gateway/session history: carry monotonic transcript message sequence through live updates and refresh SSE history when stale sequence input would otherwise append bad incremental state. (#81474) Thanks @samzong.</li>
|
||||
<li>Security/sandbox: include Windows <code>USERPROFILE</code> in the sandbox blocked home roots so credential-bearing binds (such as <code>.codex</code>, <code>.openclaw</code>, or <code>.ssh</code> under the Windows user profile) are denied even when <code>HOME</code> points at a different shell home. (#63074) Thanks @luoyanglang.</li>
|
||||
<li>Models config/auth: stop inferring provider env-var markers from broad <code>^[A-Z_][A-Z0-9_]*$</code> strings, and resolve config-backed provider <code>apiKey</code> values only through structured env SecretRefs (<code>secrets.providers[id]</code> / <code>secrets.defaults</code>), so unrelated env vars cannot accidentally become provider credentials. Thanks @sallyom.</li>
|
||||
<li>Media fetch: skip allocating and buffering the response body for bodyless media responses (HEAD probes and 204-style empty bodies), avoiding wasted heap on streams that carry no payload. Thanks @shakkernerd.</li>
|
||||
<li>CLI/onboarding: forward provider-specific auth flags (e.g. <code>--openai-api-key</code>) through the onboarding wizard so they reach provider auth methods via <code>ctx.opts</code>, letting <code>--openai-api-key "$OPENAI_API_KEY"</code> skip the redundant "use existing env var?" prompt in non-interactive harnesses. (#81669) Thanks @sjf.</li>
|
||||
<li>CLI/migrate: drop trailing periods from Codex migrate item messages and <code>REASON_CODE_MESSAGES</code> strings so plan/result rows read as labels instead of sentence fragments. (#81705) Thanks @sjf.</li>
|
||||
<li>Slack: treat malformed private-file redirect <code>Location</code> headers as unfollowable redirects instead of failing Slack media downloads.</li>
|
||||
<li>Plugins: discover provider plugins from <code>setup.providers[].envVars</code> credentials during provider discovery while keeping the deprecated <code>providerAuthEnvVars</code> fallback. (#81542) Thanks @JARVIS-Glasses.</li>
|
||||
<li>Docs/Codex harness: clarify that per-agent <code>CODEX_HOME</code> isolates <code>~/.codex</code> while inherited <code>HOME</code> intentionally keeps <code>.agents</code> discovery and subprocess user-home state available.</li>
|
||||
<li>Auth: reclaim dead-owner stale file locks before retrying locked writes, so crashed OAuth refreshes no longer wedge <code>auth-profiles.json</code> until manual cleanup.</li>
|
||||
<li>CLI tables: preserve muted/color styling on wrapped continuation lines after multiline cells, keeping <code>openclaw plugins list</code> descriptions readable.</li>
|
||||
<li>Process execution: collapse case-insensitive duplicate child environment keys on Windows so caller-provided overrides such as <code>PATH</code> cannot be shadowed by host <code>Path</code>.</li>
|
||||
<li>Gateway/diagnostics: suppress cold-start liveness warnings during the startup grace window while still sampling liveness metrics. Fixes #79915. (#81699) Thanks @joshavant.</li>
|
||||
<li>Codex harness: keep <code>oauthRef</code>-backed Codex OAuth profiles usable and stop high-confidence app-server OAuth refresh invalidation from retry-spamming raw token-refresh errors without turning entitlement or usage-limit payloads into re-auth prompts.</li>
|
||||
<li>Browser CLI: request the existing <code>operator.admin</code> gateway scope explicitly for browser control commands, avoiding unnecessary scope-upgrade approval loops. Fixes #81555. (#81716) Thanks @joshavant.</li>
|
||||
<li>Gateway/diagnostics: suppress cold-start liveness warnings during the startup grace window while still sampling liveness metrics. Fixes #79915. (#81699) Thanks @joshavant.</li>
|
||||
<li>Plugin SDK: restore the deprecated <code>openclaw/plugin-sdk/memory-core</code> package subpath as an alias of <code>memory-host-core</code>, so published memory companion plugins that still import it resolve on current hosts.</li>
|
||||
<li>Control UI/i18n: use the installed workspace pi runtime for locale refreshes, update the fallback package pin, prefer the Anthropic CI provider when available, and skip invalid provider credentials instead of failing main.</li>
|
||||
<li>Codex harness: classify native app-server token-refresh logout and relogin failures as authentication refresh errors, so users get re-authentication guidance instead of a raw runtime failure.</li>
|
||||
<li>Codex startup: treat selectable configured OpenAI agent models as Codex runtime requirements during plugin auto-enable, startup planning, and doctor install repair, so Anthropic-primary configs can still switch to OpenAI/Codex cleanly.</li>
|
||||
<li>Agents: preserve source-reply delivery metadata when merging tool-returned media into the final reply, keeping message-tool-only replies deliverable and mirrored. Thanks @pashpashpash and @vincentkoc.</li>
|
||||
<li>Replies: treat rich presentation, interactive controls, and channel-native payload data as outbound content across follow-up, heartbeat, cron, ACP, and block-streaming delivery paths, preventing card/button-only replies from being dropped as empty.</li>
|
||||
<li>WebChat/TUI: route Codex <code>tools.message</code> source replies to the active internal UI turn and mirror them to session history, so message-tool-only harness replies, including rich presentation and button-only replies, no longer disappear while WebChat and TUI remain non-targetable outbound channels. (#81586) Thanks @pashpashpash.</li>
|
||||
<li>Replies: deliver rich-only block replies even when block-streaming coalescing is enabled, keeping card and button payloads from being dropped by the text coalescer. Thanks @pashpashpash.</li>
|
||||
<li>macOS/companion: require system TLS trust before pinning a first-use direct <code>wss://</code> gateway certificate and honor <code>gateway.remote.tlsFingerprint</code> as the explicit pin for remote node-mode sessions, so fresh endpoints fail closed when macOS cannot trust the certificate unless configured out of band. Fixes #50642. Thanks @BunsDev.</li>
|
||||
<li>Update: snapshot config before update-time repair and restart writes, preserve plugin install records through doctor cleanup, and keep update-time config size drops from blocking the update while pointing users to the pre-update backup. Fixes #80077. (#80257) Thanks @Jerry-Xin and @vincentkoc.</li>
|
||||
<li>Sessions/status: classify ACP spawn-child sessions as <code>kind: "spawn-child"</code> instead of <code>"direct"</code> in <code>openclaw sessions</code> and status output; extract the duplicated session-kind classifier into a shared helper (<code>src/sessions/classify-session-kind.ts</code>) so both surfaces stay in sync. Fixes catalog #19. (#79544)</li>
|
||||
<li>Sessions/Gateway: report <code>agentRuntime.id: "acpx"</code> (or stored backend id) with <code>source: "session-key"</code> for ACP control-plane session rows in <code>openclaw sessions --json</code>, <code>openclaw status</code>, and Gateway session RPC responses instead of the incorrect <code>"auto"</code> / <code>"pi"</code> implicit fallback. Fixes catalog #18. (#79550)</li>
|
||||
<li>Telegram: delete tool-progress-only draft bubbles before rotating to the real answer, preventing orphaned progress messages in streamed replies.</li>
|
||||
<li>Codex app-server: keep per-agent <code>CODEX_HOME</code> isolation without rewriting <code>HOME</code> by default, so Codex-run subprocesses can still find normal user-home config, tokens, and CLI state unless the launch explicitly overrides <code>HOME</code>. Thanks @pashpashpash.</li>
|
||||
<li>iMessage: stop sending visible <code><media:image></code> placeholder text for media-only native image sends while preserving the internal echo key that prevents self-echo duplicate replies. (#81209) Thanks @homer-byte.</li>
|
||||
<li>Agents/sessions: create configured agent main sessions before first <code>sessions_send</code> or gateway send, so agent-to-agent messages no longer fail when the target agent has not started yet.</li>
|
||||
<li>gateway: pass Talk session scope to resolver [AI]. (#81379) Thanks @pgondhi987.</li>
|
||||
<li>Gateway protocol: require v4 clients and stream explicit chat <code>deltaText</code>/<code>replace</code> frames so SDK clients can consume assistant updates without local diffing. (#80725) Thanks @samzong.</li>
|
||||
<li>GitHub Copilot: exchange OAuth tokens for Copilot API tokens on image understanding requests and route Gemini image payloads through Chat Completions, fixing Copilot Gemini image descriptions. (#80393, #80442) Thanks @afunnyhy.</li>
|
||||
<li>Gateway: hide pending Node pairing commands, capabilities, and permissions until approval, and refresh the live approved surface when pairings change. (#80741) Thanks @samzong.</li>
|
||||
<li>Plugins/Feishu/WhatsApp/Line: enforce inbound media size caps while reading download streams, avoiding full buffering of oversized attachments. (#81044, #81050) Thanks @samzong.</li>
|
||||
<li>Plugins/install: limit install-time code safety scans to plugin-owned runtime entrypoints while keeping dependency manifest denylist checks, so trusted packages with large dependency trees no longer get blocked or warned on third-party runtime internals.</li>
|
||||
<li>Config: serialize and retry semantic config mutations centrally, so concurrent commands can rebase safe changes instead of clobbering or hand-rolling command-local retry loops. (#76601)</li>
|
||||
<li>Installer: honor <code>--no-git-update</code> for existing git checkouts before resolving release refs, preventing pinned source installs from moving during reinstall.</li>
|
||||
<li>Plugins/install: refresh OpenClaw-managed peer dependency pins when installed plugin peer ranges change, while preserving user-owned dependency pins.</li>
|
||||
<li>Require approval for setup-code device pairing [AI]. (#81292) Thanks @pgondhi987.</li>
|
||||
<li>Plugins/install: preserve third-party peer dependencies in the managed npm root when later plugin installs or updates recalculate the shared dependency tree. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/memory: prefer the npm-installed memory-lancedb plugin over the bundled fallback during duplicate resolution, keeping Active Memory's <code>memory_recall</code> tool visible after managed installs. Fixes #81193. Thanks @julio-arcila.</li>
|
||||
<li>Plugins/uninstall: prune managed third-party peer dependencies after their owning npm plugin is removed, without blocking plugin cleanup on peer-prune failures.</li>
|
||||
<li>Docker: pin setup-time container paths so stale host <code>.env</code> OpenClaw paths cannot leak into Linux containers. Fixes #80381. (#81105) Thanks @brokemac79.</li>
|
||||
<li>Channels/WeCom: refresh the official onboarding install to <code>@wecom/wecom-openclaw-plugin@2026.5.7</code> and update existing managed npm installs instead of failing on the package directory. Fixes #79884. (#80390) Thanks @brokemac79.</li>
|
||||
<li>Anthropic: reseed Claude CLI fresh-session retries from bounded OpenClaw transcript history after session rotation, preventing conversation amnesia. Fixes #80905. (#80934) Thanks @bitloi.</li>
|
||||
<li>Require explicit browser device pairing [AI]. (#81289) Thanks @pgondhi987.</li>
|
||||
<li>Require Control UI pairing before proxy-scoped access [AI]. (#81288) Thanks @pgondhi987.</li>
|
||||
<li>Installer: honor <code>--version</code> for git installs and install from the checked-in lockfile, preventing recent dependency pins from tripping pnpm's minimum-release-age gate during tag installs.</li>
|
||||
<li>Agents: deliver same-process subagent completion handoffs through the in-process agent dispatcher instead of opening a Gateway RPC loopback.</li>
|
||||
<li>Harden trusted-proxy source validation [AI]. (#81290) Thanks @pgondhi987.</li>
|
||||
<li>Agents: add permissive item schemas to array tool parameters before provider submission, preventing OpenAI-compatible schema validation from rejecting plugin tools that omit <code>items</code>. Fixes #81175. (#81217) Thanks @JARVIS-Glasses.</li>
|
||||
<li>Agents: escalate LLM idle watchdog timeouts through profile rotation and configured model fallback instead of leaving agent turns stuck after a silent model stream. Fixes #76877. (#80449) Thanks @jimdawdy-hub.</li>
|
||||
<li>Discord voice: treat OpenAI Realtime startup auth failures as fatal, suppress duplicate realtime error logs, and stop autoJoin from retrying the same broken voice channel until credentials are fixed.</li>
|
||||
<li>ACPX: stop forwarding unsupported timeout config options to Claude ACP while preserving OpenClaw's own turn timeout. (#80812) Thanks @sxxtony.</li>
|
||||
<li>Session transcripts: redact sensitive message content in the centralized JSONL append path so CLI turns, gateway transcript injection, transcript mirrors, and guarded tool results use the same configured redaction behavior. Fixes #73565. Refs #73563. (#79645) Thanks @Ziy1-Tan.</li>
|
||||
<li>Channels/iMessage: ignore Apple link-preview plugin payload attachments when users paste URLs, keeping the URL text while avoiding phantom media context. (#79374) Thanks @homer-byte.</li>
|
||||
<li>Telegram: detect polling stalls from <code>getUpdates</code> liveness only, so outbound API calls no longer mask dead inbound polling; log polling-cycle starts after transport rebuilds. Fixes #78473.</li>
|
||||
<li>fix: scan plugin runtime entries during install [AI]. (#80998) Thanks @pgondhi987.</li>
|
||||
<li>fix(plugins): scan installed dependency runtime code [AI]. (#81066) Thanks @pgondhi987.</li>
|
||||
<li>Inherit tool restrictions for delegated sessions [AI]. (#80979) Thanks @pgondhi987.</li>
|
||||
<li>Telegram: discard legacy long-poll update offsets that cannot be tied to the current bot token, so token rotation no longer leaves bots silently skipping new messages. (#80671) Thanks @sxxtony.</li>
|
||||
<li>browser: enforce navigation checks for act interactions [AI]. (#81070) Thanks @pgondhi987.</li>
|
||||
<li>Validate node exec event provenance [AI]. (#81071) Thanks @pgondhi987.</li>
|
||||
<li>Gateway: keep active reply runs visible to stuck-session diagnostics and clear no-active-work recovery state, preventing stale queued lanes after compaction or tool failures. Fixes #80677. (#81302)</li>
|
||||
<li>Codex app-server: rotate incompatible context-engine-managed native threads so Lossless-managed sessions do not resume stale hidden Codex history. (#81223) Thanks @jalehman.</li>
|
||||
<li>Codex cron: execute scheduled command-style automation payloads before workspace bootstrap or memory review, preserving existing isolated cron jobs after Codex harness migration. (#81510) Thanks @jalehman.</li>
|
||||
<li>Plugin LLM completions: honor Codex agent-runtime policy for canonical OpenAI model refs, so context-engine summarizers can use Codex OAuth instead of requiring direct <code>OPENAI_API_KEY</code> auth. (#81511) Thanks @jalehman.</li>
|
||||
<li>Gateway/OpenAI HTTP: return OpenAI-compatible 400 errors for invalid sampling params and provider validation failures instead of collapsing them to 500s. (#81275) Thanks @Lellansin.</li>
|
||||
<li>Telegram: publish plugin and skill command description localizations to native command menus while filtering unsupported locale codes and preserving Telegram command limits. (#81351) Thanks @jzakirov.</li>
|
||||
<li>Limit hook CLI tool authority [AI]. (#81065) Thanks @pgondhi987.</li>
|
||||
<li>Require admin scope for node device token management [AI]. (#81067) Thanks @pgondhi987.</li>
|
||||
<li>Restrict chat sender allowlist matching [AI]. (#80898) Thanks @pgondhi987.</li>
|
||||
<li>Update: suppress the false newer-config warning during restart health probing after an update handoff, while keeping future-version mutation guards intact. (#78652)</li>
|
||||
<li>Sessions: redact persisted tool result detail metadata before writing transcripts so diagnostic secrets do not survive tool output redaction. (#80444) Thanks @nimbleenigma.</li>
|
||||
<li>Codex runtime: allow the official installed <code>@openclaw/codex</code> package to use its private task-runtime and MCP projection SDK helpers, fixing <code>MODULE_NOT_FOUND</code> during migrated OpenAI/Codex beta runs.</li>
|
||||
<li>Codex migration: make Enter activate the highlighted checkbox row before continuing, so <code>Skip for now</code> and bulk-selection rows work even when planned items start preselected.</li>
|
||||
<li>Codex harness: keep auth-profile-backed media tools such as <code>image_generate</code> available when OpenAI auth lives in the agent's auth-profile store instead of environment variables.</li>
|
||||
<li>WhatsApp/install: allow Baileys' pinned libsignal git subdependency under pnpm 11 so source installs and local checks can complete.</li>
|
||||
<li>Require auth for sandbox browser CDP relay [AI]. (#81002) Thanks @pgondhi987.</li>
|
||||
<li>fix: detect carried exec command forms [AI]. (#81000) Thanks @pgondhi987.</li>
|
||||
<li>Reject truncated exec approval commands [AI]. (#81001) Thanks @pgondhi987.</li>
|
||||
<li>Enforce inline shell wrapper payload matching [AI]. (#80978) Thanks @pgondhi987.</li>
|
||||
<li>fix(node-pairing): replace changed pending requests [AI]. (#80894) Thanks @pgondhi987.</li>
|
||||
<li>Rate limit Google Chat webhook requests [AI]. (#80974) Thanks @pgondhi987.</li>
|
||||
<li>Docker: mount the auth-profile secret key directory so OAuth-backed auth profiles survive container rebuilds. (#80991)</li>
|
||||
<li>Onboarding: accept Codex auth profiles for canonical OpenAI model checks, avoiding false missing-auth warnings. (#80913) Thanks @rubencu.</li>
|
||||
<li>fix(feishu): normalize webhook rate-limit client keys [AI]. (#80975) Thanks @pgondhi987.</li>
|
||||
<li>fix(auth): prevent bootstrap pairing scope changes [AI]. (#80976) Thanks @pgondhi987.</li>
|
||||
<li>Validate Control UI loopback retry endpoints [AI]. (#80900) Thanks @pgondhi987.</li>
|
||||
<li>Harden exported markdown link rendering [AI]. (#80902) Thanks @pgondhi987.</li>
|
||||
<li>fix(gateway): honor minimal discovery mode for wide-area DNS-SD [AI]. (#80903) Thanks @pgondhi987.</li>
|
||||
<li>slack: enforce reaction notification policy [AI]. (#80907) Thanks @pgondhi987.</li>
|
||||
<li>Enforce gateway command scopes by caller context [AI]. (#80891) Thanks @pgondhi987.</li>
|
||||
<li>Telegram/groups: in single-account setups, treat an explicit empty <code>accounts.<id>.groups: {}</code> map the same as undefined so the root <code>channels.telegram.groups</code> allowlist still applies, instead of silently dropping every group update under the default <code>groupPolicy: "allowlist"</code>. Multi-account semantics are unchanged so per-account explicit-empty groups still scope-disable a single account without affecting siblings; the explicit way to block all groups for any account remains <code>groupPolicy: "disabled"</code>. Fixes #79427. (#81030) Thanks @kinjitakabe.</li>
|
||||
<li>Codex (app-server): project user-configured <code>mcp.servers</code> into new Codex thread configs, matching the codex-cli runtime's existing <code>-c mcp_servers=...</code> behavior so app-server-runtime agents see the same user MCP servers the CLI runtime already exposes. Plugin-curated apps remain attached via the separate <code>apps</code> config patch. Fixes #80814. Thanks @kinjitakabe.</li>
|
||||
<li>Enforce Slack plugin approval button authorization [AI]. (#80899) Thanks @pgondhi987.</li>
|
||||
<li>Recognize PowerShell -ec inline commands [AI]. (#80893) Thanks @pgondhi987.</li>
|
||||
<li>fix(qqbot): authorize approval button callbacks [AI]. (#80892) Thanks @pgondhi987.</li>
|
||||
<li>Telegram: render supported HTML tags in streamed and durable replies instead of showing literal markup. (#80977)</li>
|
||||
<li>Scrub streamable MCP redirect headers [AI]. (#80906) Thanks @pgondhi987.</li>
|
||||
<li>fix(memory-wiki): require admin scope for ingest [AI]. (#80897) Thanks @pgondhi987.</li>
|
||||
<li>memory-wiki: require write scope for Obsidian search [AI]. (#80904) Thanks @pgondhi987.</li>
|
||||
<li>WhatsApp/install: allow Baileys' pinned libsignal git subdependency under pnpm 11 so source installs and local checks can complete.</li>
|
||||
<li>WhatsApp: externalize the channel as a ClawHub/npm plugin outside the core npm runtime bundle, and bump Baileys to <code>7.0.0-rc11</code> so libsignal resolves from the registry instead of a GitHub tarball.</li>
|
||||
<li>WhatsApp: keep optional audio decoding dependencies local to the external plugin so the core npm install no longer pulls WhatsApp-only media helpers.</li>
|
||||
<li>Build: skip copied metadata for bundled plugins that are excluded from build entries, preventing update/status rebuilds from advertising missing QQ Bot runtime files. (#80925)</li>
|
||||
<li>Control UI/sessions: nest subagent sessions under their parent session in the session picker dropdown using a visual <code>└─ </code> prefix, making the parent-child relationship clear. Fixes #77628. (#78623) Thanks @chinar-amrutkar.</li>
|
||||
<li>Auto-reply: surface a visible error when the configured model backend fails and fallback produces no visible reply, while preserving intentional silent turns and side-effect-only deliveries. (#80917) Thanks @dutifulbob.</li>
|
||||
<li>Agents/exec: skip redundant heartbeat wake-ups for subagent session exec completions, preventing spurious LLM invocations on parent sessions. Fixes #66748. (#66749) Thanks @ggzeng.</li>
|
||||
<li>Provider streams: keep OpenAI-compatible SSE and JSON fallback streams draining across split chunks and fail Azure Responses streams with a bounded first-event diagnostic instead of stalling. Refs #80926. (#80927) Thanks @galiniliev and @CaptainTimon.</li>
|
||||
<li>Agents: rewrite generic provider internal errors with support request IDs into user-friendly transient error copy. (#49401) Thanks @y471823206.</li>
|
||||
<li>WhatsApp: finish handling pending debounced inbound messages before closing the socket. (#81246) Thanks @mcaxtr.</li>
|
||||
<li>CLI/commitments: write <code>--json</code> output to stdout instead of diagnostic logs so automation can parse commitment list and dismiss results. (#81215) Thanks @giodl73-repo.</li>
|
||||
<li>Update: allow pnpm GitHub-source OpenClaw updates to approve the OpenClaw package build, so source installs complete their prepare/prepack lifecycle. (#81294) Thanks @fuller-stack-dev.</li>
|
||||
<li>Telegram: preserve supported HTML tags in visible replies and durable mirrors so formatted messages render correctly instead of degrading to escaped text. (#80977) Thanks @obviyus.</li>
|
||||
<li>Plugins/runtime: attribute deprecated runtime config load/write warnings to the plugin id and source that triggered them so logs and plugin doctor runs are actionable. Refs #81394. (#81425) Thanks @BKF-Gitty.</li>
|
||||
<li>Agents/cron: honor a cron payload's explicit <code>timeoutSeconds</code> for the LLM idle watchdog even when it numerically equals <code>agents.defaults.timeoutSeconds</code>, preserving explicit per-run timeout intent and preventing stalled streaming replies from being cut to the implicit 120s cap. (#79426) Thanks @legolaz8451.</li>
|
||||
<li>Codex app-server: keep the short post-tool completion watchdog armed across dynamic tool completion bookkeeping so embedded Codex runs fail fast and release their session lane when Codex goes quiet after a tool result. (#81697) Thanks @mbelinky.</li>
|
||||
</ul>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Gateway/OpenAI HTTP: honor <code>max_completion_tokens</code> and <code>max_tokens</code> on inbound <code>/v1/chat/completions</code> requests so client-provided token caps reach the upstream provider via <code>streamParams.maxTokens</code>, with <code>max_completion_tokens</code> taking precedence when both are sent. Thanks @Lellansin.</li>
|
||||
<li>Models/OpenAI CLI auth: make <code>openclaw models auth login --provider openai</code> start the ChatGPT/Codex account login by default, while <code>--method api-key</code> remains the explicit OpenAI API-key setup path.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside explicit SDK OAuth auth-result config patches, so provider helpers emit <code>google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside SDK OAuth auth-result default config patches, so helper-built provider auth flows emit <code>google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids returned by direct <code>openclaw models auth login --set-default</code> provider auth flows before writing config, so Gemini testing targets <code>google/gemini-3.1-pro-preview</code>.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids in per-agent config defaults and auth patches, so agent-specific emitted config keeps targeting <code>google/gemini-3.1-pro-preview</code>.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids in provider catalog rows when API-key onboarding only reapplies the agent default, so emitted config keeps testing <code>google/gemini-3.1-pro-preview</code>.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids in <code>config set</code> mutation output for agent overrides and provider catalog rows, so current config emits <code>google/gemini-3.1-pro-preview</code>.</li>
|
||||
<li>Google/Gemini: canonicalize provider-qualified retired Gemini 3 Pro Preview refs during Google forward-compatible model resolution, so emitted config uses <code>google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
|
||||
<li>Google/Gemini: normalize proxy-prefixed retired Gemini 3 Pro Preview catalog rows, so emitted configs use <code>google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside per-agent model overrides before writing config, so agent-specific config emits <code>google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids in subagent, heartbeat, compaction, and subagent-tool model config during writes, so current config keeps emitting <code>google/gemini-3.1-pro-preview</code>.</li>
|
||||
<li>Docs/subagents: document <code>agents.defaults.subagents.announceTimeoutMs</code> in the sub-agent and configuration references. (#75509) Thanks @akrimm702.</li>
|
||||
<li>Cron: add direct <code>cron.get</code>, <code>openclaw cron get <id></code>, and agent-tool <code>get</code> support for inspecting one stored cron job by id. (#75117) Thanks @samzong.</li>
|
||||
<li>Agents/tools: add per-sender tool policies with canonical channel-scoped sender keys, so operators can restrict dangerous tools by requester identity across global, agent, group, core, bundled, and plugin tool surfaces. (#66933) Thanks @JerranC.</li>
|
||||
<li>ACP: expose Gateway session lineage metadata through ACP session listings and session info snapshots so clients can render subagent graphs without private Gateway side channels. (#73458) Thanks @samzong.</li>
|
||||
<li>Channels/iMessage: add <code>openclaw channels status --channel <name></code> filtering and document the BlueBubbles-to-imsg cutover path so operators can probe iMessage without starting both channel monitors. (#80706) Thanks @omarshahine.</li>
|
||||
<li>CI: add a non-blocking <code>plugin-inspector-advisory</code> artifact to Plugin Prerelease so release runs capture bundled plugin compatibility triage without changing the blocking gate.</li>
|
||||
<li>Runtime/Fly: detect Fly Machines as container environments from their runtime env vars, so gateway bind and Bonjour defaults match remote container launches. (#80209) Thanks @liorb-mountapps.</li>
|
||||
<li>Providers/fal: route GPT Image 2 and Nano Banana 2 reference-image edit requests to <code>/edit</code> with <code>image_urls</code> array, enforce NB2 edit geometry using <code>aspect_ratio</code> and <code>resolution</code> params, lift Fal edit mode input-image caps to 10 for GPT Image 2 and 14 for Nano Banana 2, and allow aspect-ratio hints in edit mode. (#77295) Thanks @leoge007.</li>
|
||||
<li>Control UI: show a plain HTML recovery panel when the app module never registers, giving blank dashboard pages a retry path and browser-extension troubleshooting link. Fixes #44107. Thanks @BunsDev.</li>
|
||||
<li>Docs: rename the broad tools nav to Capabilities, keep automation and agent coordination as sections, and keep the tools overview focused on tools, skills, and plugins. https://docs.openclaw.ai/tools</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>Build: enable additional low-churn oxlint rules for promise, TypeScript, and runtime footgun checks.</li>
|
||||
<li>Build: enable stricter Vitest lint rules for focused, disabled, conditional, hook, matcher, and expectation hazards.</li>
|
||||
<li>Build: pin explicit oxfmt defaults in the shared formatter config to keep formatting behavior stable across upgrades.</li>
|
||||
<li>TypeScript: enable stricter compiler checks for implicit returns, side-effect imports, overrides, and unused production code.</li>
|
||||
<li>Logging: add targeted model transport, payload, SSE, and code-mode diagnostics with redacted URL handling.</li>
|
||||
<li>Agents: allow <code>session.agentToAgent.maxPingPongTurns</code> up to 20 while keeping the default at 5 for longer agent-to-agent reply chains. Fixes #52382. (#52400) Thanks @thirumaleshp.</li>
|
||||
<li>Agents: add per-agent <code>tools.message.crossContext</code> overrides so sandboxed/public agents can restrict message sends to the current conversation without changing the global bot policy.</li>
|
||||
<li>Agents: add per-agent <code>tools.message.actions.allow</code> overrides so sandboxed/public agents can expose and enforce send-only message tools.</li>
|
||||
<li>Agents: omit the sandbox workspace marker from compact command progress previews while keeping internal sandbox diagnostics unchanged.</li>
|
||||
<li>Agents: widen progress draft command preview lines by 50% so Discord inline tool updates preserve more useful command context.</li>
|
||||
<li>Codex app-server: retire timed-out app-server clients after bounded turn interrupts so Discord agents do not reuse a CPU-spinning Codex process after an attempt timeout.</li>
|
||||
<li>Codex app-server: default migrated native plugin destructive-action policy to enabled while preserving explicit global and per-plugin false overrides.</li>
|
||||
<li>Build: upgrade workspace package management to pnpm 11 and keep Docker, install, update, and release workflows on the pnpm 11 config surface. (#79414) Thanks @altaywtf.</li>
|
||||
<li>Build: align Telegram QA workflows and git source installs with the pnpm 11 workspace build allowlist surface. (#80588) Thanks @altaywtf.</li>
|
||||
<li>Models: add provider-level <code>localService</code> startup for on-demand local model servers before OpenAI-compatible requests, including one-shot model probes.</li>
|
||||
<li>Agents: trim default system prompt guidance and send-only message tool schemas to reduce prompt tokens while preserving GPT-5 personality guidance.</li>
|
||||
<li>Context: add <code>/context map</code> to send a treemap image of the current session context contributors. (#79867)</li>
|
||||
<li>Slack: add <code>unfurlLinks</code> and <code>unfurlMedia</code> config for bot <code>chat.postMessage</code> replies, including per-account overrides, so Slack link and media previews can be suppressed without workspace-wide settings. Fixes #48435. (#80145) Thanks @esegev1 and @HemantSudarshan.</li>
|
||||
<li>Slack: add explicit <code>replyBroadcast</code> support for text and Block Kit thread replies so agents can opt into Slack's parent-channel <code>reply_broadcast</code> behavior. (#64365) Thanks @tony88331.</li>
|
||||
<li>Slack: preserve mention target/source metadata in inbound prompt context so agents can distinguish direct bot mentions from implicit thread wakes that mention someone else. Fixes #79025. (#75356) Thanks @tmimmanuel.</li>
|
||||
<li>Slack: canonicalize outbound delivery-mirror routes for native DM channel IDs to the peer user session so <code>message.send</code> calls to <code>D...</code> targets do not split the same Slack DM thread into a channel session. Fixes #80091. (#80111) Thanks @bek91.</li>
|
||||
<li>Plugin SDK: deprecate public subpaths that existed for at least one month and have no bundled extension production imports, keep legacy barrel/test/zod subpath package exports for backwards compatibility, and track both sets in the SDK surface report.</li>
|
||||
<li>Plugin SDK: deprecate public subpaths currently used by only one or two bundled plugin owners, keeping them importable while steering new plugin code to focused shared SDK seams or plugin-owned APIs.</li>
|
||||
<li>Plugin SDK: remove the owner-specific <code>provider-auth-login</code> public subpath after moving Chutes, GitHub Copilot, and OpenAI Codex auth flows back to provider-owned modules.</li>
|
||||
<li>Plugin SDK: remove provider-specific model, stream, and xAI compatibility helpers from public exports after moving bundled callers to provider-owned modules.</li>
|
||||
<li>Plugin SDK: expose runtime-supplied active model metadata to native plugin tool factories for diagnostics and plugin-owned policy decisions. Fixes #77857. Thanks @jamiezigelbaum.</li>
|
||||
<li>QA/Mantis: add Telegram live PR evidence automation with Convex-leased credentials, Crabbox transcript capture, motion GIF previews, and inline PR comments.</li>
|
||||
<li>QA/Mantis: add a Telegram desktop scenario builder that leases Crabbox, installs native Telegram Desktop, configures an OpenClaw Telegram gateway with leased bot credentials, and records VNC screenshot/video artifacts.</li>
|
||||
<li>Discord/voice: add realtime voice diagnostics for speaker turns, playback resets, barge-in detection, and audio cutoff analysis.</li>
|
||||
<li>Talk: add <code>talk.realtime.instructions</code> so operators can append realtime voice style instructions while preserving OpenClaw's built-in agent-consult guidance. (#79081) Thanks @VACInc.</li>
|
||||
<li>Discord/voice: default test and source installs to the pure-JS <code>opusscript</code> decoder by ignoring optional native <code>@discordjs/opus</code> builds, avoiding slow native addon compiles outside dedicated voice-performance lanes.</li>
|
||||
<li>Discord/voice: add an opt-in native <code>@discordjs/opus</code> install script and decoder preference for live voice-performance lanes without charging unrelated Docker/tests for native addon builds.</li>
|
||||
<li>Discord/voice: add <code>voice.allowedChannels</code> to restrict voice joins and bot voice-state moves to configured channels while preserving open voice behavior when unset.</li>
|
||||
<li>Gateway/skills: add an opt-in private skill archive upload install path gated by <code>skills.install.allowUploadedArchives</code>, so trusted Gateway clients can stage and install zip-backed skills only when operators explicitly enable the code-install surface. (#74430) Thanks @samzong.</li>
|
||||
<li>Codex app-server: enable Codex native code-mode-only for harness threads so deferred OpenClaw dynamic tools run through Codex's own searchable code execution surface instead of a PI-style wrapper.</li>
|
||||
<li>Dependencies: refresh workspace pins and patch targets, including ACPX <code>@agentclientprotocol/claude-agent-acp</code> <code>0.33.1</code>, Codex ACP <code>0.14.0</code>, Baileys <code>7.0.0-rc10</code>, Google GenAI <code>2.0.1</code>, OpenAI <code>6.37.0</code>, AWS SDK <code>3.1045.0</code>, Kysely <code>0.29.0</code>, Tlon skill <code>0.3.6</code>, Aimock <code>1.19.5</code>, and tsdown <code>0.22.0</code>.</li>
|
||||
<li>Dependencies: refresh workspace pins for Anthropic SDK, Smithy shared ini loading, Playwright, YAML, Aimock, TypeScript native preview, Vitest, Oxlint/Oxfmt, Vite, and pnpm 11.1.0.</li>
|
||||
<li>Dependencies: hard-pin non-peer direct dependency specs across bundled packages and add a changed-check guard so runtime installs resolve the exact versions tested by maintainers.</li>
|
||||
<li>Dependencies: move embedded Pi packages to the <code>@earendil-works</code> namespace, refresh Twitch Twurple packages, and move <code>@openclaw/fs-safe</code> from the GitHub release pin to the published npm package.</li>
|
||||
<li>Build: route Testbox changed-check delegation through Crabbox and remove the OpenClaw-specific Blacksmith Testbox helper scripts.</li>
|
||||
<li>Agents/compaction: preserve scoped background exec/process session references across embedded compaction and after-turn runtime contexts without exposing sessions from unrelated scopes. Fixes #79284. (#79307) Thanks @TurboTheTurtle.</li>
|
||||
<li>Agents/process: tell agents to inspect background sessions with <code>process log</code> before sending interactive input and to use <code>waitingForInput</code>/<code>stdinWritable</code> hints from <code>log</code>/<code>poll</code>.</li>
|
||||
<li>CLI/onboarding: improve setup, onboarding, configure, and channel command wayfinding so terminal flows explain the next useful command instead of relying on terse setup labels.</li>
|
||||
<li>Agents/Codex: remove the configurable Codex dynamic-tools profile so Codex app-server always owns workspace, edit, patch, exec, process, and plan tools while OpenClaw integration tools remain available.</li>
|
||||
<li>macOS app: update the Peekaboo bridge dependency to Peekaboo 3.0.0.</li>
|
||||
<li>Dependencies: refresh workspace pins and move the WhatsApp plugin from <code>@whiskeysockets/baileys</code> to <code>baileys</code> while keeping the <code>7.0.0-rc10</code> runtime.</li>
|
||||
<li>Plugin SDK: add bundled-plugin session actions, <code>sendSessionAttachment</code>, and Cron-backed <code>scheduleSessionTurn</code>/tag cleanup under the grouped session namespace. Replaces #75578/#75581/#75588 and part of #73384/#74483. Thanks @100yenadmin.</li>
|
||||
<li>Plugin SDK/media-understanding: add <code>extractStructuredWithModel(...)</code> plus the optional provider-side <code>extractStructured(...)</code> seam so trusted plugins can run bounded image-first structured extraction with optional supplemental text context through provider-owned runtimes such as Codex.</li>
|
||||
<li>Exec approvals: add <code>tools.exec.commandHighlighting</code> so parser-derived command highlighting in approval prompts can be enabled globally or per agent. (#79348) Thanks @jesse-merhi.</li>
|
||||
<li>Codex app-server: mirror native Codex subagent spawn lifecycle events into Task Registry so app-server child agents appear in task/status surfaces without relying on transcript text. (#79512) Thanks @mbelinky.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>CLI/media: render terminal QR codes with full-block characters by default so the bundled <code>qrcode</code> terminal renderer does not emit a pathologically dense ANSI final row in compact half-block mode that breaks scanning in some terminals. Fixes #77820. Thanks @KrasimirKralev.</li>
|
||||
<li>Agents/compaction: read post-compaction AGENTS.md refresh context from the queued run workspace instead of the runner process cwd, so CLI-backed follow-up turns re-inject the correct workspace startup rules after compaction. Fixes #70541. (#75532) Thanks @vyctorbrzezowski.</li>
|
||||
<li>Agents/read tool: treat positive offsets beyond EOF as empty ranges instead of surfacing the upstream read error, so stale pagination cursors no longer crash tool calls while unrelated read failures still fail loud. Fixes #62466. (#75536) Thanks @vyctorbrzezowski.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview refs left in Google API-key onboarding model allowlists and fallbacks, so setup-emitted config keeps testing <code>google/gemini-3.1-pro-preview</code> instead of <code>google/gemini-3-pro-preview</code>.</li>
|
||||
<li>Telegram/context: bound selected topic context to the active session so messages from before <code>/new</code> or <code>/reset</code> are not replayed into later turns. (#80848) Thanks @VACInc.</li>
|
||||
<li>Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids when resolving exact configured proxy-provider refs, so <code>kilocode/google/gemini-3-pro-preview</code> resolves to <code>kilocode/google/gemini-3.1-pro-preview</code> for Gemini 3.1 testing.</li>
|
||||
<li>CLI: strip generic OSC terminal escape payloads from sanitized output fields, preventing clipboard/title escape bodies from leaking into commitment tables and other terminal-safe text. Thanks @shakkernerd.</li>
|
||||
<li>Codex app-server: match connector-backed plugin approval elicitations by stable connector id so enabled destructive actions no longer fall through to display-name-only rejection.</li>
|
||||
<li>Build: replace selected build utility <code>tsx</code> preloads with Node native type stripping so Node 26 build paths no longer emit <code>DEP0205</code> module loader deprecation warnings. (#78584) Thanks @keshavbotagent.</li>
|
||||
<li>Media generation: honor configured music and video generation timeouts when tool calls omit <code>timeoutMs</code>, matching image generation behavior. (#80687)</li>
|
||||
<li>CLI/update/status: label beta-channel plugin fallback and model-pricing refresh failures as warnings, keeping mixed beta/latest plugin cohorts visible without making core update or Gateway reachability look failed. Fixes #80689. Thanks @BKF-Gitty.</li>
|
||||
<li>Doctor/plugins: relink managed npm plugin <code>openclaw</code> peer dependencies during <code>doctor --fix</code>, while refusing to follow package-local <code>node_modules</code> symlinks outside the plugin package. (#77412) Thanks @TheCrazyLex.</li>
|
||||
<li>iMessage: route inbound tapbacks as reaction system events instead of normal messages, defaulting to bot-authored-message notifications while allowing <code>reactionNotifications: "off" | "own" | "all"</code> overrides. Fixes #60274; refs #39031 and #39322. Thanks @hyperclaw.</li>
|
||||
<li>Control UI/performance: scope Nodes polling to the active Nodes tab, debounce stale session-list reconciliation, and bound chat-side session refreshes so long-running dashboards avoid background reload churn. Thanks @BunsDev.</li>
|
||||
<li>Plugins/channels: explain bundled channel entry files that reach the legacy plugin loader as setup-runtime loader mismatches instead of generic missing-register failures. Thanks @chinar-amrutkar.</li>
|
||||
<li>Plugins/session-end: fire a typed <code>session_end</code> plugin hook with reason <code>shutdown</code> (or <code>restart</code> when a restart is expected) for every session that was still active when the gateway process stops. Previously SIGTERM/SIGINT/restart paths closed the gateway without enumerating active sessions, leaving downstream <code>session_end</code> plugins (e.g. claude-mem) with ghost rows accumulating across restarts. The new shutdown finalizer drains an in-memory tracker that is populated by <code>session_start</code> and forgotten by replace / reset / delete / compaction emitters, so previously-finalized sessions are never double-fired. The drain is bounded to a 2 s total budget so a slow plugin cannot block process exit. Adds <code>"shutdown"</code> and <code>"restart"</code> to <code>PluginHookSessionEndReason</code>. Fixes #57790. Thanks @pandadev66.</li>
|
||||
<li>Codex app-server: clamp Codex code-mode sandboxing to workspace-write when an OpenClaw sandbox is active, preventing Docker gateway socket access from becoming a danger-full-access Codex turn.</li>
|
||||
<li>TUI: exit immediately on Ctrl+C/SIGINT after gateway disconnect and bound shutdown drain so terminal teardown cannot strand sessions. Fixes #75379. (#75381) Thanks @udaymanish6.</li>
|
||||
<li>Matrix: default outbound markdown tables to bullet lists instead of fenced code blocks. Fixes #78990. (#80890) Thanks @kinjitakabe.</li>
|
||||
<li>Bonjour/Gateway: treat active ciao probing and fresh name-conflict renames as in-progress so the mDNS watchdog waits for probe settlement before retrying, preventing rapid re-advertise loops on Windows, WSL, and other multicast-hostile hosts. (#74778) Refs #74242. Thanks @fuller-stack-dev.</li>
|
||||
<li>Providers/MiniMax: send a minimal Anthropic-compatible user fallback when message conversion filters a turn to an empty payload, so MiniMax M2.7 no longer returns <code>chat content is empty</code> after tool-heavy sessions. Fixes #74589. Thanks @neeravmakwana and @DerekEXS.</li>
|
||||
<li>Tools/media: preserve implicit allow-all semantics from <code>tools.alsoAllow</code>-only policies when preconstructing built-in media generation and PDF tools, so configured media tools become live without forcing <code>tools.allow: ["*", ...]</code>. Fixes #77841. Thanks @trialanderrorstudios.</li>
|
||||
<li>Codex/Telegram: separate code-mode tool progress from final replies, render bridged tool calls with native tool labels, and repair persisted missing tool results for safer follow-up turns. (#80663) Thanks @jalehman.</li>
|
||||
<li>Memory/search: load the platform-specific <code>sqlite-vec-<platform>-<arch></code> variant directly when the meta <code>sqlite-vec</code> package is missing from a global install, so vector recall keeps working on <code>npm install -g openclaw@latest</code> upgrades where optionalDependencies left only the platform variant on disk. Fixes #77838. Thanks @corevibe555 and @Simon2256928.</li>
|
||||
<li>Cron: keep long manual cron runs active in the task registry until completion, preventing transient <code>lost</code> markers before durable recovery reconciles. Fixes #78233. (#78243) Thanks @Feelw00.</li>
|
||||
<li>Doctor/GitHub CLI: surface a <code>GH_CONFIG_DIR</code> hint when the GitHub skill is usable but <code>gh</code> auth lives under a different operator HOME than the agent process, without warning for disabled or filtered skills. Fixes #78063. (#78095) Thanks @tmimmanuel.</li>
|
||||
<li>Gateway: dedupe concurrent <code>send</code>, <code>poll</code>, and <code>message.action</code> requests while delivery is still in flight, preventing duplicate outbound work for the same idempotency key. (#68341) Thanks @thesomewhatyou.</li>
|
||||
<li>Cron: keep main-session <code>systemEvent</code> heartbeat wakes on their bound session route for both direct and queued wake paths by dropping inherited explicit heartbeat destinations when forcing <code>target: "last"</code>. Fixes #73900. Thanks @richardmqq.</li>
|
||||
<li>Telegram: honor forced document delivery for video media so <code>--force-document</code> sends MP4s as documents instead of typed videos. Fixes #80389. (#80405) Thanks @jbetala7.</li>
|
||||
<li>Gateway: clear speculative node wake state when APNs registration is missing, preventing unregistered or mistyped node IDs from retaining wake throttle entries. Fixes #68847. (#68848) Thanks @Feelw00.</li>
|
||||
<li>Auto-reply: keep late follow-up queue drain finalizers from deleting a replacement queue registered after <code>/stop</code>, preventing immediate follow-up messages from being orphaned. Fixes #68838. (#68839) Thanks @Feelw00.</li>
|
||||
<li>Feishu: make manual App ID/App Secret setup the default channel-binding path while keeping QR scan-to-create as an optional best-effort flow, and document the manual fallback for domestic Feishu mobile clients that do not react to the QR code. Fixes #80591. Thanks @wei-wei-zhao.</li>
|
||||
<li>Memory: cap dreaming promotion writes to <code>MEMORY.md</code> by compacting oldest auto-promoted sections while preserving user-authored notes, keeping active memory below the bootstrap budget. Fixes #73691. (#74088) Thanks @YB0y.</li>
|
||||
<li>Telegram: show resolved thinking defaults in native <code>/status</code> and <code>/think</code> menus while preserving explicit session overrides. (#80341) Thanks @VACInc.</li>
|
||||
<li>Channels: cache selected channel registry lookups against the active fallback snapshot so pinned-empty registries refresh native command and alias routing after active registry swaps. (#80333) Thanks @samzong.</li>
|
||||
<li>Codex app-server: reuse native Codex CLI OAuth for isolated app-server harness login, refresh, and app inventory cache keys so ChatGPT-authenticated Codex runs no longer fall back to unauthenticated OpenAI API calls. (#79877) Thanks @jeffjhunter.</li>
|
||||
<li>Gateway: scope <code>sessions.resolve</code> sessionId and label store loads to the requested agent so large unrelated agent stores are not parsed for scoped lookups. Fixes #51264. (#79474) Thanks @samzong.</li>
|
||||
<li>Gateway: share serialized streaming event envelopes across eligible WebSocket and node subscribers while preserving per-client sequence numbers. (#80299) Thanks @samzong.</li>
|
||||
<li>Gateway: consolidate duplicate <code>openclaw doctor</code> service config panels while preserving the declined-repair <code>--force</code> hint. Fixes #80287. (#78688) Thanks @YB0y.</li>
|
||||
<li>Browser: report Chrome MCP existing-session page readiness in browser status without letting status probes exceed the client timeout. Fixes #80268. (#80280) Thanks @ai-hpc.</li>
|
||||
<li>WhatsApp: route opening-phase Baileys 428 connectionClosed through the WhatsApp reconnect policy and keep post-open 428 closes retryable, so transient setup socket closes retry with WhatsApp diagnostics instead of escaping as a bare <code>channel exited</code> error. Fixes #75736; mitigates #77443. Thanks @dataCenter430.</li>
|
||||
<li>Agents: disable Pi's default filesystem resource discovery for embedded runs while keeping OpenClaw inline extension factories active, avoiding Windows event-loop stalls during first WhatsApp-triggered agent startup. Fixes #77443. Thanks @dataCenter430.</li>
|
||||
<li>Providers/self-hosted: read model-scoped llama.cpp runtime context from <code>/props.default_generation_settings.n_ctx</code> while keeping top-level <code>n_ctx</code> as a fallback, so session budgeting reflects the loaded context window. Fixes #73664. (#74057) Thanks @brokemac79.</li>
|
||||
<li>Memory: reject symlinked directory components in configured extra memory paths before reading Markdown files. (#80331) Thanks @samzong.</li>
|
||||
<li>Sessions/transcripts: replace whole-file <code>readFile</code> scans with shared streaming helpers (<code>streamSessionTranscriptLines</code> and <code>streamSessionTranscriptLinesReverse</code>) for idempotency lookup, latest/tail assistant text reads, delivery-mirror dedupe, and compaction fork loading, so long-running sessions no longer materialize the full transcript in memory. Forward scans use <code>readline</code> over a bounded <code>createReadStream</code>; reverse scans read bounded chunks from the file end and decode complete JSONL lines newest-first without a fixed tail cap. Synthetic 200 MiB transcript: peak RSS delta drops from +252 MiB to +27 MiB while preserving malformed-line tolerance and idempotency-key return semantics. Fixes #54296. Thanks @jack-stormentswe.</li>
|
||||
<li>Browser/CDP: filter browser-internal targets from raw CDP and persistent Playwright tab selection so navigation opens real page tabs. Fixes #55734. Thanks @Demine4.</li>
|
||||
<li>WhatsApp: apply hot-reloaded <code>dmPolicy</code> and <code>allowFrom</code> settings to the active Web listener before processing new inbound DMs. Fixes #80538. Thanks @Ampaskopi129.</li>
|
||||
<li>Plugins: let <code>openclaw doctor --fix</code> repair managed plugin installs whose package entrypoints fail package-directory boundary validation after local state moves. Fixes #80592. Thanks @wei-wei-zhao.</li>
|
||||
<li>Voice-call: resume voice-originated exec approval follow-ups as internal non-delivery turns instead of rejecting them as <code>unknown channel: voice</code>. Fixes #80540. Thanks @patrickmch.</li>
|
||||
<li>Control UI: preserve the composer draft when Stop is tapped during an active chat run, preventing accidental prompt loss on mobile. Fixes #80586. Thanks @KCALLC.</li>
|
||||
<li>Infra/retry: keep jittered retry delays at or above server-supplied Retry-After lower bounds when the hint can be honored. Fixes #68541. (#68543) Thanks @Feelw00.</li>
|
||||
<li>Docs: clarify that <code>/model provider/model</code> is an exact session route, while duplicate bare model ids only use configured fallback order on non-session override paths. Refs #80562. Thanks @gaodaabao.</li>
|
||||
<li>Redact persisted secret-shaped payloads [AI]. (#79006) Thanks @pgondhi987.</li>
|
||||
<li>Agents: label <code>.openclaw/sandboxes</code> exec workdirs as sandbox runs in compact tool summaries instead of showing the full path.</li>
|
||||
<li>OpenAI Codex: surface browser OAuth and device-code login failures instead of treating failed logins as empty successful auth results. Refs #80363.</li>
|
||||
<li>CLI agents: carry runtime-only current-turn sender/reply context into CLI model prompts while keeping prompt-build hook input and transcript text clean.</li>
|
||||
<li>Control UI: keep workspace file presence checks from treating <code>fs-safe</code> stat helper failures as missing files, restoring Agents file status for existing Windows workspace files. Fixes #79953. Thanks @lovelefeng-glitch.</li>
|
||||
<li>Microsoft Foundry: report an explicit error when the Azure subscription prompt returns an id that is not present in the enabled subscription list, instead of continuing from an unsafe subscription assertion. (#62742) Thanks @oliviareid-svg.</li>
|
||||
<li>fix(matrix): gate name-based allowlist resolution [AI]. (#79007) Thanks @pgondhi987.</li>
|
||||
<li>Slack: include the bot's own root/parent message in new thread sessions so in-thread replies reach the agent with the parent text the user is responding to, instead of only <code>reply_to_id</code> metadata. Fixes #79338. Thanks @sxxtony.</li>
|
||||
<li>Docker: keep image builds on the source pnpm workspace policy so pnpm 11 can prune production dependencies without a Docker-only workspace rewrite.</li>
|
||||
<li>Agents/compaction: restore info-level gateway logs for embedded compaction start, completion, and incomplete outcomes. (#71961) Thanks @rubencu.</li>
|
||||
<li>Telegram: build reply-aware inbound turns through the shared channel context path so agents see the current reply target inline with the current message.</li>
|
||||
<li>Telegram: recover legacy message cache files that mixed JSON-array and line-delimited entries so restarted gateways preserve reply-window context. (#80567)</li>
|
||||
<li>Telegram: update the reply-context cache when messages are edited, so streamed bot replies appear in later agent context with their final text instead of the first draft.</li>
|
||||
<li>Skills/Windows: normalize compacted skill prompt locations to forward slashes after home-prefix compaction so Windows skill paths remain readable by model file tools. (#52200) Thanks @chienchandler.</li>
|
||||
<li>Control UI/Windows: update <code>@openclaw/fs-safe</code> so agent workspace file presence checks fall back correctly on Windows, preventing existing AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md, HEARTBEAT.md, and MEMORY.md files from showing as missing. Fixes #79953. Thanks @lovelefeng-glitch.</li>
|
||||
<li>Memory: skip managed dreaming cron reconciliation warnings for ordinary cron and heartbeat hook contexts that cannot manage Gateway cron. (#77027) Thanks @rubencu.</li>
|
||||
<li>Cron: treat Codex app-server turn acceptance, CLI process spawn, and tool starts as execution milestones, preventing isolated runs from tripping the early startup watchdog after work has begun.</li>
|
||||
<li>Codex app-server: treat current-turn <code><turn_aborted></code> raw markers as terminal so interrupted native-tool turns release Discord agent sessions instead of waiting for the outer timeout.</li>
|
||||
<li>Yuanbao: bump <code>openclaw-plugin-yuanbao</code> to 2.13.1 to support <code>sourceReplyDeliveryMode: "automatic"</code> for group chat. (#79814) Thanks @loongfay.</li>
|
||||
<li>Memory: keep <code>memory_search</code> result <code>corpus</code> labels aligned with the hit source, so session transcript hits surface as <code>sessions</code> and memory-file hits stay <code>memory</code>. Fixes #72885. (#71898, #72886) Thanks @rubencu.</li>
|
||||
<li>Codex app-server: default native plugin app tool approvals to automatic so non-destructive read tools run when destructive actions are disabled.</li>
|
||||
<li>Plugins: allow untracked local source plugins in the global extensions directory to load TypeScript package entries while keeping managed installs strict about compiled runtime output. Fixes #80503. Thanks @Kaspre.</li>
|
||||
<li>Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while converting manifest catalog rows into emitted provider config, so <code>google/gemini-3.1-pro-preview</code> is used for testing instead of <code>google/gemini-3-pro-preview</code>.</li>
|
||||
<li>Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids inside saved model allowlists and fallback chains, so proxy routes like <code>openrouter/google/gemini-3-pro-preview</code> are persisted as Gemini 3.1 Pro Preview.</li>
|
||||
<li>Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids in configured proxy/provider-auth model catalogs, so regenerated config keeps testing <code>google/gemini-3.1-pro-preview</code> instead of <code>google/gemini-3-pro-preview</code>.</li>
|
||||
<li>Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while onboarding provider catalog presets, so setup-emitted proxy configs test <code>google/gemini-3.1-pro-preview</code> instead of <code>google/gemini-3-pro-preview</code>.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids in provider catalog rows during generic config writes, so unrelated config changes keep testing <code>google/gemini-3.1-pro-preview</code>.</li>
|
||||
<li>Models: keep configured fallback chains ahead of configured primary models for override selections with duplicate model ids, preventing fallback jumps to the wrong provider. Fixes #80562.</li>
|
||||
<li>Native apps: advertise the Gateway protocol compatibility range so chat and node sessions can connect to v3 gateways after additive v4 client updates.</li>
|
||||
<li>Gateway/agents: keep stale <code>sessions_send</code> ACP manager and <code>web_fetch</code> runtime chunks importable after package updates, preventing live gateways from breaking before restart. Fixes #78804. Thanks @Gomesy72.</li>
|
||||
<li>Gateway/install: preserve service environment value-source metadata in <code>openclaw gateway install</code>, so systemd reinstall paths keep env-file-backed secrets out of inline unit metadata. Refs #77406, #77427. Thanks @stainlu and @brokemac79.</li>
|
||||
<li>Auto-reply/reset: include inbound sender context in bare <code>/new</code> and <code>/reset</code> model prompts while keeping startup instructions out of transcript prompts, so agents see sender identity on the first reset turn. Fixes #77360. Thanks @srb11e.</li>
|
||||
<li>Gateway: avoid synchronous restart-sentinel state probes during post-attach startup, preventing slow Windows or redirected state directories from blocking channel turns. Fixes #79264. Thanks @liyi58.</li>
|
||||
<li>Agents/auth: update successful model auth profile status with one locked store write, reducing post-model reply latency from duplicate <code>auth-profiles.json</code> saves. Thanks @mcaxtr.</li>
|
||||
<li>Agents/image: honor explicit <code>image</code> tool model overrides even when <code>agents.defaults.imageModel</code> is unset, restoring one-off vision calls for configured multimodal providers. Fixes #79341. Thanks @haumanto.</li>
|
||||
<li>Doctor/update: leave live systemd gateway units unchanged during noninteractive update-mode service repair, so update-time doctor does not silently overwrite operator-owned unit directives. Refs #80462.</li>
|
||||
<li>Update: accept optional leading <code>v</code> prefixes when verifying exact npm package install targets, so <code>openclaw update --tag v2026...</code> does not roll back after installing the matching bare package version. Refs #74069; #80480. Thanks @Kaspre.</li>
|
||||
<li>Doctor: treat missing plugin ids in <code>plugins.deny</code> as stale config warnings instead of fatal validation errors, and remove them during stale plugin cleanup so update repair does not restore last-known-good config for deny-only stale plugin refs. Refs #77802. Thanks @Kaspre.</li>
|
||||
<li>Codex app-server: preserve prompt-local current-turn context through context-engine prompt projection, so replied-to Telegram messages stay visible to the Codex model input.</li>
|
||||
<li>Telegram: pass agent-scoped media roots through gateway message actions so workspace-local media from the active agent is not rejected as cross-agent access. Thanks @frankekn.</li>
|
||||
<li>CLI/gateway: keep <code>gateway status --deep</code> plugin-aware so configured plugin manifest warnings, including missing channel config metadata, stay visible during install and update smoke checks.</li>
|
||||
<li>Doctor/status: clarify gateway token source conflict warnings and suppress them inside the managed Gateway service credential context.</li>
|
||||
<li>Feishu: accept Schema 2 card callbacks whose operator identity is nested under <code>operator.user_id</code>, so card buttons dispatch instead of being dropped as malformed. Fixes #71670. (#71787) Thanks @rubencu.</li>
|
||||
<li>Feishu: fall back to a top-level group send when normal group quoted replies target a withdrawn or missing message, preventing replies from disappearing silently while preserving native topic safety. Fixes #79349. Thanks @arlen8411.</li>
|
||||
<li>Doctor: stop flagging the live compatibility agent directory as orphaned when the configured default agent is not <code>main</code>. Fixes #74313. (#74438) Thanks @carlos4s.</li>
|
||||
<li>Auth/Claude CLI: persist fresher managed external CLI OAuth credentials back to <code>auth-profiles.json</code>, preventing stale <code>anthropic:claude-cli</code> profiles from repeatedly bootstrapping and flooding debug logs. Fixes #80129. Thanks @Caulderein.</li>
|
||||
<li>Context: render <code>/context map</code> only from actual run context and persist Codex app-server run reports without counting deferred tool-search schemas as prompt-loaded tool schemas.</li>
|
||||
<li>Codex app-server: report Codex-native tool execution to diagnostics so long-running native <code>bash</code>, web, file, and MCP tools no longer look like stale embedded runs to the watchdog. (#80217)</li>
|
||||
<li>Codex app-server: refresh Codex account rate limits after subscription usage-limit failures so Discord and other channel replies can show the next reset time instead of saying Codex returned none. Thanks @pashpashpash.</li>
|
||||
<li>Agents/auth: let Codex-backed OpenAI agent turns use <code>auth.order.openai</code> entries for Codex-compatible OAuth and API-key profiles while keeping existing <code>openai-codex</code> profile ordering valid.</li>
|
||||
<li>Codex app-server: emit async <code>after_tool_call</code> observations for native tool completions not covered by the native hook relay so observability plugins can record Codex-native tools. (#80372) Thanks @VACInc.</li>
|
||||
<li>Tasks: route group and channel task completions through the requester session so the parent agent can send the visible summary instead of stopping at a generic task-status line. Fixes #77251. (#77365) Thanks @funmerlin.</li>
|
||||
<li>Telegram: preserve blank lines between manually indented bullet blocks and following numbered sections in rendered replies. Fixes #76998. Thanks @evgyur.</li>
|
||||
<li>Agents/sandbox: allow read-only sandbox sessions to read the <code>/agent</code> workspace mount while keeping write/edit/apply_patch workspace-only guarded, restoring <code>read /agent/...</code> for <code>workspaceAccess: "ro"</code>. Fixes #39497. Thanks @stainlu and @teosborne.</li>
|
||||
<li>Slack: pass configured agent identity through draft preview sends so partial streaming replies keep custom username/avatar on the initial Slack message. Fixes #38235. (#38237) Thanks @lacymorrow.</li>
|
||||
<li>Slack: support <code>allowBots: "mentions"</code> for bot-authored messages that mention the receiving bot, matching the documented Discord-style mode without accepting every bot message. Fixes #43587. (#43588) Thanks @raw34.</li>
|
||||
<li>Slack: refresh private file URLs with <code>files.info</code> when inbound DM file events omit or stale attachment URLs, preventing file attachments from being dropped before media hydration. Fixes #50129. (#50200) Thanks @smartchainark.</li>
|
||||
<li>Slack: add scoped message-tool formatting hints so agents use Markdown for plain sends and direct mrkdwn for Block Kit fields. Fixes #34609. (#50979) Thanks @carrotRakko.</li>
|
||||
<li>Slack: describe <code>download-file</code> file ids separately from message timestamps and return a targeted recovery error when agents pass <code>messageId</code> instead of <code>fileId</code>. (#74155) Thanks @jarvis-ai-gregmoser.</li>
|
||||
<li>Slack: retain processed room messages for <code>requireMention=false</code> channels so always-on Slack rooms keep recent conversation context between turns. (#38658) Thanks @syedamaann.</li>
|
||||
<li>Slack: compile interactive reply directives for direct outbound sends without bypassing the <code>interactiveReplies</code> capability gate, preserving Block Kit for Slack CLI and cron deliveries. (#78220) Thanks @kazamak.</li>
|
||||
<li>Slack: keep DM last-route updates scoped to the active non-main DM session, including threaded DM turns, so isolated Slack DM sessions do not overwrite the shared main route. (#73085) Thanks @clawSean.</li>
|
||||
<li>Slack/ACP: route Slack channel and DM messages through configured ACP bindings when no runtime binding exists, keeping bound thread replies pinned to the persistent ACP session and dropping unavailable configured targets instead of falling back to <code>main</code>. (#73101) Thanks @Raasl.</li>
|
||||
<li>Slack: mark unresolved thread replies as ambiguous and skip them instead of treating them as root channel messages, keeping thread continuation on the SDK-backed participation store. (#75630) Thanks @soichiyo.</li>
|
||||
<li>Slack: let same-channel message tool sends opt out of inherited thread context with <code>topLevel: true</code> or <code>threadId: null</code>, allowing agents to post a new parent-channel message from inside a Slack thread. Fixes #79807. Thanks @vexclawx31.</li>
|
||||
<li>Slack: prefer full rich-text block content over truncated socket-mode message previews so long inbound Slack messages reach agents intact. Fixes #79027. Thanks @BobAccentWebDev.</li>
|
||||
<li>Slack: include structured Slack API error details in setup, probe, streaming, and reply logs while preserving token redaction. (#53966) Thanks @deucemask.</li>
|
||||
<li>Gateway/agents: keep structured reasons when active-run queueing fails and deprecate the legacy boolean queue helper, so steering and subagent wake diagnostics distinguish completed, non-streaming, and compacting runs. Fixes #80156. Thanks @markus-lassfolk.</li>
|
||||
<li>System events: dedupe keyed events across the queue while preserving unkeyed, delivery-route, and trust-boundary event identity. (#73040) Thanks @statxc.</li>
|
||||
<li>Agents/UI: compact exec and tool progress rows by hiding redundant shell tool names, replacing known workspace paths with short context markers, and preserving Discord trace scrubbing for compact command lines.</li>
|
||||
<li>ACPX: run and await the embedded ACP backend startup probe by default so the gateway <code>ready</code> signal no longer fires before the acpx runtime has either become usable or reported a probe failure; set <code>OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE=0</code> to restore lazy startup. Fixes #79596. Thanks @bzelones.</li>
|
||||
<li>Gateway/status: surface model-pricing bootstrap and refresh failures as degraded health/status warnings while keeping Gateway liveness healthy. Fixes #79599. Thanks @bzelones.</li>
|
||||
<li>OpenAI-compatible models: strip prior assistant reasoning fields from replayed Chat Completions history by default, preventing oMLX/vLLM Qwen follow-up turns from rejecting or stalling on stale <code>reasoning</code> payloads. Fixes #46637. Thanks @zipzagster and @lexhoefsloot.</li>
|
||||
<li>CLI/onboarding: give non-Azure custom providers a safe generated context window and heal legacy 4k wizard entries without overwriting explicit valid small model limits, preventing first-turn compaction loops. Fixes #79428. (#79911) Thanks @Jefsky.</li>
|
||||
<li>OpenAI-compatible models: add <code>compat.strictMessageKeys</code> to strip Chat Completions replay messages to <code>role</code> and <code>content</code> for strict providers that reject OpenAI-style tool and metadata keys. Fixes #50374. Thanks @choutos.</li>
|
||||
<li>Bedrock Mantle: add <code>plugins.entries.amazon-bedrock-mantle.config.discovery.enabled=false</code> to suppress automatic Mantle discovery and IAM bearer-token generation while keeping the plugin enabled. Fixes #67288. Thanks @kanekoh.</li>
|
||||
<li>Ollama: stop native <code>/api/chat</code> requests from copying catalog <code>contextWindow</code> or <code>maxTokens</code> into <code>options.num_ctx</code> unless <code>params.num_ctx</code> is explicitly configured, avoiding pathological prompt-ingestion latency on local large-context models. Fixes #62267. Thanks @BenSHPD.</li>
|
||||
<li>Ollama: keep the model idle watchdog enabled for <code>*:cloud</code> models routed through a local Ollama host, so cloud-backed tool-loop stalls fail over visibly instead of inheriting local-model no-idle behavior. Fixes #79350. Thanks @geek111.</li>
|
||||
<li>Voice/Ollama: honor routed voice agent <code>tools.allow</code> for classic embedded voice responses, including empty allowlists, so no-tool Ollama agents do not receive tool schemas. Fixes #79506. Thanks @donkeykong91.</li>
|
||||
<li>Agents/doctor: warn when channel-routed agents cannot call the <code>message</code> tool, so operators can fix tool policy mismatches before explicit channel actions such as attachments or thread replies fail. Refs #80128. Thanks @jeffjhunterai.</li>
|
||||
<li>Gateway: reread config from disk after the first in-process restart loop startup, preventing SIGUSR1 restarts from reusing a stale startup snapshot and dropping config written after boot. Fixes #79947. Thanks @TheLevti.</li>
|
||||
<li>Codex app-server: deliver native image-generation outputs from Codex <code>savedPath</code> events as reply media, so blank-text image generation turns still attach the generated file. Thanks @keshavbotagent.</li>
|
||||
<li>Network/SSRF: keep pinned automatic DNS lookups on IPv4 when dual-stack hosts also publish AAAA records, and treat <code>EADDRNOTAVAIL</code> as a transient gateway network failure instead of a fatal crash. Fixes #80078. Thanks @takamasa-aiso.</li>
|
||||
<li>Control UI: show compact one-line live/idle/terminal run status badges in the Sessions table and rename the active-minute filter to its updated-within meaning. Fixes #78307. Thanks @BunsDev.</li>
|
||||
<li>Control UI: scope chat session-list refreshes by agent and skip disk-only agent store discovery for configured-only lists, preventing post-first-message session switching stalls on large Windows stores. Fixes #79675. Thanks @lovelefeng-glitch, @BunsDev.</li>
|
||||
<li>Control UI: allow Appearance tweakcn theme imports through the served CSP so browser-local custom theme links no longer fail with a <code>connect-src</code> violation. Fixes #78504. Thanks @BunsDev.</li>
|
||||
<li>Control UI/config: remove plugin allowlist entries that the form auto-added when a plugin enable toggle is reverted before saving, so reverting the visible toggle clears dirty state without persisting unintended allowlist changes. (#78329) Thanks @samzong.</li>
|
||||
<li>Gateway/mobile: reuse bootstrap-issued device-token scopes on handoff reconnects and surface device-token scope mismatches separately from token mismatches while preserving full shared-token dashboard/native sessions. Fixes #79292. Thanks @BunsDev.</li>
|
||||
<li>Media/host-read: allow buffer-verified gzip, tar, and 7z archives in the shared host-local media validator alongside ZIP and document attachments.</li>
|
||||
<li>Plugins/install: retry managed npm plugin installs without npm alias overrides after npm's <code>Invalid comparator: npm:</code> failure, so older npm versions can install official plugins instead of aborting. (#80539) Thanks @rubencu.</li>
|
||||
<li>Plugins/doctor: invalidate persisted plugin registry snapshots when plugin diagnostics point at deleted source paths, so <code>openclaw doctor</code> stops repeating stale warnings after a local extension is replaced by a managed npm plugin. Fixes #80087. (#80134) Thanks @hclsys.</li>
|
||||
<li>Doctor/OpenAI Codex: preserve Codex auth intent when auto-repairing legacy <code>openai-codex/*</code> model refs to canonical <code>openai/*</code> by adding provider/model-scoped Codex runtime policy, preventing repaired configs from falling through to direct OpenAI API-key auth. Fixes #78533 and #78570. Thanks @superck110 and @Azmodump.</li>
|
||||
<li>CLI/agents: surface durable message delivery status from <code>sendDurableMessageBatch</code> in <code>deliverAgentCommandResult</code> and <code>openclaw agent --json --deliver</code>, preserving suppressed hook outcomes as terminal no-retry results while exposing partial and failed sends for automation. Supersedes #53961 and #57755. Thanks @Kaspre.</li>
|
||||
<li>Agents: apply the LLM idle watchdog while provider stream setup is still pending, preventing silent pre-stream model hangs from waiting for the full agent timeout.</li>
|
||||
<li>Cron: let isolated self-cleanup runs inspect their own job run history while keeping other cron jobs and mutation actions blocked. Fixes #80019. Thanks @hclsys.</li>
|
||||
<li>Cron: report isolated agent-turn setup and pre-model stalls with phase-specific timeout errors instead of waiting for the full job budget when no model call starts. Fixes #74803. Thanks @jeffsteinbok-openclaw and @dgkim311.</li>
|
||||
<li>CLI/plugins: treat arbitrary unknown subcommands outside plugin CLI metadata as normal unknown commands instead of suggesting <code>plugins.allow</code>, while preserving allowlist guidance for real plugin command roots. Fixes #80109. (#80123) Thanks @kagura-agent.</li>
|
||||
<li>CLI/config: persist explicit <code>config set</code> and <code>config patch</code> values that equal runtime defaults instead of reporting success while dropping them. Fixes #79856. (#80106) Thanks @abodanty and @hclsys.</li>
|
||||
<li>OpenAI/realtime voice: accept Codex-compatible legacy audio and transcript event aliases so provider protocol drift does not drop assistant audio or captions.</li>
|
||||
<li>Discord/voice: keep default agent-proxy realtime sessions from auto-speaking filler before the forced OpenClaw consult answer, finish Discord playback on realtime response completion, and queue later exact-speech answers until playback idles to avoid mid-sentence replacement.</li>
|
||||
<li>Gateway: return deterministic <code>400 invalid_request_error</code> responses for malformed encoded session-kill HTTP paths instead of letting route-shaped requests fall through to later Gateway handlers. (#72439) Thanks @rubencu.</li>
|
||||
<li>Control UI: serve root PWA and favicon assets from <code>/__openclaw__/</code> SPA routes so tab icons, install metadata, and the service worker do not 404 after internal navigation. Fixes #80072. Thanks @CodeNovice2017.</li>
|
||||
<li>Exec/safe bins: compare trusted safe-bin dirs with path-specific case folding on case-insensitive filesystems so Windows and default macOS paths match without weakening case-sensitive mounts. (#42131) Thanks @hkochar.</li>
|
||||
<li>OpenAI/realtime voice: honor disabled input-audio interruption locally so server VAD speech-start events do not clear Discord playback after operators set <code>interruptResponseOnInputAudio: false</code>.</li>
|
||||
<li>Telegram: keep no-response DM turns quiet instead of rewriting them into visible silent-reply chatter. Fixes #78188. (#78228) Thanks @Beandon13.</li>
|
||||
<li>Telegram: handle managed select button callbacks before the raw callback fallback while preserving delimiter-containing option values such as <code>env|prod</code>. (#79816) Thanks @moeedahmed.</li>
|
||||
<li>OpenAI-compatible models: handle JSON chat-completion bodies returned to streaming requests, preserving reasoning fields and visible text instead of completing an empty agent turn. Fixes #77870.</li>
|
||||
<li>Discord/models: defer model picker component interactions before loading route, model, and preference data, preventing "This interaction failed" timeouts under gateway load. Fixes #77283. Thanks @colin-chang.</li>
|
||||
<li>xAI: expose <code>/think low|medium|high</code> for reasoning-capable Grok models and keep <code>reasoning.effort</code> on native Responses payloads while preserving off-only behavior for non-reasoning routes. Fixes #79210. Thanks @colinmcintosh.</li>
|
||||
<li>CLI/media: let explicit image description model refs use bundled static provider catalogs and generic model-backed image hooks, so <code>openclaw infer image describe --model zai/glm-4.6v</code> works like direct model runs and Anthropic auth probes avoid stale Claude 3 Haiku catalog entries.</li>
|
||||
<li>Models/Anthropic: add <code>anthropic/claude-haiku-4-5</code> to Anthropic API-key agent allowlist defaults when an Anthropic default model is configured, so cron model overrides can select the current Haiku alias. Fixes #78000.</li>
|
||||
<li>Agents/compaction: initialize built-in context engines before CLI transcript compaction resolves the default engine, preventing clean-process <code>legacy</code> engine registration failures during CLI session persistence. Fixes #79446. Thanks @TurboTheTurtle.</li>
|
||||
<li>Agents/Anthropic-compatible: strip replayed thinking blocks for custom Anthropic-compatible models that explicitly declare <code>supportsReasoningEffort: false</code>, preventing Kimi-compatible providers from resending unsupported <code>thinking</code> content. Fixes #47452.</li>
|
||||
<li>Kimi: keep Anthropic-compatible thinking streams valid by supplying required thinking budgets and enough output room for hidden reasoning plus final text. (#80481) Thanks @InTheCloudDan.</li>
|
||||
<li>Browser: wait longer for existing-session Chrome MCP status and non-deep doctor probes so slow first attaches do not falsely report offline while keeping raw CDP status probes short. (#77473) Thanks @rubencu.</li>
|
||||
<li>Gateway/logging: install console capture before foreground Gateway fast-path parsing and suppress known libsignal session dumps even in verbose mode, preventing raw terminal logs from printing WhatsApp session key material. (#76306) Thanks @rubencu.</li>
|
||||
<li>Exec approvals: keep <code>exec.approval.list</code> on the lightweight policy-summary path so listing pending approvals no longer loads the rich tree-sitter command explainer. (#76943) Thanks @rubencu.</li>
|
||||
<li>Agents: surface concise default-visible warnings when <code>exec</code>/<code>bash</code> tool calls fail after the assistant claims success, while keeping raw stderr hidden unless verbose details are enabled. Fixes #60497. (#80003) Thanks @jbetala7.</li>
|
||||
<li>Channels/iMessage: keep redacted failed probe details in non-sensitive health snapshots so Full Disk Access failures no longer appear as configured/OK in status output. Fixes #79795.</li>
|
||||
<li>Agents: stop blank model-emitted tool calls before dispatch while preserving id-based tool-name recovery, preventing Kimi/NVIDIA blank-name retry loops without creating a callable <code>_blank</code> sentinel. Fixes #34129. (#56391) Thanks @smartchainark.</li>
|
||||
<li>Agents/Telegram: deliver the canonical final assistant answer instead of replaying accumulated pre-tool text blocks, preventing duplicate Telegram replies and raw-looking tool-output fragments from leaking into chat delivery. Fixes #79621 and #79986. Thanks @nonzeroclaw and @dudaefj.</li>
|
||||
<li>Auto-reply/TUI: keep fallback timeout recovery deliverable after a primary model lifecycle error by emitting fallback progress and deferring terminal TUI errors until recovery has a chance to finish. Fixes #80000. (#80009) Thanks @TurboTheTurtle.</li>
|
||||
<li>Heartbeat: clear stale auto fallback model overrides when the configured default model changes, so heartbeat runs follow updated <code>agents.defaults.model.primary</code> without requiring a manual reset. Fixes #74284. Thanks @brtkwr and @bitloi.</li>
|
||||
<li>CLI/agent: let <code>openclaw agent --model</code> use the backend/admin Gateway scope without cached device-token scopes silently downscoping the request. (#78837) Thanks @VACInc.</li>
|
||||
<li>CLI/help: keep help and version invocations configless while improving shared port, channel, plugin, task, session, message, pairing, and auth recovery text.</li>
|
||||
<li>CLI/config: explain strict JSON parse failures with a valid example and the plain-string escape hatch.</li>
|
||||
<li>CLI/secrets: turn offline Gateway reload failures into actionable recovery text.</li>
|
||||
<li>CLI/channels: explain missing or ambiguous channel selections with next commands.</li>
|
||||
<li>CLI/channels: defer guided channel status collection until a channel is selected, keeping <code>openclaw channels add</code> first screen quieter.</li>
|
||||
<li>CLI/channels: exit guided channel setup cleanly on cancellation instead of printing the internal wizard error.</li>
|
||||
<li>Plugins/CLI: route disabled Matrix and LanceDB memory command roots to plugin-enable guidance instead of generic unknown-command errors.</li>
|
||||
<li>Browser/Docker: detect Playwright-managed Chromium from <code>PLAYWRIGHT_BROWSERS_PATH</code> and the default Playwright cache on Linux, so Docker installs that persist <code>/home/node/.cache/ms-playwright</code> no longer need <code>browser.executablePath</code>.</li>
|
||||
<li>Ollama: keep DeepSeek V4 cloud models thinking-capable even when Ollama Cloud <code>/api/show</code> omits the <code>thinking</code> capability, so <code>/think high</code> no longer rejects <code>ollama/deepseek-v4-*:cloud</code>.</li>
|
||||
<li>ACPX/Claude ACP: keep foreground prompts waiting for their own result when autonomous task-notification results arrive during the same session, and retarget the patch for Claude Agent ACP <code>0.33.1</code>.</li>
|
||||
<li>WhatsApp: keep Baileys media uploads from passing non-Dispatcher agents to undici in <code>7.0.0-rc10</code>, and patch the bundled Baileys declaration so the latest tsdown build stays warning-clean.</li>
|
||||
<li>Build: keep tsdown <code>0.22.0</code> warning-clean by externalizing known third-party declaration edges and replacing relative channel config module augmentations with explicit built-in channel fields.</li>
|
||||
<li>ACP sessions: map canonical runtime options to backend-advertised ACP config keys like Claude's <code>effort</code> while keeping persisted OpenClaw state canonical. (#79926) Thanks @InTheCloudDan.</li>
|
||||
<li>Models/Discord: support <code>provider/*</code> entries in <code>agents.defaults.models</code> so <code>/model</code>, <code>/models</code>, and model pickers can show dynamically discovered models for selected providers without exact model allowlists. Fixes #79485. Thanks @rendrag-git.</li>
|
||||
<li>Gateway/watch: rebuild or restage missing bundled-plugin dist and runtime-postbuild outputs before launching the Gateway from a source checkout, preventing incomplete watch-mode runtime trees. (#70805) Thanks @rubencu.</li>
|
||||
<li>CLI/update: allow restart health probes from the previous gateway protocol during self-update, and make plugin dry-runs report exact npm target versions instead of <code>unknown</code> while preserving unchanged status.</li>
|
||||
<li>OpenAI/Codex: forward persisted <code>openai-codex</code> OAuth profile metadata into Codex plugin harness attempts after canonical <code>openai/*</code> migration, so OAuth-only installs keep using native Codex auth instead of falling through to direct OpenAI API-key auth. Fixes #79978.</li>
|
||||
<li>OpenAI/Codex: point gateway missing-key recovery and wizard docs at the canonical <code>openai/gpt-5.5</code> plus Codex OAuth route, and fix trajectory export errors so they suggest the valid <code>openclaw sessions</code> command.</li>
|
||||
<li>Google/Gemini: normalize retired <code>google/gemini-3-pro-preview</code> primary, fallback, and model-map refs during config load and unrelated config writes so saved config keeps targeting Gemini 3.1 Pro Preview.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside emitted Google provider model config, so regenerated models.json rows test <code>google/gemini-3.1-pro-preview</code>.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids for explicit OpenAI-compatible Google and Gemini CLI provider configs, so emitted config targets <code>google/gemini-3.1-pro-preview</code>.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids preserved from existing merged models.json providers so config emission keeps targeting <code>google/gemini-3.1-pro-preview</code>.</li>
|
||||
<li>Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside provider auth config patches so setup-emitted provider catalogs test <code>google/gemini-3.1-pro-preview</code>.</li>
|
||||
<li>GitHub Copilot: mint short-lived Copilot API tokens with the same <code>vscode-chat</code> integration identity used by runtime requests, and refresh legacy cached tokens missing that identity so image-capable Copilot models no longer inherit the <code>copilot-language-server</code> scope. Fixes #79946, #80074. Thanks @TurboTheTurtle.</li>
|
||||
<li>Plugins/doctor: drop stale managed npm install records when <code>openclaw doctor --fix</code> removes npm packages that shadow bundled plugins, so the rebuilt registry no longer resurrects the removed package metadata.</li>
|
||||
<li>Doctor: warn when a per-agent model config omits the <code>fallbacks</code> key and <code>agents.defaults.model.fallbacks</code> is non-empty. Covers both string-form (<code>"model": "..."</code>) and partial-object form (<code>"model": { "primary": "..." }</code>) — both silently clobber the defaults chain at runtime. Use <code>"fallbacks": []</code> to explicitly opt out of fallbacks, or add <code>"fallbacks": [...]</code> to inherit or override. Fixes #79369. Thanks @Kaspre.</li>
|
||||
<li>Discord/voice: reuse or suppress late realtime consult tool calls without stealing newer speaker context or speaking forced fallback answers twice.</li>
|
||||
<li>Discord/voice: skip likely incomplete realtime forced-consult transcript fragments and non-actionable closings so stale partial speech does not queue delayed answers over the next turn.</li>
|
||||
<li>Discord/voice: keep realtime forced consults from clearing active exact-speech playback, so back-to-back voice answers queue instead of cutting each other off.</li>
|
||||
<li>Discord/voice: synthesize realtime playback timestamps from emitted Discord PCM so OpenAI realtime barge-in truncation no longer sees <code>audioEndMs=0</code> and skips legitimate interruptions.</li>
|
||||
<li>Plugin SDK: keep activated linked plugin runtime facades loadable when bundled plugin fallback is disabled. Thanks @shakkernerd.</li>
|
||||
<li>Feishu: auto-thread <code>message(action="send")</code> replies inside the topic when the active session is group_topic or group_topic_sender, and propagate <code>replyInThread</code> through text, card, and media outbound adapters so topic-scoped sessions no longer post at the group root. Fixes #74903. (#77151) Thanks @ai-hpc.</li>
|
||||
<li>WhatsApp: pass routing context into voice-note transcript echo preflight so echoed transcripts can deliver to the originating chat. Fixes #79778. (#79788) Thanks @hclsys.</li>
|
||||
<li>Cron/failover: classify structured OpenAI-compatible <code>server_error</code> payloads as <code>server_error</code>, expose that reason in cron state, and let one-shot cron retry policy honor <code>retryOn: ["server_error"]</code> without requiring raw <code>5xx</code> text. (#45594) Thanks @clovericbot.</li>
|
||||
<li>Slack: wake the resolved thread session after interactive reply button/select clicks and carry Slack delivery context through the queued interaction event, so clicks continue the visible conversation. Fixes #79676 and #61502. (#79836) Thanks @velvet-shark, @tianxiaochannel-oss88, and @Saicheg.</li>
|
||||
<li>WhatsApp/streaming: send only the new suffix when text-end block replies repeat prior preambles across tool-call cycles, preventing cumulative WhatsApp preamble messages. Fixes #78946. (#79120) Thanks @brokemac79 and @papawattu.</li>
|
||||
<li>Tests/security audit: sandbox <code>audit-exec-surface.test.ts</code> under a per-case OpenClaw home tempdir, redirecting <code>OPENCLAW_HOME</code> (which wins over <code>HOME</code>/<code>USERPROFILE</code> in <code>resolveRawHomeDir</code>) alongside <code>HOME</code> and <code>USERPROFILE</code>, so its <code>saveExecApprovals(...)</code> calls never touch the live <code>~/.openclaw/exec-approvals.json</code> on the host running the suite. Sibling exec-approvals tests already used the tempdir pattern; this file did not, so running <code>pnpm test</code> against a contributor's local checkout was silently truncating their real approvals to <code>{ "version": 1, "agents": {} }</code>. (#79885) Thanks @omarshahine.</li>
|
||||
<li>ACP/gateway: preserve <code>AcpRuntimeError</code> cause chain (code/method/JSON-RPC detail) through the lifecycle boundary so gateway logs, telegram replies, and tool-result text show the actual upstream failure instead of opaque <code>Internal error</code>/<code>[object Object]</code>, with redaction applied before the chain reaches log or reply surfaces.</li>
|
||||
<li>Channels/iMessage: wire <code>action: "reply"</code> attachments through <code>imsg send-rich --file</code> when the installed imsg build advertises that capability (probed once via <code>imsg send-rich --help</code> and cached on the private-API status). Reply now hydrates <code>media</code>/<code>mediaUrl</code>/<code>fileUrl</code>/<code>mediaUrls[0]</code>/<code>filePath</code>/<code>path</code>/base64 <code>buffer</code>+<code>filename</code> through the shared outbound resolver, stages buffers via the existing <code>withTempFile</code> helper, rejects <code>http(s)://</code> URL attachments with a targeted error pointing callers at <code>send</code>'s full attachment-resolver pipeline, and falls back to the explicit <code>imsg#114 not landed yet</code> error on older imsg builds. Depends on the upstream <code>openclaw/imsg#114</code> capability landing in an installable release; until then the new path stays gated and users see the same explicit fallback <code>#79822</code> introduced. (#79864) Thanks @omarshahine.</li>
|
||||
<li>Telegram: preserve the first-preview debounce while appending true partial-stream deltas, so edited draft previews no longer duplicate earlier text when providers emit incremental output. (#80045) Thanks @TurboTheTurtle.</li>
|
||||
<li>Agents/Anthropic: report 1M session context for Claude Opus/Sonnet 4 models even when local model config still advertises 200k, matching model discovery and preventing premature status/UI overflow. Fixes #66766.</li>
|
||||
<li>Models/OpenRouter: hide missing-auth direct provider rows in <code>/model status</code> when they are only duplicated by a nested OpenRouter model id such as <code>openrouter/google/...</code>, while preserving explicitly configured direct providers. Fixes #62317.</li>
|
||||
<li>Models: preserve an explicitly selected provider/model such as <code>opencode-go/deepseek-v4-pro</code> when another provider owns the same bare model alias. Fixes #79325.</li>
|
||||
<li>Models/config: explain missing <code>models.providers.<provider>.models[]</code> registration when a model exists only in <code>agents.defaults.models</code>, instead of returning a bare unknown-model error. Fixes #80089.</li>
|
||||
<li>MCP/tools: prefix bundle MCP server/tool fragments that would start with digits, keeping generated tool names valid for Moonshot/Kimi and other strict providers. Fixes #79179.</li>
|
||||
<li>Models/OpenRouter: treat <code>403 API key budget limit exceeded</code> as billing so model fallback advances instead of retrying the exhausted primary. Fixes #60191. Thanks @omgitsgela.</li>
|
||||
<li>Models/OpenRouter: repair stale session overrides that lost the outer <code>openrouter/</code> provider wrapper, so sessions return to the configured OpenRouter model instead of failing as an unknown direct-provider model. Fixes #78161. Thanks @hjamal7-bit.</li>
|
||||
<li>Google/Gemini: default API-key onboarding back to <code>google/gemini-3.1-pro-preview</code> so fresh Gemini test configs exercise Gemini 3.1 Pro Preview.</li>
|
||||
<li>Telegram: show full provider/model labels for nested OpenRouter model ids in the model picker, so <code>openrouter/openai/gpt-5.4-mini</code> no longer displays as <code>openai/gpt-5.4-mini</code>. Fixes #67792. (#72752) Thanks @iot2edge.</li>
|
||||
<li>Models/OpenRouter: preserve live <code>supported_parameters</code> tool support metadata so non-tool Perplexity Sonar models no longer receive agent tool payloads and fall back unnecessarily. Fixes #64175. Thanks @Catfish-75.</li>
|
||||
<li>Models/OpenRouter: add MoonshotAI Kimi K2.5 to the bundled OpenRouter catalog so onboarding/model pickers can offer it without waiting for live discovery. Fixes #14601.</li>
|
||||
<li>Models/OpenRouter: keep keyRef/tokenRef-backed auth profiles visible to read-only PI model discovery, so OpenRouter models stay available in model pickers without storing plaintext keys. Fixes #58106. Thanks @ThalynLabs.</li>
|
||||
<li>Models/list: include explicit configured provider rows and read-only auth-backed catalog rows in the default configured view without loading PI's full registry, keeping Control UI pickers aligned with usable model auth. Refs #79381. Thanks @ismael-81.</li>
|
||||
<li>Security/audit: honor <code>tools.byProvider["provider/model"].deny</code> when reporting small-model web/browser exposure, so per-model OpenRouter mitigations clear the <code>models.small_params</code> exposure signal. Fixes #80118.</li>
|
||||
<li>Models/Moonshot: accept direct <code>moonshotai/...</code> and <code>moonshot-ai/...</code> refs as aliases for canonical <code>moonshot/...</code>, so copied OpenRouter Kimi ids no longer fail as unknown direct models. Fixes #73876. (#74946) Thanks @jeffrey701.</li>
|
||||
<li>Kimi Code: use Kimi's stable <code>kimi-for-coding</code> API model id in bundled catalog, onboarding, and docs while normalizing legacy <code>kimi-code</code> and <code>k2p5</code> refs. Fixes #79965.</li>
|
||||
<li>Telegram: render cached reply targets and nearby group chatter as one selected conversation context window, so stale replies no longer split JSON reply chains from local chat context.</li>
|
||||
<li>Volcengine/Kimi: strip provider-unsupported tool schema length and item constraint keywords for direct and coding-plan models so hosted Kimi runs do not reject message tools with <code>minLength</code>. Fixes #38817.</li>
|
||||
<li>DeepSeek: backfill V4 <code>reasoning_content</code> replay fields for unowned OpenAI-compatible proxy providers, preventing follow-up request failures outside the bundled DeepSeek and OpenRouter routes. Fixes #79608.</li>
|
||||
<li>iMessage: emit a WARN log when an action is blocked because the imsg private API bridge is not attached, so operators see the silent-drop in <code>~/.openclaw/logs/openclaw.log</code> instead of having to read per-session trajectory JSONL <code>tool.result</code> payloads. Common after a gateway restart un-injects the dylib from Messages.app. (#80035) Thanks @omarshahine.</li>
|
||||
<li>Codex: cross-fill missing <code>thread.id</code> and <code>thread.sessionId</code> before schema validation so live Codex app-server responses that omit <code>sessionId</code> no longer fail <code>thread/start</code> or <code>thread/resume</code>. Fixes #80124. (#80137) Thanks @kagura-agent.</li>
|
||||
<li>Agents/Pi: wait for embedded abort cleanup to settle before releasing the session write lock, preventing follow-up turns from racing previous prompt teardown. (#80239) Thanks @samzong.</li>
|
||||
<li>WhatsApp: downgrade OpenClaw watchdog-triggered Web reconnects from runtime errors to recovery warnings and clear the recovered reconnect status after the next healthy connection. (#77026) Thanks @rubencu.</li>
|
||||
<li>ACPX/Windows: hide the MCP proxy target child process window on Windows so ACP-backed agents do not flash or fail because of terminal window handling. Fixes #60672. (#60678) Thanks @KChow-ctrl.</li>
|
||||
<li>Agents: abort generic repeated no-progress tool loops at the critical threshold when identical calls keep returning identical outcomes. (#80668) Thanks @frankekn.</li>
|
||||
<li>Exec approvals: omit generated command highlights for non-POSIX Windows and shell-wrapper approval commands until those command languages have native highlighting support. (#80566) Thanks @jesse-merhi.</li>
|
||||
<li>Telegram: keep verbose tool progress and result drafts separate from the final assistant answer so tool output no longer blends into the final Telegram message. (#80294) Thanks @jalehman.</li>
|
||||
<li>Plugin SDK/Windows: enable the native require fast path for root <code>openclaw/plugin-sdk</code> dist aliases instead of forcing Jiti transforms. (#80878) Thanks @medns.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.12/OpenClaw-2026.5.12.zip" length="52670462" type="application/octet-stream" sparkle:edSignature="RhckloxLoZhtGQZ+0jrH0qWN3py61an+kiAdgEJY9IZGekTKmJ+DWHXY3ixQJYvKf2WLgxOzaY6Jy27pF+kECw=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.5.7</title>
|
||||
<pubDate>Thu, 07 May 2026 22:36:27 +0000</pubDate>
|
||||
@@ -449,368 +923,5 @@
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.2/OpenClaw-2026.5.2.zip" length="51078259" type="application/octet-stream" sparkle:edSignature="NwoecacHxJOYpltNmB/y7LV5I8ZIh5pENWSydbOM1vsfgSrcb7pRP+Zm2nih1IAq7hh1tOmQ0XWnsohic7U4DA=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.4.29</title>
|
||||
<pubDate>Thu, 30 Apr 2026 21:47:22 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026042990</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.4.29</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.4.29</h2>
|
||||
<h3>Highlights</h3>
|
||||
<ul>
|
||||
<li>Messaging and automation get active-run steering by default, visible-reply enforcement, spawned subagent routing metadata, and opt-in follow-up commitments for heartbeat-delivered reminders. Thanks @vincentkoc, @scoootscooob, @samzong, and @vignesh07.</li>
|
||||
<li>Memory grows into a people-aware wiki with provenance views, per-conversation Active Memory filters, partial recall on timeout, and bounded REM preview diagnostics. Thanks @vincentkoc, @quengh, @joeykrug, and @samzong.</li>
|
||||
<li>Provider/model coverage expands with NVIDIA onboarding/catalogs plus faster manifest-backed model/auth paths, Bedrock Opus 4.7 thinking parity, and safer Codex/OpenAI-compatible replay and streaming behavior. Thanks @eleqtrizit, @shakkernerd, @prasad-yashdeep, @woodhouse-bot, and @LyHug.</li>
|
||||
<li>Gateway and packaged-plugin reliability focuses on slow-host startup, reusable model catalogs, event-loop readiness diagnostics, runtime-dependency repair, stale-session recovery, and version-scoped update caches. Thanks @lpendeavors, @DerFlash, @vincentkoc, @pashpashpash, and @jhsmith409.</li>
|
||||
<li>Channel fixes cluster around Slack Block Kit limits, Telegram proxy/webhook/polling/send resilience, Discord startup/rate-limit handling, WhatsApp delivery/liveness, and Microsoft Teams/Matrix/Feishu edge cases. Thanks @slackapi, @SymbolStar, @djgeorg3, @TinyTb, @dseravalli, @nklock, and @alex-xuweilong.</li>
|
||||
<li>Security and operations add OpenGrep scanning, sharper GHSA triage policy, safer exec/pairing/owner-scope handling, Docker/onboarding automation, and web-fetch IPv6 ULA opt-in for trusted proxy stacks. Thanks @jesse-merhi, @pgondhi987, @mmaps, @jinjimz, and @jeffrey701.</li>
|
||||
</ul>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Security/tools: configured tool sections (<code>tools.exec</code>, <code>tools.fs</code>) no longer implicitly widen restrictive profiles (<code>messaging</code>, <code>minimal</code>). Users who need those tools under a restricted profile must add explicit <code>alsoAllow</code> entries; a startup warning identifies affected configs. Fixes #47487. Thanks @amknight.</li>
|
||||
<li>Agents/commitments: add opt-in inferred follow-up commitments with hidden batched extraction, per-agent/per-channel scoping, heartbeat delivery, CLI management, a simple <code>commitments.enabled</code>/<code>commitments.maxPerDay</code> config, and heartbeat-interval due-time clamping so magical check-ins do not echo immediately. (#74189) Thanks @vignesh07.</li>
|
||||
<li>Messages/queue: make <code>steer</code> drain all pending Pi steering messages at the next model boundary, keep legacy one-at-a-time steering as <code>queue</code>, and add a dedicated steering queue docs page. Thanks @vincentkoc.</li>
|
||||
<li>Messages/queue: default active-run queueing to <code>steer</code> with a 500ms followup fallback debounce, and document the queue modes, precedence, and drop policies on the command queue page. Thanks @vincentkoc.</li>
|
||||
<li>Messages: add global <code>messages.visibleReplies</code> so operators can require visible output to go through <code>message(action=send)</code> for any source chat, while <code>messages.groupChat.visibleReplies</code> stays available as the group/channel override. Thanks @scoootscooob.</li>
|
||||
<li>Gateway/events: surface <code>spawnedBy</code> on subagent chat and agent broadcast payloads so clients can route child session events without an extra session lookup. (#63244) Thanks @samzong.</li>
|
||||
<li>Memory/wiki: add agent-facing people wiki metadata, canonical aliases, person cards, relationship graphs, privacy/provenance reports, evidence-kind drilldown, and search modes for person lookup, question routing, source evidence, and raw claims. Thanks @vincentkoc.</li>
|
||||
<li>Active Memory: add optional per-conversation <code>allowedChatIds</code> and <code>deniedChatIds</code> filters so operators can enable recall only for selected direct, group, or channel conversations while keeping broad sessions skipped. (#67977) Thanks @quengh.</li>
|
||||
<li>Active Memory: return bounded partial recall summaries when the hidden memory sub-agent times out, including the default temporary-transcript path, so useful recovered context is not discarded. (#73219) Thanks @joeykrug.</li>
|
||||
<li>Gateway/memory: add a read-only <code>doctor.memory.remHarness</code> RPC so operator clients can preview bounded REM dreaming output without running mutation paths. (#66673) Thanks @samzong.</li>
|
||||
<li>Providers/NVIDIA: add the NVIDIA provider with API-key onboarding, setup docs, static catalog metadata, and literal model-ref picker support so NVIDIA hosted models can be selected with their provider prefix intact. (#71204) Thanks @eleqtrizit.</li>
|
||||
<li>Models: suppress explicitly configured openai-codex/gpt-5.4-mini inline entries so a stale models config written by <code>openclaw doctor --fix</code> cannot bypass the manifest capability block and cause repeated assistant-turn failures when the runtime switches to that model on ChatGPT-backed Codex accounts. Conditional suppressions (e.g. qwen Coding Plan endpoint guards) remain bypassable by explicit user configuration. (#74451) Thanks @0xCyda, @hclsys, and @Marvae.</li>
|
||||
<li>Added SQLite-backed plugin state store (<code>api.runtime.state.openKeyedStore</code>) for restart-safe keyed registries with TTL, eviction, and automatic plugin isolation. Thanks @amknight.</li>
|
||||
<li>Plugin SDK: mark remaining legacy alias exports and diffs tool/config aliases with deprecation metadata, and add a guard so future legacy alias comments require <code>@deprecated</code> tags. Thanks @vincentkoc.</li>
|
||||
<li>CLI/QR/dependencies: internalize small terminal progress and QR wrapper helpers while keeping the real QR encoder dependency direct, reducing the default runtime dependency graph without changing QR output behavior. Thanks @vincentkoc.</li>
|
||||
<li>Dependencies: refresh workspace runtime, plugin, and tooling packages, including ACP, Pi, AWS SDK, TypeBox, pnpm, oxlint, oxfmt, jsdom, pdfjs, ciao, and tokenjuice, while keeping patched ACP behavior and lint gates current. Thanks @mariozechner.</li>
|
||||
<li>Gateway/dev: run <code>pnpm gateway:watch</code> through a named tmux session by default, with <code>gateway:watch:raw</code> and <code>OPENCLAW_GATEWAY_WATCH_TMUX=0</code> for foreground mode, so repeated starts respawn an inspectable watcher without trapping the invoking agent shell. Thanks @vincentkoc.</li>
|
||||
<li>Gateway/diagnostics: emit an opt-in startup diagnostics timeline that records gateway lifecycle and plugin-load phases behind a config flag, so slow-start diagnosis no longer requires bespoke instrumentation. Thanks @shakkernerd.</li>
|
||||
<li>Control UI/i18n: extend the locale registry with new Persian (fa), Dutch (nl), Vietnamese (vi), Italian (it), Arabic (ar), and Thai (th) entries and ship <code>fa</code>, <code>nl</code>, <code>vi</code>, and <code>zh-TW</code> docs glossaries, so the docs translation pipeline and the Control UI language picker stay aligned across surfaces. Thanks @vincentkoc.</li>
|
||||
<li>Channels: add Yuanbao channel docs entrance so the Tencent Yuanbao bot appears in the channel listing and sidebar navigation. (#73443) Thanks @loongfay.</li>
|
||||
<li>Channels/Yuanbao: update plugin GitHub location to YuanbaoTeam/yuanbao-openclaw-plugin and add "yuanbao" alias to channel catalog. (#74253) Thanks @loongfay.</li>
|
||||
<li>Docker setup: add <code>OPENCLAW_SKIP_ONBOARDING</code> so automated Docker installs can skip the interactive onboarding step while still applying gateway defaults. (#55518) Thanks @jinjimz.</li>
|
||||
<li>Security policy: classify media/base64 decode and format-conversion overhead after configured acceptance limits as performance-only for GHSA triage unless a report demonstrates a limit bypass, crash, exhaustion, data exposure, or another boundary bypass. (#74311)</li>
|
||||
<li>Security/OpenGrep: add a precise OpenGrep rulepack, source-rule compiler, provenance metadata check, and PR/full scan workflows that validate first-party code and rulepack-only changes while uploading SARIF to GitHub Code Scanning. (#69483) Thanks @jesse-merhi.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Providers/OpenAI Codex: preserve existing wrapped Codex streams during OpenAI attribution so PI OAuth bearer injection reaches ChatGPT/Codex Responses, and strip native Codex-only unsupported payload fields without touching custom compatible endpoints. (#75111) Thanks @keshavbotagent.</li>
|
||||
<li>Agents/tool-result guard: use the resolved runtime context token budget for non-context-engine tool-result overflow checks, so long tool-heavy sessions no longer compact early when <code>contextTokens</code> is larger than native <code>contextWindow</code>. Fixes #74917. Thanks @kAIborg24.</li>
|
||||
<li>Gateway/systemd: exit with sysexits 78 for supervised lock and <code>EADDRINUSE</code> conflicts so <code>RestartPreventExitStatus=78</code> stops <code>Restart=always</code> restart loops instead of repeatedly reloading plugins against an occupied port. Fixes #75115. Thanks @yhyatt.</li>
|
||||
<li>Agents/runtime: skip blank visible user prompts at the embedded-runner boundary before provider submission while still allowing internal runtime-only turns and media-only prompts, so Telegram/group sessions no longer leak raw empty-input provider errors when replay history exists. Fixes #74137. Thanks @yelog, @Gracker, and @nhaener.</li>
|
||||
<li>Auto-reply/group chats: fall back to automatic source delivery when a channel precomputes message-tool-only replies but the <code>message</code> tool is unavailable, so Discord/Slack-style group turns do not silently complete without a visible reply. Fixes #74868. Thanks @kagura-agent.</li>
|
||||
<li>Browser/gateway: share one browser control runtime across the HTTP control server and <code>browser.request</code>, and refresh browser profile config from the source snapshot, so CLI status/start honors configured <code>browser.executablePath</code>, <code>headless</code>, and <code>noSandbox</code> instead of falling back to stale auto-detection. Fixes #75087; repairs #73617. Thanks @civiltox and @martingarramon.</li>
|
||||
<li>Agents/subagents: bound automatic orphan recovery with persisted recovery attempts and a wedged-session tombstone, and teach task maintenance/doctor to reconcile those sessions so restart loops no longer require manual <code>sessions.json</code> surgery. Fixes #74864. Thanks @solosage1.</li>
|
||||
<li>Gateway/startup: skip pre-bind web-fetch provider discovery for credential-free <code>tools.web.fetch</code> config, so Docker/Kubernetes gateways bind even when optional fetch limits are present. Fixes #74896. Thanks @KoykL.</li>
|
||||
<li>Infra/tmp: tolerate concurrent temp-dir permission repairs by rechecking directories that another process already tightened, so parallel ACP subprocess startup no longer throws <code>Unsafe fallback OpenClaw temp dir</code>. Fixes #66867. Thanks @Kane808-AI and @jarvisz8.</li>
|
||||
<li>Signal: match group allowlists against inbound Signal group ids as well as sender ids, and process explicitly configured Signal groups without requiring mentions unless <code>requireMention</code> is set. Fixes #53308. Thanks @minupla and @juan-flores077.</li>
|
||||
<li>Slack: require bot-authored room messages with <code>allowBots=true</code> to come from an explicitly channel-allowlisted bot or from a room where an explicit Slack owner is present, so broad bot relays cannot run unattended. Fixes #59284. Thanks @andrewhong-translucent.</li>
|
||||
<li>Signal: bound <code>signal-cli</code> installer release and archive downloads with explicit timeouts, declared and streamed size checks, and partial-file cleanup. Fixes #54153. Thanks @jinduwang1001-max and @juan-flores077.</li>
|
||||
<li>Signal: derive <code>getAttachment</code> HTTP response caps from <code>channels.signal.mediaMaxMb</code> with base64 headroom, so inbound photos and videos no longer drop behind the 1 MiB RPC default. Fixes #73564. Thanks @heyhudson.</li>
|
||||
<li>Signal: keep the long-lived receive SSE monitor open while idle instead of applying the 10s RPC/check deadline, so <code>signal-cli</code> 0.14.3 event streams no longer reconnect before inbound messages arrive. Fixes #74741. Thanks @fgabelmannjr and @k7n4n5t3w4rt.</li>
|
||||
<li>Models/OpenAI Codex: restore <code>openai-codex/gpt-5.4-mini</code> for ChatGPT/Codex OAuth PI runs after live OAuth proof, and align the manifest, forward-compat metadata, docs, and regression tests so stale cron and heartbeat configs resolve again. Fixes #74451. Thanks @0xCyda, @hclsys, and @Marvae.</li>
|
||||
<li>Memory/runtime-deps: retain the native <code>node-llama-cpp</code> runtime only when local memory search is configured, so packaged installs can repair local embeddings without relying on unreachable global npm installs. Fixes #74777. Thanks @LLagoon3.</li>
|
||||
<li>Plugins/runtime-deps: replace stale symlinked mirror target roots before writing runtime-mirror temp files and skip rewriting already materialized hardlinks, so cross-version container upgrades no longer crash-loop on read-only image-layer paths while warm mirrors do less churn. Fixes #75108; refs #75069. Thanks @coletebou and @xiaohuaxi.</li>
|
||||
<li>Plugins/runtime-deps: keep bundled provider policy config loading from staging plugin runtime dependencies, so config reads no longer fail on locked-down <code>/var/lib/openclaw/plugin-runtime-deps</code> directories. Fixes #74971. Thanks @eurojojo.</li>
|
||||
<li>Plugins/runtime-deps: always write a dependency map in generated runtime-deps install manifests, so npm does not crash or prune staged bundled-plugin packages when the plan is empty. Fixes #74949. Thanks @hclsys.</li>
|
||||
<li>Security/outbound: strip re-formed HTML tags during plain-text sanitization so nested tag fragments cannot leave a CodeQL-detected <code><script></code> sequence behind. Thanks @vincentkoc.</li>
|
||||
<li>Security/secrets: compare credential bytes with padded timing-safe buffers instead of hashing candidate passwords before equality checks. Thanks @vincentkoc.</li>
|
||||
<li>Security/QQBot: sanitize debug log arguments before writing to <code>console.*</code>, so gateway payload fields cannot forge extra log lines when debug logging is enabled. Thanks @vincentkoc.</li>
|
||||
<li>QQBot: unify slash command auth and c2cOnly gating in the command registry, pass <code>allowQQBotDataDownloads</code> when sending slash command file attachments, align clear-storage with actual downloads directory, and add <code>/bot-me</code> to display sender user ID. (#73616) Thanks @cxyhhhhh.</li>
|
||||
<li>CLI/agents/status: keep <code>openclaw agents</code>, text <code>agents list</code>, and plain text <code>status</code> on read-only metadata paths so human output no longer preloads plugin runtimes or live channel scans before printing. Fixes #74195. Thanks @NianJiuZst.</li>
|
||||
<li>Agents/local models: derive context-window guard thresholds from the effective model window with 4k/8k safety floors, so small local models are no longer rejected by fixed 16k/32k preflight cutoffs. Fixes #42999. Thanks @chengjialu8888.</li>
|
||||
<li>PDF extraction: resolve PDF.js standard fonts from the installed package root and pass a filesystem path to the Node fallback extractor, so built-in font PDFs render without <code>file://</code> URL lookup failures. Fixes #51455; carries forward #70936, #54447, and #62175. Thanks @anyech, @JuanRdBO, and @solomonneas.</li>
|
||||
<li>Media: treat legacy Word/OLE attachments with <code>application/msword</code> or <code>application/x-cfb</code> MIME as binary so printable-looking <code>.doc</code> files are not embedded into prompts as text. Fixes #54176; carries forward #54380. Thanks @andyliu.</li>
|
||||
<li>Config: accept documented <code>browser.tabCleanup</code> keys in strict root config validation, so configured tab cleanup no longer fails before runtime reads it. Fixes #74577. Thanks @lonexreb and @ezdlp.</li>
|
||||
<li>Cron: validate disabled job schedule edits before persisting updates, so invalid cron changes no longer partially mutate stored jobs. Fixes #74459. Thanks @yfge.</li>
|
||||
<li>CLI/cron: warn when <code>openclaw cron add --message</code> omits a nonblank <code>--agent</code>, including blank agent values and session-key jobs, so scheduled agent-turn jobs make default-agent fallback explicit while system events stay quiet. Fixes #42196; carries forward #42245. Thanks @ethanclaw.</li>
|
||||
<li>CLI/progress: suppress nested progress spinners and line clears while TUI input owns raw stdin, so Crestodian <code>/status</code> no longer disturbs the active input row. (#75003) Thanks @velvet-shark.</li>
|
||||
<li>Channels/status: keep Telegram, Slack, and Google Chat read-only allowlist/default-target accessors on config-only paths, so status and channel summaries do not resolve SecretRef-backed runtime credentials. Thanks @eusine.</li>
|
||||
<li>Telegram: use durable message edits for streaming previews instead of native draft state, so generated replies no longer flicker through draft-to-message transitions that look like duplicates. (#75073) Thanks @obviyus.</li>
|
||||
<li>Telegram: clamp low long-polling client timeouts so configured <code>timeoutSeconds</code> values below the <code>getUpdates</code> poll window no longer force a fresh HTTPS connection every few seconds. Fixes #75114. Thanks @hpinho77.</li>
|
||||
<li>Active Memory: clarify the deprecated <code>modelFallbackPolicy</code> warning and config help so <code>modelFallback</code> is described as a chain-resolution last resort, not runtime failover. (#74602) Thanks @jeffrey701.</li>
|
||||
<li>Channels/Discord: keep read-only allowlist/default-target accessors from resolving SecretRef-backed bot tokens, so status and channel summaries no longer fail when tokens are only available in gateway runtime. (#74737) Thanks @eusine.</li>
|
||||
<li>Gateway/sessions: align session abort wait semantics across <code>chat</code>, <code>agent</code>, and <code>sessions</code> server methods so abort RPCs return after the targeted sessions actually halt instead of resolving early while runs are still draining. (#74751) Thanks @BunsDev.</li>
|
||||
<li>Agents/output: drop copied inbound metadata-only assistant replay turns before provider replay instead of synthesizing a placeholder, so Telegram and other channels cannot receive <code>[assistant copied inbound metadata omitted]</code> as model output. Fixes #74745. Thanks @adamwdear and @Marvae.</li>
|
||||
<li>Doctor/memory: suppress skipped embedding-readiness warnings for key-optional providers such as Ollama and LM Studio while preserving timeout and not-ready diagnostics. Fixes #74608 and #73882. Thanks @hclsys.</li>
|
||||
<li>Channels/groups: preserve observe-only turn suppression for prepared dispatch paths and restore deprecated channel turn runtime aliases, so passive observer/group flows stay silent while older plugins keep compiling. Thanks @vincentkoc.</li>
|
||||
<li>Feishu: skip empty-text messages (e.g. <code>{"text":""}</code>) that carry no media, so no blank user turn is written to the session and downstream LLM providers cannot reject the request with "messages must not be empty". (#74634) Thanks @xdengli and @hclsys.</li>
|
||||
<li>Feishu/Bitable: clean up newly created placeholder rows whose fields contain only default empty values while preserving meaningful link, attachment, user, number, boolean, and location values during create-app cleanup. (#73920) Carries forward #40602. Thanks @boat2moon.</li>
|
||||
<li>macOS app: keep attach-only mode and the Debug Settings launchd toggle marker-only, so launching with <code>--attach-only</code>/<code>--no-launchd</code> no longer uninstalls the Gateway LaunchAgent or drops active sessions. (#72174) Thanks @DolencLuka.</li>
|
||||
<li>macOS Canvas: stop auto-reloading the current A2UI host during push/eval/snapshot flows, so pushed A2UI content remains visible instead of returning to the empty Canvas shell. Fixes #73337. Thanks @Gr4via.</li>
|
||||
<li>Plugin SDK: restore the deprecated <code>plugin-sdk/zalouser</code> command-auth facade so published Lark/Zalo plugins that import it load on current hosts. Fixes #74702. Thanks @Goron01.</li>
|
||||
<li>Plugins/runtime-deps: include bundled provider plugins when <code>models.providers</code>, auth profiles, agent defaults, or subagent model refs configure that provider, while keeping inactive default-enabled provider plugins out of doctor repair. Refs #74307. Thanks @Skeptomenos.</li>
|
||||
<li>Plugins/runtime: resolve relative plugin <code>api.resolvePath</code> inputs against the plugin root instead of the host working directory, while keeping absolute and home paths user-resolved. Fixes #74718. Thanks @jimdawdy-hub.</li>
|
||||
<li>Plugins/runtime-deps: refresh mirrored root chunks through a temporary file before replacing the active copy, so failed refreshes do not delete chunks that running plugin imports still need. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/runtime-deps: prefer <code>require</code> conditional exports when building staged dependency aliases, so CommonJS-only plugin runtime deps such as <code>ws</code> do not resolve to ESM wrappers under Jiti. Fixes #74547. Thanks @aderius.</li>
|
||||
<li>Bonjour/Gateway: cap flapping advertiser restarts in a sliding window, so mDNS probing/name-conflict loops disable discovery instead of churning indefinitely on constrained hosts. Refs #74209 and #74242. Thanks @ndj888 and @Sanjays2402.</li>
|
||||
<li>Plugins/runtime-deps: verify staged package entry files before reusing mirrored runtime roots, so browser-control repairs incomplete <code>ajv</code>/MCP SDK installs after update instead of failing after restart on a missing <code>ajv/dist/ajv.js</code>. Refs #74630. Thanks @spickeringlr.</li>
|
||||
<li>Heartbeat: resolve <code>responsePrefix</code> template variables with the selected provider, model, and thinking context before delivering alerts or suppressing prefixed <code>HEARTBEAT_OK</code> replies. Fixes #43064; repairs #43065; supersedes #46858. Thanks @yweiii and @JunJD.</li>
|
||||
<li>Memory/LanceDB: show full memory UUIDs in the <code>memory_forget</code> candidate list so agents can pass the displayed ID back to targeted deletion without hitting the full-UUID validator. (#66913) Thanks @amittell.</li>
|
||||
<li>File-transfer plugin: require canonical read-path preflight authorization for <code>file.fetch</code>, fail closed when <code>dir.fetch</code> preflight entries are missing, absolute, or traversing, and recheck returned archive entries before handing archive bytes to callers. Carries forward #74134. Thanks @omarshahine.</li>
|
||||
<li>Channels/Feishu: retry file-typed iOS video resource downloads as <code>media</code> after a Feishu/Lark HTTP 502 and preserve the original 502 when the fallback also fails. Fixes #49855; carries forward #50164 and #73986. Thanks @alex-xuweilong.</li>
|
||||
<li>Providers/Amazon Bedrock: expose the full Claude Opus 4.7 thinking profile (<code>xhigh</code>, <code>adaptive</code>, and <code>max</code>) for Bedrock model refs, while keeping Opus/Sonnet 4.6 on adaptive-by-default, so <code>/think</code> menus and validation match the Anthropic transport behavior. Fixes #74701. Thanks @prasad-yashdeep, @sparkleHazard, @Sanjays2402, and @hclsys.</li>
|
||||
<li>Plugins/tokenjuice: compile the bundled plugin against tokenjuice 0.7.0's published OpenClaw host types instead of a local compatibility shim, so package contract drift fails in OpenClaw validation before release. Thanks @vincentkoc.</li>
|
||||
<li>OAuth/secrets: ignore root-level Google OAuth <code>client_secret_*.json</code> downloads so local client-secret files do not appear as commit candidates. (#74689) Thanks @jeongdulee.</li>
|
||||
<li>Memory: mirror <code>sqlite-vec</code> into packaged bundled-plugin runtime deps for the default memory plugin, so builtin vector search does not lose its SQLite extension after upgrading to 2026.4.27. Fixes #74692. Thanks @mozi1924.</li>
|
||||
<li>Gateway/startup: bound local discovery advertisement during startup, so a stuck discovery plugin can no longer keep the Gateway from reaching ready. Fixes #73865; refs #74630 and #74633. Thanks @lpendeavors, @moltar-bot, and @Saboor711.</li>
|
||||
<li>Gateway/models: serve the last successful model catalog while stale reloads refresh in the background, so Gateway control-plane and OpenAI-compatible requests no longer block behind model-provider rediscovery after model config changes. Refs #74135, #74630, and #74633. Thanks @DerFlash, @moltar-bot, and @Saboor711.</li>
|
||||
<li>CLI/status: resolve read-only channel setup runtime fallback from the packaged OpenClaw dist root, so <code>status --all</code>, <code>status --deep</code>, channel, and doctor paths do not crash when an external channel plugin needs setup metadata. Fixes #74693. Thanks @giangthb.</li>
|
||||
<li>SDK/events: keep per-run SDK event streams from surfacing duplicate raw chat projection frames, while normalizing chat-only projection frames and preserving raw access through <code>rawEvents</code>. Refs #74704. Thanks @BunsDev.</li>
|
||||
<li>SDK: report Gateway terminal <code>agent.wait</code> timeout snapshots with lifecycle metadata as <code>timed_out</code> while keeping bare wait deadlines non-terminal. Thanks @clawsweeper.</li>
|
||||
<li>Google Meet: block managed Chrome intro/test speech until browser health proves the participant is in-call, and expose <code>speechReady</code> diagnostics so login, admission, permission, and audio-bridge blockers no longer look like successful speech. Refs #72478. Thanks @DougButdorf.</li>
|
||||
<li>Slack/commands: keep native command argument menus on select controls for encoded choice values up to Slack's option limit and truncate fallback button labels to Slack's button-text limit, so long valid choices no longer render invalid Slack blocks. Thanks @slackapi.</li>
|
||||
<li>Agents/Codex: flush accepted debounced steering messages before normal app-server turn cleanup, so inbound follow-ups acknowledged as queued are not dropped when the turn completes before the debounce fires. Thanks @vincentkoc.</li>
|
||||
<li>Slack/interactive replies: keep rendered buttons and selects within Slack Block Kit value and count limits, and align command argument select values with Slack's option limit, so overlong agent-authored choices no longer make Slack reject the whole block payload. Thanks @slackapi.</li>
|
||||
<li>Slack/interactive replies: drop overlong Block Kit button URLs while preserving valid callback values, so malformed link buttons no longer make Slack reject the whole interactive reply. Thanks @slackapi.</li>
|
||||
<li>Slack/commands: truncate native command argument-menu confirmation text to Slack's dialog limit, so long plugin arg names no longer make fallback buttons render invalid Block Kit payloads. Thanks @slackapi.</li>
|
||||
<li>Slack/exec approvals: cap native approval metadata context to Slack's element and text limits, so large approval details no longer make Slack reject the approval card. Thanks @slackapi.</li>
|
||||
<li>Slack/exec approvals: cap native approval update fallback text to Slack's message limit while preserving the rendered approval blocks, so long commands no longer make resolved or expired approval cards stay stale after <code>chat.update</code> rejects <code>msg_too_long</code>. Thanks @slackapi.</li>
|
||||
<li>Slack/commands: cap native command argument-menu fallback rows to Slack's message block limit, so large plugin choice lists no longer make Slack reject the generated menu. Thanks @slackapi.</li>
|
||||
<li>Slack/commands: drop fallback command argument buttons whose encoded values exceed Slack's button-value limit, so one oversized plugin choice no longer makes Slack reject the whole menu. Thanks @slackapi.</li>
|
||||
<li>Slack/messages: merge message-tool presentation and interactive blocks on Slack sends, so buttons and selects are no longer dropped when a structured message body is also present. Thanks @slackapi.</li>
|
||||
<li>Slack/messages: cap Block Kit fallback text to Slack's send limit while preserving the rendered blocks, so long context fallbacks no longer make rich Slack messages fail with <code>msg_too_long</code>. Thanks @slackapi.</li>
|
||||
<li>Slack/messages: cap Block Kit fallback text on message edits while preserving the rendered blocks, so long context fallbacks no longer make Slack reject <code>chat.update</code> calls with <code>msg_too_long</code>. Thanks @slackapi.</li>
|
||||
<li>Channels/WhatsApp: require Baileys outbound message ids before marking auto-replies delivered, so transcript text and ack reactions no longer make failed group replies look sent. Fixes #49225. Thanks @TinyTb.</li>
|
||||
<li>CLI/update: scope packaged Node compile caches by OpenClaw version and install metadata, so global installs no longer reuse stale compiled chunks after package updates. Thanks @pashpashpash.</li>
|
||||
<li>Channels/Voice call: keep pre-auth webhook in-flight limiting active when socket remote address metadata is missing, so slow-body requests from stripped-IP proxy paths still share the fallback bucket. (#74453) Thanks @davidangularme.</li>
|
||||
<li>Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc.</li>
|
||||
<li>Channels/Microsoft Teams: treat configured <code>19:...@thread.tacv2</code> and legacy <code>19:...@thread.skype</code> team/channel IDs as already resolved during startup, avoiding false <code>channels unresolved</code> warnings while preserving Graph name lookup for display-name entries. Fixes #74683. Thanks @dseravalli.</li>
|
||||
<li>CLI/browser: preserve parent flags while lazy-loading browser subcommands, so <code>openclaw browser --json open</code> and <code>openclaw browser --json tabs</code> keep machine-readable output after reparsing. Fixes #74574. Thanks @devintegeritsm.</li>
|
||||
<li>Exec/elevated: preserve <code>turnSourceChannel</code> as <code>messageProvider</code> on approval-followup runs so <code>tools.elevated.allowFrom.<provider></code> checks no longer fail with <code>provider=null</code> after the user approves an async elevated command. Fixes #74646. Thanks @xhd2015.</li>
|
||||
<li>Plugins/runtime-deps: add <code>openclaw plugins deps</code> inspection and repair with script-free package-manager defaults shared across plugin installers, so operators can repair missing bundled runtime deps without corrupting JSON output or blocking unrelated conflict-free deps. Thanks @vincentkoc.</li>
|
||||
<li>Agents/output: strip internal <code>[tool calls omitted]</code> replay placeholders from user-facing replies while preserving visible reply whitespace. Fixes #74573. Thanks @blaspat.</li>
|
||||
<li>Providers/Google Vertex: route authorized_user ADC credentials through OpenClaw's REST transport so Docker installs using gcloud application-default credentials no longer crash in the Google SDK before requests are sent. Fixes #74628. Thanks @frankhal2001-design.</li>
|
||||
<li>ACP/resolver: fall through to thread-bound session resolution when an explicit <code>--session</code> token cannot be resolved while preserving the bad-token diagnostic when no thread binding exists, so Discord slash commands that auto-fill the current thread ID as the positional ACP target no longer return "Unable to resolve session target" errors. Fixes #66299. Thanks @hclsys, @kindomLee, and @martingarramon.</li>
|
||||
<li>Agents/sessions: emit a terminal lifecycle backstop when embedded timeout/error turns return without <code>agent_end</code>, so Gateway sessions no longer stay stuck in <code>running</code> after failover surfaces a timeout. Fixes #74607. Thanks @millerc79.</li>
|
||||
<li>Gateway/diagnostics: include stuck-session reason hints and recovery skip causes in warnings, so operators can tell whether a lane is waiting on active work, queued work, or stale bookkeeping. Thanks @vincentkoc.</li>
|
||||
<li>Providers/DeepSeek: expose native DeepSeek V4 <code>xhigh</code> and <code>max</code> thinking levels through the provider <code>resolveThinkingProfile</code> hook so <code>/think xhigh|max</code> applies the intended effort instead of falling back to base levels. (#73008) Thanks @ai-hpc.</li>
|
||||
<li>Agents/Codex: bound embedded-run cleanup, trajectory flushing, and command-lane task timeouts after runtime failures, so Discord and other chat sessions return to idle instead of staying stuck in processing. Thanks @vincentkoc.</li>
|
||||
<li>Heartbeat/exec: consume successful metadata-only async exec completions silently so Telegram and other chat surfaces no longer ask users for missing command logs after <code>No session found</code>. Fixes #74595. Thanks @gkoch02.</li>
|
||||
<li>Web fetch: add a documented <code>tools.web.fetch.ssrfPolicy.allowIpv6UniqueLocalRange</code> opt-in and thread it through cache keys and DNS/IP checks so trusted fake-IP proxy stacks using <code>fc00::/7</code> can work without broad private-network access. Fixes #74351. Thanks @jeffrey701.</li>
|
||||
<li>OpenAI Codex: restore <code>/verbose full</code> persistence and app-server tool-output forwarding, and retry Gateway E2E temp-home cleanup so debug runs do not regress on stale validation or cleanup flakes. Thanks @vincentkoc.</li>
|
||||
<li>Anthropic/Meridian: preserve text and thinking content seeded on <code>content_block_start</code> in anthropic-messages streams, so <code>[thinking, text]</code> replies no longer persist as empty turns or trigger empty-response fallbacks. Fixes #74410. Thanks @vyctorbrzezowski.</li>
|
||||
<li>Channels/Matrix: complete the cross-signing handshake on <code>openclaw matrix verify confirm-sas</code> so the operator's other Matrix device clears its <code>Verifying…</code> loop instead of staying stuck after the agent confirms. (#74542) Thanks @nklock.</li>
|
||||
<li>CLI/status: honor channel-specific model context-window overrides when reporting effective context, so channel-scoped sessions reflect the active window in <code>openclaw status</code>. Thanks @HemantSudarshan.</li>
|
||||
<li>Sandbox/Docker: tolerate Docker daemon unavailability when sandbox mode is off, so doctor and preflight checks no longer fail on installs that do not run the Docker daemon. Fixes #73671. Thanks @kaseonedge.</li>
|
||||
<li>Control UI/mobile: persist mobile chat settings through Lit-managed state and route mobile navigation through the same view-state path so chat panel toggles survive transitions on small viewports. Thanks @BunsDev.</li>
|
||||
<li>Control UI/exports: align sidebar trigger affordances across the resizable divider, mobile layout, and exported-HTML transcript template so the sidebar toggle and exported transcript sidebar render with consistent hit areas and styling. Thanks @BunsDev.</li>
|
||||
<li>Control UI/chat: disable the page refresh affordance while a chat run is active so accidental refreshes do not abort an in-flight reply. Thanks @Angfr95 and @BunsDev.</li>
|
||||
<li>Memory/LanceDB: return real memory records from <code>openclaw ltm list</code> (with optional <code>--limit</code> and createdAt ordering) instead of an empty placeholder, so the CLI surface matches the documented LTM listing contract. (#67952) Thanks @zhangyue19921010.</li>
|
||||
<li>Media: include redacted per-attempt resize failures and resolved model input capabilities in vision-pipeline errors so ARM64 image failures are diagnosable without closing the remaining routing investigation. Refs #74552. Thanks @1yihui.</li>
|
||||
<li>Control UI/i18n: route zh-CN agent, debug, channel-refresh, and exec-approval copy through the locale source while preserving the English <code>Cron Jobs</code> agent tab label and the security-audit command styling. Carries forward #39692 repair context. Thanks @hepeng154833488 and @vincentkoc.</li>
|
||||
<li>Auto-reply: honor explicit <code>silentReply.direct: "allow"</code> for clean empty or reasoning-only direct chat turns while keeping the default direct-chat empty-response guard conservative. Fixes #74409. Thanks @jesuskannolis.</li>
|
||||
<li>OpenAI Codex: send a non-empty Responses input item when a Codex turn only has systemPrompt-backed instructions, avoiding ChatGPT backend 400s from <code>input: []</code>. Fixes #73820. Thanks @woodhouse-bot.</li>
|
||||
<li>Ollama: normalize provider-prefixed tool-call names at the native stream boundary so Kimi/Ollama calls such as <code>functions.exec</code> dispatch as <code>exec</code> instead of missing configured tools. Fixes #74487. Thanks @afurm and @carreipeia.</li>
|
||||
<li>Security/audit: resolve configured model aliases before model-tier and small-parameter checks, so alias-based GPT-5/Codex configs no longer report false weak-model warnings. Fixes #74455. Thanks @blaspat.</li>
|
||||
<li>CLI/agent: isolate Gateway-timeout embedded fallback runs under explicit <code>gateway-fallback-*</code> sessions so accepted Gateway runs cannot race transcript locks or replace the routed conversation session. Fixes #62981. Thanks @HemantSudarshan.</li>
|
||||
<li>CLI/QR/device-pair: reject malformed public setup URLs before issuing mobile pairing bootstrap tokens, while keeping valid bare host:port setup URLs supported. Thanks @Lucenx9.</li>
|
||||
<li>Models/UI: hide unauthenticated providers from the default Web chat, <code>/models</code>, and model setup pickers while keeping explicit full-catalog browse paths through <code>view: "all"</code>, <code>/models <provider> all</code>, and <code>models list --all</code>. Fixes #74423. Thanks @guarismo and @SymbolStar.</li>
|
||||
<li>Ollama: keep explicit local model runs on target-provider runtime hooks when PI discovery is skipped, so one-shot Ollama calls no longer cold-load unrelated provider runtimes before streaming. Fixes #74078. Thanks @sakalaboator.</li>
|
||||
<li>Slack/prompts: rely on Slack <code>interactiveReplies</code> guidance instead of generic <code>inlineButtons</code> config hints so enabled Slack button directives are not contradicted. Fixes #46647. Thanks @jeremykoerber.</li>
|
||||
<li>Slack/reactions: treat duplicate <code>already_reacted</code> responses as idempotent success so repeated agent reaction adds no longer surface as tool failures. Fixes #69005. Thanks @shipitsteven and @martingarramon.</li>
|
||||
<li>Channels/Discord: cool down Cloudflare/Error 1015 HTML 429 REST failures during startup application lookup and gateway metadata fetches, add <code>channels.discord.applicationId</code> as an app-id lookup bypass, sanitize HTML bodies before logging, and honor Retry-After before falling back to a conservative cooldown. Fixes #38853. (#74489) Thanks @djgeorg3 and @Garyko0730.</li>
|
||||
<li>Slack/tools: expose <code>fileId</code> in the shared message tool schema so <code>download-file</code> can receive Slack attachment IDs from inbound placeholders. Fixes #45574. Thanks @chadvegas.</li>
|
||||
<li>Exec: reject invalid per-call <code>host</code> values instead of silently falling back to the default target, so hostname-like values fail before commands run. Fixes #74426. Thanks @scr00ge-00 and @vyctorbrzezowski.</li>
|
||||
<li>Google/Gemini: send non-empty placeholder content when a Gemini run is triggered with empty or filtered user content, avoiding <code>contents is not specified</code> API errors. Thanks @CaoYuhaoCarl.</li>
|
||||
<li>Heartbeat: preserve non-task <code>HEARTBEAT.md</code> context around <code>tasks:</code> blocks and apply <code>agents.defaults.heartbeat</code> to all agents unless per-agent heartbeat entries restrict scope. Thanks @Sekhar03.</li>
|
||||
<li>Markdown: preserve paragraph breaks inside loose list items in shared outbound formatting while keeping tight list spacing stable. Thanks @Lucenx9.</li>
|
||||
<li>Build/Gateway: route restart, shutdown, respawn, diagnostics, command-queue cleanup, and runtime cleanup through one stable gateway lifecycle runtime entry so rebuilt packages do not strand long-running gateways on stale hashed chunks. Carries forward #73964. Thanks @pashpashpash.</li>
|
||||
<li>Memory/wiki: keep broad shared-source and generated related-link blocks from turning every page into a search hit, cap noisy backlinks, support all-term searches such as people-routing queries, and prefer readable page body snippets over generated metadata. Thanks @vincentkoc.</li>
|
||||
<li>Cron/Gateway: abort and bounded-clean up timed-out isolated agent turns before recording the timeout, so stale cron sessions cannot leave Discord or other chat lanes stuck in <code>processing</code> after a timeout. Thanks @vincentkoc.</li>
|
||||
<li>Agents/errors: suppress malformed streaming tool-call JSON fragments before they reach chat surfaces while preserving provider request-validation diagnostics. Fixes #59076; keeps #59080 as duplicate coverage. (#59118) Thanks @singleGanghood.</li>
|
||||
<li>CLI/models: restore provider-filtered <code>models list --all --provider <id></code> rows for providers without manifest/static catalog coverage, including Anthropic and Amazon Bedrock, while keeping the compatibility fallback off expensive availability and resolver paths. Thanks @shakkernerd.</li>
|
||||
<li>CLI/models: keep manifest auth-evidence credentials visible across <code>models status</code>, auth probes, and PI model discovery so workspace-scoped provider auth does not disagree between listing, probing, and execution. Thanks @shakkernerd.</li>
|
||||
<li>CLI/models: move local credential evidence such as Google Vertex ADC into generic plugin manifest setup metadata so the model-list auth index stays declarative without provider-specific runtime branches. Thanks @shakkernerd.</li>
|
||||
<li>CLI/models: compute the <code>models list</code> Auth column through one command-local provider auth index so row rendering no longer repeats auth profile, env, configured-provider, AWS, or synthetic-auth checks per model row. Thanks @shakkernerd.</li>
|
||||
<li>CLI/models: move the OpenAI listable catalog into the plugin manifest so <code>models list --all --provider openai</code> uses the manifest fast path instead of loading provider runtime normalization hooks. Thanks @shakkernerd.</li>
|
||||
<li>CLI/tools: keep the Gateway <code>tools.*</code> RPC namespace out of plugin command discovery and managed proxy startup, so stray commands like <code>openclaw tools effective</code> fail quickly instead of cold-loading plugin metadata. Refs #73477. Thanks @oromeis.</li>
|
||||
<li>CLI/status: keep default text <code>openclaw status --usage</code> on metadata-only channel scans unless <code>--deep</code> or <code>--all</code> is set, and send stray <code>openclaw tools --help</code> through the precomputed root-help fast path so latency-triage commands avoid plugin/runtime cold loads before printing. Refs #73477 and #74220. Thanks @oromeis and @NianJiuZst.</li>
|
||||
<li>Agents/diagnostics: trace embedded-run startup and preparation stage timings before model I/O, and warn only on severe slow stages, so Docker/VPS latency reports can identify whether plugin loading, auth/model resolution, tool inventory, bootstrap, MCP/LSP, resource loading, or stream setup is dominating pre-run latency without noisy normal logs. Refs #73428. Thanks @Dimaoggg, @quangtran88, and @Heyvhuang.</li>
|
||||
<li>Agents/subagents: cache persisted subagent run registry reads by file signature while preserving fresh-parse isolation, so busy gateways stop reparsing unchanged <code>subagents/runs.json</code> on controller/list/status hot paths. Refs #72338. Thanks @argus-as.</li>
|
||||
<li>Gateway/clients: wait for the event loop to become responsive before opening Gateway WebSocket RPC/probe/client connections while charging that readiness wait to caller timeouts, so Windows deferred module-evaluation stalls no longer turn healthy loopback gateways into false handshake timeouts across status, TUI, ACP, MCP, node-host, and plugin client paths. Refs #74279 and #48270. Thanks @wongcode and @joost-heijden.</li>
|
||||
<li>Gateway/Windows: read listener command lines via PowerShell before falling back to <code>wmic</code>, so restart health can recognize OpenClaw listeners on modern Windows installs and avoid long anonymous-port waits. Refs #74280. Thanks @zym951223.</li>
|
||||
<li>Plugins/runtime-deps: record process start-time in bundled dependency install locks and expire recycled-PID locks, so Docker gateway restarts recover from stale <code>.openclaw-runtime-deps.lock</code> directories without waiting through repeated five-minute timeouts. Fixes #74346. (#74361) Thanks @jhsmith409.</li>
|
||||
<li>Plugins/runtime-deps: memoize packaged bundled runtime dist-mirror preparation after the first successful pass while keeping source-checkout mirrors refreshable, so constrained Docker/VPS installs avoid repeated root scans before chat turns. Refs #73428, #73421, #73532, and #73477. Thanks @Dimaoggg, @oromeis, @oadiazp, @jmfraga, @bstanbury, @antoniusfelix, and @jkobject.</li>
|
||||
<li>Channels/Discord: treat bare numeric outbound targets that match the effective Discord DM allowlist as user DMs while preserving account-specific legacy <code>dm.allowFrom</code> precedence over inherited root <code>allowFrom</code>. (#74303) Thanks @Squirbie.</li>
|
||||
<li>Channels/Discord/Slack: share one DM policy/allowlist resolver across runtime, setup, allowlist editing, and doctor repair, so legacy <code>dm.policy</code> / <code>dm.allowFrom</code> compatibility migrates to canonical <code>dmPolicy</code> / <code>allowFrom</code> without divergent access checks. Thanks @Squirbie.</li>
|
||||
<li>Control UI: make the chat sidebar split divider focusable, keyboard-resizable, ARIA-described, and pointer-event based so sidebar resizing works without a mouse. Thanks @BunsDev.</li>
|
||||
<li>Agents/usage: keep PI embedded-run telemetry attributed to the resolved model provider instead of the PI harness label, so OpenRouter and other provider-backed turns report the right provider in session usage and traces. Thanks @vincentkoc.</li>
|
||||
<li>Agents/attribution: send OpenClaw attribution headers on native OpenAI and Codex traffic, including SDK transports, realtime voice and TTS, device-code auth, WHAM usage, and remote embeddings, so PI-origin defaults no longer leak into provider requests. Thanks @vincentkoc.</li>
|
||||
<li>Agents/auth: keep OAuth auth profiles inherited from the main agent read-through instead of copying refresh tokens into secondary agents, and refresh Codex app-server tokens against the owning store so multi-agent swarms avoid reused refresh-token failures. Fixes #74055. Thanks @ClarityInvest.</li>
|
||||
<li>Channels/Telegram: honor <code>ALL_PROXY</code> / <code>all_proxy</code> and service-level <code>OPENCLAW_PROXY_URL</code> when constructing the HTTP/1-only Telegram Bot API transport, so Windows and service installs that rely on those proxy settings no longer fall back to direct egress. Fixes #74014; refs #74086. Thanks @SymbolStar.</li>
|
||||
<li>Channels/Telegram: keep raw host/network-unreachable Bot API connect failures non-fatal and route tagged polling uncaught exceptions through the Telegram restart path, so transient reachability failures no longer kill the Gateway or leave long polling stuck. Fixes #60515; refs #74540. Thanks @HemantSudarshan, @thacid22, and @ewimsatt.</li>
|
||||
<li>Channels/Telegram: continue polling when <code>deleteWebhook</code> hits a transient network failure but <code>getWebhookInfo</code> confirms no webhook is configured, so startup does not retry cleanup forever after the webhook was already removed. Refs #74086; carries forward #47384. Thanks @clovericbot.</li>
|
||||
<li>Channels/Telegram: retry native quote replies without <code>reply_parameters.quote</code> when Telegram returns <code>QUOTE_TEXT_INVALID</code>, so stale or truncated quote excerpts no longer drop the whole reply. Fixes #74581. Thanks @moeedahmed.</li>
|
||||
<li>Channels/Telegram: apply strict safe-send retry to inbound final replies when grammY wraps a pre-connect failure, while leaving ambiguous plain network envelopes single-shot to avoid duplicate visible messages. Fixes #74203. Thanks @nanli2000cn.</li>
|
||||
<li>Channels/Telegram: surface polling liveness warnings in channel status and doctor when a running long-poller has not completed <code>getUpdates</code> after startup grace or its transport activity is stale, so silent polling failures no longer look clean. Refs #74299. Thanks @lolaopenclaw.</li>
|
||||
<li>Channels/Telegram: publish webhook runtime state and warn when <code>setWebhook</code> has not completed after startup grace, so webhook-mode accounts no longer look healthy while registration is still failing or retrying. Refs #74299. Thanks @lolaopenclaw and @martingarramon.</li>
|
||||
<li>Channels/Telegram: bound native command menu <code>deleteMyCommands</code> and <code>setMyCommands</code> Bot API calls and allow the same timeout-triggered transport fallback retry as other startup control calls, so Windows/WSL network stalls cannot leave command sync hanging behind an otherwise running provider. Refs #74086. Thanks @SymbolStar.</li>
|
||||
<li>ACP/commands: accept forwarded ACP timeout config controls in the OpenClaw bridge, treat unsupported discard-close controls as recoverable cleanup, and restore native <code>/verbose full</code> plus no-arg status behavior, so Discord command menus and nested ACP turns no longer fail on supported session controls. Thanks @vincentkoc.</li>
|
||||
<li>Codex harness: interrupt and release native app-server turns that go quiet after an OpenClaw dynamic-tool response without sending <code>turn/completed</code>, so Discord and other chat lanes do not stay stuck in <code>processing</code>. Thanks @vincentkoc.</li>
|
||||
<li>Codex harness: bound OpenClaw dynamic tool responses to 30 seconds and fail closed with an explicit tool result when the app-server bridge would otherwise strand the turn in <code>processing</code>. Thanks @vincentkoc.</li>
|
||||
<li>TUI/status: clear stale <code>streaming</code> footer state when a final event arrives after the active run was already cleared and no tracked runs remain, while preserving concurrent-run ownership and inactive local <code>/btw</code> terminal handling. Fixes #64825; carries forward #64842, #64843, #64847, and #64862. Thanks @briandevans and @Yanhu007.</li>
|
||||
<li>Channels/Discord: fail startup closed when Discord cannot resolve the bot's own identity and keep mention gating active when only configured mention patterns can detect mentions, so the provider no longer continues with a missing bot id. Fixes #42219; carries forward #46856 and #49218. Thanks @education-01 and @BenediktSchackenberg.</li>
|
||||
<li>Channels/Discord: split long CJK replies at punctuation and code-point-safe fallback boundaries so Discord chunking stays readable without corrupting astral characters. Fixes #38597; repairs #71384. Thanks @p3nchan.</li>
|
||||
<li>TUI: keep the streaming watchdog alive across active tool/lifecycle proof-of-life, pause it during disconnects, and reload history after stale reconnect runs so long-running chats stop flipping to false idle or hanging on stale streaming. Fixes #69081. Thanks @EenvoudJasper.</li>
|
||||
<li>Browser/gateway: ignore Playwright dialog-close races from <code>Page.handleJavaScriptDialog</code> so browser automation no longer crashes the Gateway when a dialog disappears before Playwright accepts it. (#40067) Thanks @randyjtw.</li>
|
||||
<li>Cron/Gateway: defer missed isolated agent-turn catch-up out of the channel startup window, so overdue cron work cannot starve Discord or Telegram while providers connect after a restart. Thanks @vincentkoc.</li>
|
||||
<li>Heartbeat/cron: defer heartbeat turns while cron work is active or queued, add opt-in <code>heartbeat.skipWhenBusy</code> for subagent/nested lane pressure, and retry busy skips without advancing the schedule so local Ollama hosts do not run heartbeat and cron prompts concurrently. Fixes #50773. Thanks @scottgl9.</li>
|
||||
<li>Agents/thinking: honor configured model <code>compat.supportedReasoningEfforts</code> entries that include <code>xhigh</code>, so custom OpenAI-compatible provider refs expose and validate <code>/think xhigh</code> consistently across command menus, Gateway sessions, agent CLI, and <code>llm-task</code>. Carries forward #48904. Thanks @Milchstrassse and @wufunc.</li>
|
||||
<li>Vercel AI Gateway: expose provider-owned <code>/think xhigh</code> for trusted OpenAI/Codex upstream refs and Claude adaptive thinking for Anthropic upstream refs, while leaving untrusted namespaced refs on base levels. Carries forward #41561. Thanks @Zcg2021.</li>
|
||||
<li>Plugins/runtime-deps: prune stale <code>openclaw-unknown-*</code> bundled runtime dependency roots during Gateway startup while keeping recent or locked roots, so old staging debris cannot keep growing across restarts. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/runtime-deps: include ten more root-package runtime dependencies (<code>@agentclientprotocol/sdk</code>, <code>@lydell/node-pty</code>, <code>croner</code>, <code>dotenv</code>, <code>jiti</code>, <code>json5</code>, <code>jszip</code>, <code>markdown-it</code>, <code>tar</code>, <code>web-push</code>) in <code>MIRRORED_CORE_RUNTIME_DEP_NAMES</code> so they are mirrored into the runtime-deps tree alongside <code>semver</code> and <code>tslog</code>, preventing <code>Cannot find package 'X'</code> failures from core dist code (for example <code>qmd-manager</code>, <code>cron/schedule</code>, <code>infra/archive</code>, <code>infra/push-web</code>, <code>infra/backup-create</code>, <code>process/supervisor/adapters/pty</code>) when no enabled extension owns the dependency. Adds a static drift guard test that scans <code>src/</code> for value imports of root-package deps and fails CI when one is missing from the mirror allowlist or extension-owned set. Refs #74199. Thanks @maxpuppet.</li>
|
||||
<li>Ollama: compose caller abort signals with guarded-fetch timeouts for native <code>/api/chat</code> streams, so <code>/stop</code> and early cancellation still interrupt local Ollama requests that also carry provider timeout budgets. Refs #74133. Thanks @obviyus.</li>
|
||||
<li>Doctor/TTS: migrate legacy <code>messages.tts.enabled</code>, agent TTS, channel TTS, and voice-call plugin TTS toggles to <code>auto</code> mode during <code>openclaw doctor --fix</code>, matching the documented TTS config contract. Thanks @vincentkoc.</li>
|
||||
<li>CLI/logs: fall back to the configured Gateway file log when implicit loopback Gateway connections close or time out before or during <code>logs.tail</code>, so <code>openclaw logs</code> still works while diagnosing local-model Gateway disconnects. Refs #74078. Thanks @sakalaboator.</li>
|
||||
<li>MCP/plugins: stringify non-array plugin tool results with chat-content coercion instead of default object stringification, so MCP callers receive useful JSON/text content from plugin tools. Thanks @vincentkoc.</li>
|
||||
<li>Active Memory/QMD: make gateway-start QMD refresh opt-in via <code>memory.qmd.update.startup</code>, keep normal memory access lazy, preserve interactive file watching, and align watcher dependency/build ignores with QMD's scanner so cold gateway startup no longer imports or initializes QMD by default. Thanks @codexGW.</li>
|
||||
<li>Channels/Discord: remove Discord-owned queued-run timeout replies through the shared channel lifecycle queue while preserving message ordering and compatibility timeout constants, so long Discord turns stay governed by session/tool/runtime lifecycle instead of channel fallback errors. Thanks @codexGW.</li>
|
||||
<li>Agents/tools: clamp <code>process.poll</code> waits to 30 seconds, advertise that cap in the tool schema, and honor abort signals while waiting, so long command polls cannot pin agent responsiveness after cancellation. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK: add tracked Discord component-message helpers and a Telegram account-resolution compatibility facade, so existing plugins using those subpaths resolve while new plugins stay on generic channel SDK contracts. Thanks @vincentkoc.</li>
|
||||
<li>Shared labels: preserve Unicode combining marks and NFC-equivalent accented text in group/channel slug normalization so non-Latin labels no longer lose meaningful characters. Fixes #58932; carries forward #58942 and #58995. Thanks @fengqing-git, @Starhappysh, and @koen666.</li>
|
||||
<li>Channels/Telegram: include probed video width and height when sending regular Telegram videos, so portrait clips render with the correct orientation instead of being stretched by clients. (#18915) Thanks @storyarcade.</li>
|
||||
<li>Docs/Hetzner: clarify that SSH tunnel access requires <code>AllowTcpForwarding local</code> before running <code>ssh -L</code>, so hardened VPS sshd configs do not block loopback Gateway access. Fixes #54557; carries forward #54564; refs #54954. Thanks @satishkc7, @blackstrype, and @Aftabbs.</li>
|
||||
<li>Agents/config: preserve authored <code>agents.defaults.params</code> and per-model <code>agents.defaults.models[].params</code> during narrowed internal config writes, so OpenAI transport overrides such as <code>transport: "sse"</code> and <code>openaiWsWarmup: false</code> are not stripped from <code>openclaw.json</code>. Fixes #73607; refs #73428. Thanks @quangtran88.</li>
|
||||
<li>Agents/model config: resolve per-model extra params through canonical model keys while preserving legacy double-prefixed fallback entries, so provider-prefixed model ids such as <code>openrouter/auto</code> keep their configured runtime params. (#44319) Thanks @HenryXiaoYang.</li>
|
||||
<li>Gateway/shutdown: report structured shutdown warnings and HTTP close timeout warnings through <code>ShutdownResult</code> while preserving lifecycle hook hardening. Carries forward #41296. Thanks @edenfunf.</li>
|
||||
<li>Control UI: keep Agents Overview and config-form select dropdowns on their configured value after options render while preserving inherited agent model placeholders. Fixes #40352; carries forward #52948. Thanks @xiaoquanidea.</li>
|
||||
<li>Agents/exec: launch zsh, bash, and fish host exec shells with startup files suppressed while preserving existing PATH fallbacks, so daemon env is not overridden by shell startup files. Carries forward #40200; fixes #40179. Thanks @NewdlDewdl.</li>
|
||||
<li>Plugins/QA: prebuild the private QA channel runtime before plugin gauntlet source runs so wrapper CPU/RSS measurements are not polluted by private QA dist rebuild work. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/QA: add a Kitchen Sink plugin gauntlet that installs the external package, checks command inventory, MCP tools, channel status, provider turns, gateway RSS, CPU, and fatal log anomalies. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/config: reuse the bundled plugin alias scan within a single config normalization pass, so Kitchen Sink-style plugin configs no longer peg Gateway CPU by repeatedly rescanning bundled metadata before agent turns. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/channels: reject malformed runtime channel registrations that omit required config helpers before they can poison channel status. Thanks @vincentkoc.</li>
|
||||
<li>MCP/plugins: serialize raw plugin tool return values through the plugin-tools MCP bridge so Kitchen Sink-style tools no longer surface <code>undefined</code> content. Thanks @vincentkoc.</li>
|
||||
<li>Gateway/reload: bound default restart deferral and SIGUSR1 restart drain to five minutes while preserving explicit <code>deferralTimeoutMs: 0</code> indefinite waits, so stale active work accounting cannot block config reloads forever. Thanks @vincentkoc.</li>
|
||||
<li>Active Memory: register the prompt-build hook with the configured recall timeout plus setup grace instead of the 150s maximum budget, so default memory recall cannot delay turn startup for multiple minutes. Thanks @vincentkoc.</li>
|
||||
<li>Gateway/readiness: include an <code>eventLoop</code> diagnostic block in local or authenticated <code>/readyz</code> responses with event-loop delay (p99 and max), event-loop utilization, CPU core ratio, and a <code>degraded</code> flag, so operators can see when slow startups or runaway turns stall the event loop. Thanks @vincentkoc.</li>
|
||||
<li>Gateway/agents: schedule accepted agent runs after the accepted RPC frame has a chance to flush, so pre-turn prompt/context work is less likely to starve immediate <code>agent.wait</code> callers. Thanks @vincentkoc.</li>
|
||||
<li>CLI/update: tolerate stale memory-runtime import failures during best-effort CLI process teardown, so <code>openclaw update</code> replacing hashed runtime chunks before the finalizer runs no longer surfaces as exit-time <code>Cannot find module</code> noise. Thanks @vincentkoc.</li>
|
||||
<li>CLI/channels logs: reuse the rolling log-file resolver so <code>openclaw channels logs</code> falls back to the active dated log across date boundaries without reading unrelated custom log files. Fixes #42875; carries forward #42904 and #43043. Thanks @ethanclaw and @wdskuki.</li>
|
||||
<li>CLI/update: skip tracked plugins disabled in config during post-update plugin sync before npm, ClawHub, or marketplace update checks, preserving their install records without failing the update. Fixes #73880. Thanks @islandpreneur007.</li>
|
||||
<li>Control UI: fix Peak Error Hours showing incorrect hourly rates when the browser's timezone observes DST, by storing hourly message counts with UTC date keys and using DST-aware <code>Date.getHours()</code> for local conversion. Also extract <code>accumulateMessageCounts</code> helper to reduce duplicated daily/hourly aggregation logic. (#49396) Thanks @konanok.</li>
|
||||
<li>iMessage: normalize known leading attributedBody corruption markers on sent-message echo text keys so delayed reflected echoes with U+FFFD/U+FFFE/U+FFFF/FEFF prefixes are dropped without collapsing interior text. Fixes #59973; carries forward #59980 and #62191. Thanks @neeravmakwana and @maguilar631697.</li>
|
||||
<li>Security/audit: recognize dangerous node command IDs as valid <code>gateway.nodes.denyCommands</code> entries, so audit only warns on real typos or unsupported patterns. (#56923) Thanks @chziyue.</li>
|
||||
<li>Cron: treat implicit text payloads with agent-turn overrides as agent turns, preserving model overrides for scheduled text prompts instead of pruning them as system events. Fixes #28905. (#64060) Thanks @liaoandi.</li>
|
||||
<li>Telegram/exec approvals: stop treating general Telegram chat allowlists and <code>defaultTo</code> routes as native exec approvers; Telegram now uses explicit <code>execApprovals.approvers</code> or owner identity from <code>commands.ownerAllowFrom</code>, matching the first-pairing owner bootstrap path. Thanks @pashpashpash.</li>
|
||||
<li>Plugins/providers: keep Gateway startup primary-model discovery on metadata-only provider entries and reuse active non-speech capability providers even with explicit plugin entries, avoiding unnecessary provider registry loads during startup and media capability checks. Fixes #73729, #73835, and #73793; carries forward #73853 and #73794. Thanks @sg1416-zg, @brokemac79, and @poolside-ventures.</li>
|
||||
<li>Chat commands: route sensitive group <code>/diagnostics</code> and <code>/export-trajectory</code> approvals and results to a private owner route, preferring same-surface DMs before falling back to the first configured owner route, so Discord group invocations can land in Telegram when that is the primary owner interface. Thanks @pashpashpash.</li>
|
||||
<li>Gateway/hooks: keep successful <code>deliver:false</code> agent hooks silent, log a hook audit record for suppressed success announcements, and suppress fallback summaries after attempted hook delivery while still surfacing failed hook runs. Repairs #55761; builds on #36332 and #49234. Thanks @EffortlessSteven, @cioclawcode, and @BrennerSpear.</li>
|
||||
<li>Plugin SDK/Discord: restore a deprecated <code>openclaw/plugin-sdk/discord</code> compatibility facade and the legacy compat group-policy warning export for the published <code>@openclaw/discord@2026.3.13</code> package, covering its config, account, directory, status, and thread-binding imports while keeping new plugins on generic SDK subpaths. Fixes #73685; supersedes #73703. Thanks @rderickson9 and @SymbolStar.</li>
|
||||
<li>Channels/Discord: suppress duplicate gateway monitors when multiple enabled accounts resolve to the same bot token, preferring config tokens over default env fallback and reporting skipped duplicates as disabled. Supersedes #73608. Thanks @kagura-agent.</li>
|
||||
<li>CLI/health: build channel health summaries from inspected credential metadata plus runtime state, so <code>openclaw health --json</code> reports Discord <code>running</code>, <code>connected</code>, and <code>tokenSource</code> consistently with channel status. Fixes #44354. Thanks @ferenc-acs.</li>
|
||||
<li>Control UI/Talk: decode Google Live binary WebSocket JSON frames and stop queued browser audio on interruption or shutdown, so browser Talk leaves <code>Connecting Talk...</code> and barge-in no longer plays stale audio. Fixes #73601 and #73460; supersedes #73466. Thanks @Spolen23 and @WadydX.</li>
|
||||
<li>Channels/Discord: ignore stale route-shaped conversation bindings after a Discord channel is reconfigured to another agent, while preserving explicit focus and subagent bindings. Fixes #73626. Thanks @ramitrkar-hash.</li>
|
||||
<li>Agents/bootstrap: pass pending BOOTSTRAP.md contents through the first-run user prompt while keeping them out of privileged system context, and show limited bootstrap guidance when workspace file access is unavailable. Fixes #73622. Thanks @mark1010.</li>
|
||||
<li>ACP/tasks: classify parent-owned ACP sessions as background work regardless of persistent runtime mode, and close terminal stale ACP sessions when no active binding remains, so delegated ACP output reports through the parent task notifier instead of acting like a normal foreground chat session. Refs #73609. Thanks @joerod26.</li>
|
||||
<li>Tasks: keep terminal mirrored TaskFlow timestamps pinned to task completion time and let maintenance repair stale mirrors, so ACP terminal delivery updates no longer leave inconsistent flow audits. Refs #73609. Thanks @joerod26.</li>
|
||||
<li>Gateway/sessions: add conservative stuck-session recovery that releases only stale session lanes while active embedded runs, reply operations, and lane tasks remain serialized, so queued follow-ups can drain without aborting legitimate long-running turns. Refs #73581, #73655, #73652, #73705, #73647, #73602, #73592, and #73601. Thanks @WS-Q0758, @bryangauvin, @spenceryang1996-dot, @bmilne1981, @mattmcintyre, @Vksh07, and @Spolen23.</li>
|
||||
<li>Plugins: cache unchanged plugin manifest loads by file signature, reducing repeated JSON/JSON5 parsing and manifest normalization in bursty startup and runtime registry paths. Refs #73532 and #73647; carries forward #73678. Thanks @TheDutchRuler.</li>
|
||||
<li>Plugins/runtime-deps: cache unchanged bundled runtime mirror dist-file materialization decisions and close file-lock handles on owner-write failures, reducing repeated startup chunk scans and avoiding FileHandle-GC recovery stalls. Refs #73532. Thanks @oadiazp and @bstanbury.</li>
|
||||
<li>Plugins/runtime-deps: retry and defer transient cleanup failures for owned runtime staging directories so CLI startup no longer aborts after a successful bundled dependency swap. Refs #73903. Thanks @bobfreeman1989.</li>
|
||||
<li>Plugins/runtime-deps: cache bundled runtime-deps JSON/package files by file signature, reducing repeated staged-runtime metadata reads during bundled channel startup. Refs #73647 and #73705. Thanks @mattmcintyre and @bmilne1981.</li>
|
||||
<li>Plugins/runtime-deps: delegate bundled plugin dependency staging to complete npm/pnpm install plans with durable runtime state, removing retained-manifest and source-checkout cache reconciliation from Gateway startup. Refs #73532. Thanks @oadiazp, @bstanbury, and @jmfraga.</li>
|
||||
<li>Plugins/runtime-deps: replace Gateway-start root chunk dependency inference with explicit mirrored-root dependency metadata, reducing staged runtime scans while preserving lazy per-plugin installs. Refs #73532. Thanks @oadiazp and @bstanbury.</li>
|
||||
<li>Plugins/runtime-deps: run pnpm staged installs outside the repository workspace and disable pnpm release-age gates for exact bundled runtime dependency materialization, so bundled plugin dependency repair writes packages into the generated stage without blocking fresh packaged dependencies. Refs #73532. Thanks @oadiazp and @bstanbury.</li>
|
||||
<li>CLI/TUI: keep <code>chat.history</code> off model-catalog discovery so initial Gateway-backed TUI history loads cannot block behind slow provider/plugin model scans on low-core hosts. Refs #73524. Thanks @harshcatsystems-collab.</li>
|
||||
<li>Channels/WhatsApp: flag recently reconnected linked accounts in channel status even when the socket is currently healthy, so flapping WhatsApp Web sessions no longer look clean after a brief reconnect. Refs #73602. Thanks @Vksh07.</li>
|
||||
<li>Channels/WhatsApp: log shared dispatcher delivery failures with reply kind, message id, chat id, and connection id, so typing-without-send reports can identify whether the WhatsApp send path rejected a generated reply. Refs #74269. Thanks @tomcosta-git.</li>
|
||||
<li>Feishu: suppress distinct late <code>final</code> text deliveries after a streaming card has already closed, while keeping media attachments deliverable, so late-finals no longer reopen duplicate Feishu cards. Fixes #71977. (#72294) Thanks @MonkeyLeeT.</li>
|
||||
<li>Gateway: expose <code>gateway.handshakeTimeoutMs</code> in config, schema, and docs while preserving <code>OPENCLAW_HANDSHAKE_TIMEOUT_MS</code> precedence, so loaded or low-powered hosts can tune local WebSocket pre-auth handshakes without patching dist files. Supersedes #51282; refs #73592 and #73652. Thanks @henry-the-frog.</li>
|
||||
<li>Gateway/TUI/status: align configured and env-based WebSocket handshake budgets across local clients, probes, and fallback RPCs while preserving explicit status timeouts and paired-device auth fallback, so slow local gateways are not marked unreachable by a shorter client watchdog. Refs #73524, #73535, #73592, and #73602. Thanks @harshcatsystems-collab, @DJBlackhawk, and @Vksh07.</li>
|
||||
<li>Gateway/startup: return retryable <code>UNAVAILABLE</code> during the sidecar startup window and keep CLI/TUI/status clients retrying inside their existing timeout budget, so early connects no longer surface as terminal handshake failures. Fixes #73652. Thanks @spenceryang1996-dot.</li>
|
||||
<li>Gateway/proxy: bypass inherited proxy environment for local Gateway control-plane WebSockets to <code>localhost</code> as well as loopback IPs, so Windows/WSL proxy settings cannot intercept local CLI/TUI Gateway connections. Supersedes #73474; refs #73602. Thanks @DhtIsCoding.</li>
|
||||
<li>Doctor/Gateway: use a lightweight <code>status</code> RPC without channel summary work for doctor Gateway liveness, so slow health snapshots do not falsely drive service restart repair. Fixes #64400; supersedes #64511. Thanks @CHE10X and @EronFan.</li>
|
||||
<li>Agents/auth: scope external CLI credential discovery to configured providers during model auth status and startup prewarm, so opencode-only and other single-provider gateways do not block on unrelated Claude CLI Keychain probes. Fixes #73908. Thanks @Ailuras.</li>
|
||||
<li>Agents/model selection: resolve slash-form aliases before provider/model parsing and keep alias-resolved primary models subject to transient provider cooldowns, so cron and persisted sessions do not retry cooled-down raw aliases. Fixes #73573 and #73657. Thanks @akai-shuuichi and @hashslingers.</li>
|
||||
<li>Agents/Claude CLI: reuse already-cached macOS Keychain credentials for no-prompt Claude credential reads, so doctor/runtime checks do not miss fresh interactive Claude auth. Fixes #73682. Thanks @RyanSandoval.</li>
|
||||
<li>Agents/Claude CLI doctor: scope workspace and project-dir checks to agents that actually use the Claude CLI runtime, so non-default Claude agents no longer make the default agent look Claude-backed. Fixes #73903. Thanks @bobfreeman1989.</li>
|
||||
<li>Gateway/sessions: expose effective agent runtime metadata on session rows, <code>sessions.patch</code>, and local <code>openclaw sessions --json</code>, while keeping Claude CLI-backed rows on the canonical model provider so runtime backend and model identity are no longer conflated. Fixes #73090. Thanks @vishutdhar.</li>
|
||||
<li>Gateway/auth status: scope external CLI credential overlays to configured providers, runtimes, or profiles and keep status reads off new Keychain prompts, so single-provider Gateway configs no longer probe unrelated Claude/Codex/MiniMax auth on startup. Fixes #73908. Thanks @Ailuras.</li>
|
||||
<li>Agents/runtime status: expose effective agent runtime metadata in <code>agents.list</code>, Control UI agent panels, and <code>/agents</code>, and avoid rendering stale or cumulative CLI token totals as live context usage. Fixes #73660, #73578, and #45268. Thanks @spartman, @DashLabsDev, and @xyooz.</li>
|
||||
<li>Agents/transcripts: strip empty assistant text blocks while preserving valid text, images, and signatures, so Anthropic-style providers no longer reject sanitized transcript turns. Fixes #73640. Thanks @jowhee327.</li>
|
||||
<li>Gateway/sessions: preserve session keys on hidden lifecycle events so channel-routed runs still persist terminal session state and do not strand session status as running after Codex turn completion. Thanks @cathrynlavery.</li>
|
||||
<li>Providers/Bedrock: omit deprecated <code>temperature</code> for Claude Opus 4.7 Bedrock model ids, named and application inference profiles, including dotted <code>opus-4.7</code> refs, and classify the nested validation response for failover. Fixes #73663. Thanks @bstanbury.</li>
|
||||
<li>Gateway: raise the preauth/connect-challenge timeout to 15s so cold CLI starts on slower hosts have more time to process the WebSocket challenge before the Gateway closes the connection. Fixes #51469; refs #73592 and #62060. Thanks @GothicFox and @jackychen-png.</li>
|
||||
<li>CLI/status: fall back to a bounded local <code>status</code> RPC when loopback detail probes time out or report unknown capability, so reachable local gateways are no longer marked unreachable by slow read diagnostics. Fixes #73535; refs #48360, #62762, #51357, and #42019. Thanks @RacecarGuy, @justinschille, @DJBlackhawk, @tianyaqpzm, and @0xrsydn.</li>
|
||||
<li>CLI/gateway: reuse cached paired-device auth during <code>gateway probe</code> and report post-connect diagnostic failures as degraded reachability, so healthy local gateways are no longer marked unreachable after loopback auth or read timeouts. Fixes #48360. Thanks @RacecarGuy.</li>
|
||||
<li>Channels/Discord: give Discord Gateway WebSocket handshakes a 30s timeout so stalled TLS/network transitions emit an error and Carbon can continue its reconnect loop instead of leaving the bot silent until restart. Refs #50046. Thanks @codexGW.</li>
|
||||
<li>Mattermost/WebSocket: send protocol ping/pong keepalives and terminate stale sessions when pongs stop arriving, so silent TCP drops reconnect instead of leaving monitoring idle. Fixes #41837; carries forward #57621; refs #50138, #44160, and #51104. Thanks @JasonWang1124.</li>
|
||||
<li>Channels/Telegram: suppress standalone failed edit/write warning payloads when a user-facing assistant error reply already covers the turn, while keeping unresolved mutating failures visible behind success-looking or suppressed-error replies. Fixes #39631; refs #73750; carries forward #39636 and #39717; leaves #39406 for configurable delivery policy. Thanks @Bartok9 and @Bortlesboat.</li>
|
||||
<li>Control UI/agents: persist the Set Default action through <code>agents.list[].default</code> instead of writing the unsupported <code>agents.defaultId</code> field, so saved default-agent changes survive config validation. Fixes #65565; carries forward #72585. Thanks @luyao618.</li>
|
||||
<li>NVIDIA/NIM: persist the <code>NVIDIA_API_KEY</code> provider marker and mark bundled NVIDIA Chat Completions models as string-content compatible, so NIM models load from <code>models.json</code> and OpenAI-compatible subagent calls send plain text content. Fixes #73013 and #50107; refs #73014. Thanks @bautrey, @iot2edge, @ifearghal, and @futhgar.</li>
|
||||
<li>Channels/Discord: let text-only configs drop the <code>GuildVoiceStates</code> gateway intent and expose a bounded <code>/gateway/bot</code> metadata timeout with rate-limited fallback logs, reducing idle CPU and warning floods. Fixes #73709 and #73585. Thanks @sanchezm86 and @trac3r00.</li>
|
||||
<li>Agents/sessions: mark same-turn <code>sessions_send</code> and A2A reply prompts with an inter-session <code>isUser=false</code> envelope before they reach the model, so foreign session output no longer lands as bare active user text. Fixes #73702; refs #73698, #73609, #73595, and #73622. Thanks @alvelda.</li>
|
||||
<li>Channels/Telegram: fail closed when account-level public DM settings conflict with a restrictive top-level <code>allowFrom</code>, and require an effective wildcard before <code>dmPolicy="open"</code> behaves as public access. Fixes #73756; refs #73698. Thanks @Hilo-Hilo and @xace1825.</li>
|
||||
<li>Channels/security: move open-DM allowlist semantics into the shared policy helpers and align Discord, Slack, Mattermost, Matrix, Feishu, LINE, IRC, Google Chat, Zalo, Zalo User, QQ Bot, and Synology Chat so <code>dmPolicy="open"</code> is public only with an effective wildcard and otherwise still respects sender allowlists. Refs #73756 and #73698. Thanks @Hilo-Hilo and @xace1825.</li>
|
||||
<li>ACP/tasks: sweep orphaned parent-owned ACP sessions whose task records are gone, preserving bound persistent sessions but clearing unbound stale ACPX metadata so old child sessions cannot silently respawn into chat. Fixes #73609. Thanks @joerod26.</li>
|
||||
<li>Outbound/security: strip known internal runtime scaffolding such as <code><system-reminder></code> and <code><previous_response></code> at the final channel delivery boundary and keep Discord output on targeted tag stripping, so degraded harness replies cannot leak those tags to users. Fixes #73595. Thanks @gabrielexito-stack and @martingarramon.</li>
|
||||
<li>Security/Telegram: load Telegram security adapters in read-only audit/doctor, audit malformed Telegram DM <code>allowFrom</code> entries even when groups are disabled, and keep allowlist DM audits from counting stale pairing-store senders, so public/shared-DM risk checks stay accurate. Refs #73698. Thanks @xace1825.</li>
|
||||
<li>Plugins: remove hidden manifest, provider-owner, bootstrap, and channel metadata caches so plugin installs, manifest edits, and bundled-root changes are visible on the next metadata read while keeping runtime/module loader caches for actual plugin code. Thanks @shakkernerd.</li>
|
||||
<li>CLI/plugins: use plugin metadata snapshots for install slot selection and add opt-in plugin lifecycle timing traces, so plugin install avoids runtime-loading the plugin registry for metadata-only decisions. Thanks @shakkernerd.</li>
|
||||
<li>fix(plugins): restrict bundled plugin dir resolution to trusted package roots. (#73275) Thanks @pgondhi987.</li>
|
||||
<li>fix(security): prevent workspace PATH injection via service env and trash helpers. (#73264) Thanks @pgondhi987.</li>
|
||||
<li>Active Memory: allow <code>allowedChatTypes</code> to include explicit portal/webchat sessions and classify <code>agent:...:explicit:...</code> session keys before opaque session ids can shadow the chat type. Fixes #65775. (#66285) Thanks @Lidang-Jiang.</li>
|
||||
<li>Active Memory: allow the hidden recall sub-agent to use both <code>memory_recall</code> and the legacy <code>memory_search</code>/<code>memory_get</code> memory tool contract, so bundled <code>memory-lancedb</code> recall works without breaking the default <code>memory-core</code> path. Fixes #73502. (#73584) Thanks @Takhoffman.</li>
|
||||
<li>fix(device-pairing): validate callerScopes against resolved token scopes on repair [AI]. (#72925) Thanks @pgondhi987.</li>
|
||||
<li>Active Memory docs: document the <code>cacheTtlMs</code> 1000-120000 ms range and 15000 ms default so setup snippets do not lead users past the schema limit. Fixes #65708. (#65737) Thanks @WuKongAI-CMU.</li>
|
||||
<li>fix(agents): canonicalize provider aliases in byProvider tool policy lookup [AI]. (#72917) Thanks @pgondhi987.</li>
|
||||
<li>fix(security): block npm_execpath injection from workspace .env [AI-assisted]. (#73262) Thanks @pgondhi987.</li>
|
||||
<li>Tools/web_fetch: decode response bodies from raw bytes using declared HTTP, XML, or HTML meta charsets before extraction, so Shift_JIS and other legacy-charset pages no longer return mojibake. Fixes #72916. Thanks @amknight.</li>
|
||||
<li>Active Memory: skip payload-less <code>memory_search</code> transcript tool results when building debug telemetry, so newer empty entries no longer hide the latest useful debug payload. (#68773) Thanks @SimbaKingjoe.</li>
|
||||
<li>Active Memory: keep recall setup time from consuming the configured model timeout while giving the hook runner an explicit bounded budget for the plugin, so slow embedded-run setup no longer causes immediate recall timeouts. Fixes #72606. (#72620) Thanks @hyspacex.</li>
|
||||
<li>Channels/Discord: bound message read/search REST calls, route those actions through Gateway execution, and fall back to <code>CommandTargetSessionKey</code> for inbound hook session keys so Discord reads do not hang and hooks still fire when <code>SessionKey</code> is empty. Fixes #73431. (#73521) Thanks @amknight.</li>
|
||||
<li>Plugins/media: auto-enable provider plugins referenced by <code>agents.defaults.imageGenerationModel</code>, <code>videoGenerationModel</code>, and <code>musicGenerationModel</code> primary/fallback refs, so configured Google and MiniMax media providers do not stay disabled behind a restrictive plugin allowlist. Thanks @vincentkoc.</li>
|
||||
<li>Memory-core/dreaming: retry managed dreaming cron registration after startup when the cron service is not reachable yet, so the scheduled Memory Dreaming Promotion sweep recovers without waiting for heartbeat traffic. Fixes #72841. Thanks @amknight.</li>
|
||||
<li>Acpx/runtime: validate the runtime session mode at the <code>AcpxRuntime.ensureSession</code> wrapper boundary so callers that pass anything other than <code>persistent</code> or <code>oneshot</code> get a clear <code>ACP_INVALID_RUNTIME_OPTION</code> error instead of silently round-tripping through the encoded handle as a default <code>persistent</code> mode and later throwing <code>SessionResumeRequiredError</code>. Investigation context: #73071. (#73548) Thanks @amknight.</li>
|
||||
<li>CLI/infer: keep web-search fallback on missing provider API keys, preserve structured validation errors from the selected provider, and let per-request image describe prompts override configured media-entry prompts. (#63263) Thanks @Spolen23.</li>
|
||||
<li>Chat commands: include configured model-catalog reasoning metadata when building <code>/think</code> argument menus so Ollama Cloud and other provider-owned reasoning models show supported levels instead of only <code>off</code>. Fixes #73515; supersedes #73568. Thanks @danielzinhu99 and @neeravmakwana.</li>
|
||||
<li>Channels/Telegram: suppress generic tool-progress chatter when preview streaming is off, so non-streaming Telegram turns only deliver final replies while approvals, media, and errors still route normally. Refs #72363 and #72482. Thanks @neeravmakwana and @SweetSophia.</li>
|
||||
<li>CLI/model probes: add repeatable image <code>--file</code> inputs to <code>infer model run</code> for local and gateway multimodal model smokes, so vision models such as Ollama Qwen VL and Gemini can be tested through the raw model-probe surface. Fixes #63700. Thanks @cedricjanssens.</li>
|
||||
<li>CLI/model probes: request trusted operator scope for <code>infer model run --gateway --model <provider/model></code> so Gateway raw model smokes can use one-off provider/model overrides instead of being rejected before provider auth resolution. Fixes #73759. Thanks @chrislro.</li>
|
||||
<li>CLI/image describe: pass <code>--prompt</code> and <code>--timeout-ms</code> through <code>infer image describe</code> and <code>describe-many</code>, so custom vision instructions and slow local model budgets reach media-understanding providers such as Ollama, OpenAI, Google, and OpenRouter. Refs #63700. Thanks @cedricjanssens.</li>
|
||||
<li>Model selection: include the rejected provider/model ref and allowlist recovery hint when a stored session override is cleared, so local model selections such as Gemma GGUF variants do not fall back to the default with a generic message. Refs #71069. Thanks @CyberRaccoonTeam.</li>
|
||||
<li>OpenAI-compatible providers: drop malformed event-only or blank-data SSE frames before the OpenAI SDK stream parser sees them, so proxies that split <code>event:</code> from <code>data:</code> no longer crash streaming runs with <code>Unexpected end of JSON input</code>. Fixes #52802. Thanks @LyHug.</li>
|
||||
<li>Gateway/OpenAI-compatible streaming: strip <code><final></code> tags split across streamed model deltas before they reach SSE clients, so <code>/v1/chat/completions</code> no longer emits tag remnants or drops content when final-answer wrappers cross chunk boundaries. Fixes #63325. Thanks @tzwickl.</li>
|
||||
<li>Ollama: resolve explicitly selected signed-in <code>:cloud</code> models through <code>/api/show</code> when <code>/api/tags</code> omits them, so working models such as <code>gemini-3-flash-preview:cloud</code> and <code>deepseek-v4-pro:cloud</code> do not fail dynamic model resolution before the native <code>/api/chat</code> transport runs. Fixes #73909. Thanks @chtse53.</li>
|
||||
<li>Discord/exec approvals: keep the local <code>/approve</code> prompt when no native Discord approval runtime is active, and send a manual fallback notice when native approval delivery reaches no targets, so failed DM cards no longer leave approval turns silent or dependent on model-written shell commands. Fixes #73954; carries forward #74027. Thanks @guarismo and @brokemac79.</li>
|
||||
<li>Local model prompt caching: keep stable Project Context above volatile channel/session prompt guidance and stop embedding current channel names in the message tool description, so Ollama, MLX, llama.cpp, and other prefix-cache backends avoid avoidable full prompt reprocessing across channel turns. Fixes #40256; supersedes #40296. Thanks @rhclaw and @sriram369.</li>
|
||||
<li>Gateway/OpenAI-compatible API: guard provider policy lookup against runtime providers with non-array <code>models</code> values, so <code>/v1/chat/completions</code> no longer fails with <code>provider?.models?.some is not a function</code>. Fixes #66744; carries forward #66761. Thanks @MightyMoud, @MukundaKatta.</li>
|
||||
<li>WhatsApp/Web: pass explicit Baileys socket timings into every WhatsApp Web socket and expose <code>web.whatsapp.*</code> keepalive, connect, and query timeout settings so unstable networks can avoid repeated 408 disconnect and opening-handshake timeout loops. Fixes #56365. (#73580) Thanks @velvet-shark.</li>
|
||||
<li>WhatsApp/Web: recover recently active listeners when a post-408 reconnect keeps receiving transport frames but stops delivering app messages, while keeping group metadata fallback off Baileys sends. Fixes #63855 and #66920; refs #7433, #67986, #70856, #60007, and #72621. Thanks @legonhilltech-jpg, @octopuslabs-fl, @Kanorin-chan, and @stuswan.</li>
|
||||
<li>Channels/Telegram: persist native command metadata on target sessions so topic, helper, and ACP-bound slash commands keep their session metadata attached to the routed conversation. (#57548) Thanks @GaosCode.</li>
|
||||
<li>Channels/native commands: keep validated native slash command replies visible in group chats while preserving explicit owner allowlists for command authorization. (#73672) Thanks @obviyus.</li>
|
||||
<li>Pairing/doctor: bootstrap <code>commands.ownerAllowFrom</code> from the first approved DM pairing when no command owner exists, and have doctor explain missing owners so privileged slash commands are not accidentally unusable after onboarding. Thanks @pashpashpash.</li>
|
||||
<li>Telegram/exec: infer native exec approvers from <code>commands.ownerAllowFrom</code> and auto-enable the Telegram approval client when an owner is resolvable, so owner-only commands such as <code>/diagnostics</code> can be approved in Telegram without duplicate per-channel approver config. Thanks @pashpashpash.</li>
|
||||
<li>Auto-reply/session: carry the tail of user/assistant turns into the freshly-rotated transcript on silent in-reply session resets (compaction failure, role-ordering conflict) so direct-chat continuity survives the rebind. Fixes #70853. (#70898) Thanks @neeravmakwana.</li>
|
||||
<li>Skills: load grouped skill directories such as <code>skills/<group>/<skill>/SKILL.md</code> from configured skill roots while keeping grouped discovery capped for large directories. Fixes #56915. (#72534) Thanks @ottodeng, @MoerAI, and @i010542.</li>
|
||||
<li>Config: skip malformed non-string <code>env.vars</code> entries before env-reference checks, so config loading no longer crashes on JSON values like numbers or booleans. (#42402) Thanks @MiltonHeYan.</li>
|
||||
<li>Docker Compose: default missing config and workspace bind mounts to <code>${HOME:-/tmp}/.openclaw</code> so manual compose runs do not create invalid empty-source volume specs. (#64485) Thanks @jlapenna.</li>
|
||||
<li>Agents/context engines: preserve the child agent's configured <code>agentDir</code> when subagent cleanup re-resolves a context engine, so <code>onSubagentEnded</code> hooks keep operating on the correct per-agent state. (#67243) Thanks @jarimustonen.</li>
|
||||
<li>Channels/WhatsApp: restrict pairing verification replies to real inbound user content, preventing unsolicited prompts from receipts, typing indicators, presence updates, and other non-message Baileys upserts. Fixes #73797. (#73823) Thanks @hclsys.</li>
|
||||
<li>Configure/Ollama: show the configured Ollama model allowlist after Cloud only or Cloud + Local setup and skip slow per-model cloud metadata fetches. (#73995) Thanks @obviyus.</li>
|
||||
<li>Channels/WhatsApp: detect explicit group <code>@mentions</code> again when the bot's own E.164 is in <code>allowFrom</code>, so shared-number setups no longer skip group pings that directly mention the bot. Fixes #49317. (#73453) Thanks @juan-flores077.</li>
|
||||
<li>WhatsApp/reliability: publish real transport-liveness into WhatsApp channel status and force earlier reconnects on silent transport stalls, so quiet healthy sessions stay connected while wedged sockets recover before the later remote 408 path. (#72656) Thanks @Sathvik-1007.</li>
|
||||
<li>Core/channels: tighten selected runtime, media, and plugin edge-case handling while preserving existing behavior. Thanks @jesse-merhi.</li>
|
||||
<li>Channels/WhatsApp: strip leaked plural tool-call XML wrappers on every WhatsApp-visible outbound path and keep channel error payloads out of WhatsApp chats. (#71830) Thanks @rubencu.</li>
|
||||
<li>Agents/embedded-runner: inject the resolved OAuth bearer (and forward the run abort signal) on the boundary-aware embedded stream fallback so models that route through <code>openai-codex-responses</code> and other boundary-aware transports stop failing with <code>401 Unauthorized: Missing bearer or basic authentication in header</code>. Fixes #73559. (#73588) Thanks @openperf.</li>
|
||||
<li>Telegram/gateway: bound outbound Bot API calls and cache bundled plugin alias lookup so slow Telegram sends or WSL2 filesystem scans no longer wedge gateway replies. (#74210) Thanks @obviyus.</li>
|
||||
<li>Configure/GitHub Copilot: reuse existing Copilot auth during configure and show the provider's manifest model catalog in the model picker. (#74276) Thanks @obviyus.</li>
|
||||
<li>Configure/models: keep the model picker scoped to the selected manifest provider and enable its bundled plugin before catalog lookup, so choosing GitHub Copilot no longer falls back to Ollama or skips the catalog. (#74322) Thanks @obviyus.</li>
|
||||
<li>Auto-reply/subagents: reject <code>/focus</code> from leaf subagents and scope fallback target resolution to the requesting subagent's children, so subagents cannot bind conversations outside their control boundary. (#73613) Thanks @drobison00.</li>
|
||||
<li>Gateway/startup: skip inherited workspace startup memory for sandboxed spawned sessions without real-workspace write access, so <code>/new</code> no longer preloads host workspace memory into isolated child runs. (#73611) Thanks @drobison00.</li>
|
||||
<li>Agents/tool policy: validate caller group IDs against session or spawned context before applying group-scoped tool policies or persisting gateway group metadata, so forged group IDs cannot unlock more permissive tools. (#73720) Thanks @mmaps.</li>
|
||||
<li>Commands: keep channel-prefixed owner allowlist entries scoped to matching providers so webchat command contexts cannot inherit external channel owners. Thanks @zsxsoft.</li>
|
||||
<li>Auth/device pairing: bound bootstrap handoff token issuance, redemption, and approved pairing baselines to the documented per-role scope allowlist, so bootstrap approvals cannot persistently grant <code>operator.admin</code>, <code>operator.pairing</code>, or <code>node.exec</code> scopes. Thanks @eleqtrizit.</li>
|
||||
<li>Providers/GitHub Copilot: support the GUI/RPC wizard device-code auth flow so onboarding from non-TTY clients (gateway RPC bridge, GUI wizards) completes instead of returning empty profiles. Dangerous-state handling now distinguishes <code>access_denied</code> and <code>expired_token</code> from transport errors. (#73290) Thanks @indierawk2k2.</li>
|
||||
<li>Installer/Linux: warn before switching an unwritable npm global prefix to <code>~/.npm-global</code>, then tell users to run future global updates with <code>npm i -g openclaw@latest</code> without <code>sudo</code> so npm keeps using the redirected user prefix. Fixes #44365; carries forward #50479. Thanks @Sayeem3051.</li>
|
||||
<li>Gateway/plugins: enable the native <code>require()</code> fast path on Windows for bundled plugin modules so plugin loading uses <code>require()</code> instead of Jiti's transform pipeline, reducing startup from ~39s to ~2s on typical 6-plugin setups. Fixes #68656. (#74173) Thanks @galiniliev.</li>
|
||||
<li>macOS app: detect stale Gateway TLS certificate pins, automatically repair trusted Tailscale Serve rotations, and surface paired-but-disconnected Mac companion nodes so partial Gateway connections no longer look healthy. Thanks @guti.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.29/OpenClaw-2026.4.29.zip" length="50896802" type="application/octet-stream" sparkle:edSignature="YfQ25zMGgDv8XvHbdlL/s0SMJXyu763l5ppnfjiKOjSyxZY9sfoLaoXthcctFQDXA8isR1EEb/EEausu+XkFCA=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -65,8 +65,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026051200
|
||||
versionName = "2026.5.12"
|
||||
versionCode = 2026051600
|
||||
versionName = "2026.5.16"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@@ -1612,15 +1612,6 @@ internal fun resolveOperatorSessionConnectAuth(
|
||||
)
|
||||
}
|
||||
|
||||
val explicitBootstrapToken = auth.bootstrapToken?.trim()?.takeIf { it.isNotEmpty() }
|
||||
if (explicitBootstrapToken != null) {
|
||||
return NodeRuntime.GatewayConnectAuth(
|
||||
token = null,
|
||||
bootstrapToken = explicitBootstrapToken,
|
||||
password = null,
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ai.openclaw.app.gateway
|
||||
|
||||
const val GATEWAY_PROTOCOL_VERSION = 4
|
||||
const val GATEWAY_MIN_PROTOCOL_VERSION = 3
|
||||
const val GATEWAY_MIN_PROTOCOL_VERSION = 4
|
||||
|
||||
@@ -64,6 +64,7 @@ data class GatewayConnectErrorDetails(
|
||||
val code: String?,
|
||||
val canRetryWithDeviceToken: Boolean,
|
||||
val recommendedNextStep: String?,
|
||||
val pauseReconnect: Boolean? = null,
|
||||
val reason: String? = null,
|
||||
)
|
||||
|
||||
@@ -736,6 +737,7 @@ class GatewaySession(
|
||||
code = it["code"].asStringOrNull(),
|
||||
canRetryWithDeviceToken = it["canRetryWithDeviceToken"].asBooleanOrNull() == true,
|
||||
recommendedNextStep = it["recommendedNextStep"].asStringOrNull(),
|
||||
pauseReconnect = it["pauseReconnect"].asBooleanOrNull(),
|
||||
reason = it["reason"].asStringOrNull(),
|
||||
)
|
||||
}
|
||||
@@ -1040,20 +1042,17 @@ class GatewaySession(
|
||||
detailCode == "AUTH_TOKEN_MISMATCH"
|
||||
}
|
||||
|
||||
private fun shouldPauseReconnectAfterAuthFailure(error: ErrorShape): Boolean =
|
||||
when (error.details?.code) {
|
||||
"AUTH_TOKEN_MISSING",
|
||||
"AUTH_BOOTSTRAP_TOKEN_INVALID",
|
||||
"AUTH_PASSWORD_MISSING",
|
||||
"AUTH_PASSWORD_MISMATCH",
|
||||
"AUTH_RATE_LIMITED",
|
||||
"PAIRING_REQUIRED",
|
||||
"CONTROL_UI_DEVICE_IDENTITY_REQUIRED",
|
||||
"DEVICE_IDENTITY_REQUIRED",
|
||||
-> true
|
||||
"AUTH_TOKEN_MISMATCH" -> deviceTokenRetryBudgetUsed && !pendingDeviceTokenRetry
|
||||
else -> false
|
||||
}
|
||||
private fun shouldPauseReconnectAfterAuthFailure(error: ErrorShape): Boolean {
|
||||
val target = desired
|
||||
return shouldPauseGatewayReconnectAfterAuthFailure(
|
||||
error = error,
|
||||
hasBootstrapToken = target?.bootstrapToken?.trim()?.isNotEmpty() == true,
|
||||
role = target?.options?.role,
|
||||
scopes = target?.options?.scopes ?: emptyList(),
|
||||
deviceTokenRetryBudgetUsed = deviceTokenRetryBudgetUsed,
|
||||
pendingDeviceTokenRetry = pendingDeviceTokenRetry,
|
||||
)
|
||||
}
|
||||
|
||||
private fun shouldClearStoredDeviceTokenAfterRetry(error: ErrorShape): Boolean = error.details?.code == "AUTH_DEVICE_TOKEN_MISMATCH"
|
||||
|
||||
@@ -1068,6 +1067,36 @@ class GatewaySession(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun shouldPauseGatewayReconnectAfterAuthFailure(
|
||||
error: GatewaySession.ErrorShape,
|
||||
hasBootstrapToken: Boolean,
|
||||
role: String?,
|
||||
scopes: List<String>,
|
||||
deviceTokenRetryBudgetUsed: Boolean,
|
||||
pendingDeviceTokenRetry: Boolean,
|
||||
): Boolean =
|
||||
when (error.details?.code) {
|
||||
"AUTH_TOKEN_MISSING",
|
||||
"AUTH_BOOTSTRAP_TOKEN_INVALID",
|
||||
"AUTH_PASSWORD_MISSING",
|
||||
"AUTH_PASSWORD_MISMATCH",
|
||||
"AUTH_RATE_LIMITED",
|
||||
"CONTROL_UI_DEVICE_IDENTITY_REQUIRED",
|
||||
"DEVICE_IDENTITY_REQUIRED",
|
||||
-> true
|
||||
"PAIRING_REQUIRED" ->
|
||||
!(
|
||||
hasBootstrapToken &&
|
||||
role?.trim() == "node" &&
|
||||
scopes.isEmpty() &&
|
||||
error.details.reason == "not-paired" &&
|
||||
(error.details.pauseReconnect == false ||
|
||||
error.details.recommendedNextStep == "wait_then_retry")
|
||||
)
|
||||
"AUTH_TOKEN_MISMATCH" -> deviceTokenRetryBudgetUsed && !pendingDeviceTokenRetry
|
||||
else -> false
|
||||
}
|
||||
|
||||
internal fun buildGatewayWebSocketUrl(
|
||||
host: String,
|
||||
port: Int,
|
||||
|
||||
@@ -29,14 +29,14 @@ import java.util.UUID
|
||||
@Config(sdk = [34])
|
||||
class GatewayBootstrapAuthTest {
|
||||
@Test
|
||||
fun connectsOperatorSessionWhenOnlyBootstrapAuthExists() {
|
||||
assertTrue(
|
||||
fun doesNotConnectOperatorSessionWhenOnlyBootstrapAuthExists() {
|
||||
assertFalse(
|
||||
shouldConnectOperatorSession(
|
||||
NodeRuntime.GatewayConnectAuth(token = "", bootstrapToken = "bootstrap-1", password = ""),
|
||||
storedOperatorToken = "",
|
||||
),
|
||||
)
|
||||
assertTrue(
|
||||
assertFalse(
|
||||
shouldConnectOperatorSession(
|
||||
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
|
||||
storedOperatorToken = null,
|
||||
@@ -84,17 +84,14 @@ class GatewayBootstrapAuthTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveOperatorSessionConnectAuthUsesBootstrapWhenNoStoredOperatorTokenExists() {
|
||||
fun resolveOperatorSessionConnectAuthIgnoresBootstrapWhenNoStoredOperatorTokenExists() {
|
||||
val resolved =
|
||||
resolveOperatorSessionConnectAuth(
|
||||
auth = NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
|
||||
storedOperatorToken = null,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
|
||||
resolved,
|
||||
)
|
||||
assertNull(resolved)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -174,7 +171,7 @@ class GatewayBootstrapAuthTest {
|
||||
|
||||
assertEquals("fp-1", prefs.loadGatewayTlsFingerprint(endpoint.stableId))
|
||||
assertEquals("setup-bootstrap-token", desiredBootstrapToken(runtime, "nodeSession"))
|
||||
assertEquals("setup-bootstrap-token", desiredBootstrapToken(runtime, "operatorSession"))
|
||||
assertNull(desiredBootstrapToken(runtime, "operatorSession"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package ai.openclaw.app.gateway
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class GatewaySessionReconnectTest {
|
||||
@Test
|
||||
fun bootstrapNodePairingRequiredKeepsReconnectActive() {
|
||||
val error =
|
||||
GatewaySession.ErrorShape(
|
||||
code = "NOT_PAIRED",
|
||||
message = "pairing required",
|
||||
details =
|
||||
GatewayConnectErrorDetails(
|
||||
code = "PAIRING_REQUIRED",
|
||||
canRetryWithDeviceToken = false,
|
||||
recommendedNextStep = "wait_then_retry",
|
||||
pauseReconnect = false,
|
||||
reason = "not-paired",
|
||||
),
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
shouldPauseGatewayReconnectAfterAuthFailure(
|
||||
error = error,
|
||||
hasBootstrapToken = true,
|
||||
role = "node",
|
||||
scopes = emptyList(),
|
||||
deviceTokenRetryBudgetUsed = false,
|
||||
pendingDeviceTokenRetry = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bootstrapNodePairingRequiredWithoutRetryHintPausesReconnect() {
|
||||
val error =
|
||||
GatewaySession.ErrorShape(
|
||||
code = "NOT_PAIRED",
|
||||
message = "pairing required",
|
||||
details =
|
||||
GatewayConnectErrorDetails(
|
||||
code = "PAIRING_REQUIRED",
|
||||
canRetryWithDeviceToken = false,
|
||||
recommendedNextStep = null,
|
||||
reason = "not-paired",
|
||||
),
|
||||
)
|
||||
|
||||
assertTrue(
|
||||
shouldPauseGatewayReconnectAfterAuthFailure(
|
||||
error = error,
|
||||
hasBootstrapToken = true,
|
||||
role = "node",
|
||||
scopes = emptyList(),
|
||||
deviceTokenRetryBudgetUsed = false,
|
||||
pendingDeviceTokenRetry = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonBootstrapPairingRequiredStillPausesReconnect() {
|
||||
val error =
|
||||
GatewaySession.ErrorShape(
|
||||
code = "NOT_PAIRED",
|
||||
message = "pairing required",
|
||||
details =
|
||||
GatewayConnectErrorDetails(
|
||||
code = "PAIRING_REQUIRED",
|
||||
canRetryWithDeviceToken = false,
|
||||
recommendedNextStep = "wait_then_retry",
|
||||
reason = "not-paired",
|
||||
),
|
||||
)
|
||||
|
||||
assertTrue(
|
||||
shouldPauseGatewayReconnectAfterAuthFailure(
|
||||
error = error,
|
||||
hasBootstrapToken = false,
|
||||
role = "node",
|
||||
scopes = emptyList(),
|
||||
deviceTokenRetryBudgetUsed = false,
|
||||
pendingDeviceTokenRetry = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bootstrapRoleUpgradeStillPausesReconnect() {
|
||||
val error =
|
||||
GatewaySession.ErrorShape(
|
||||
code = "NOT_PAIRED",
|
||||
message = "pairing required",
|
||||
details =
|
||||
GatewayConnectErrorDetails(
|
||||
code = "PAIRING_REQUIRED",
|
||||
canRetryWithDeviceToken = false,
|
||||
recommendedNextStep = null,
|
||||
reason = "role-upgrade",
|
||||
),
|
||||
)
|
||||
|
||||
assertTrue(
|
||||
shouldPauseGatewayReconnectAfterAuthFailure(
|
||||
error = error,
|
||||
hasBootstrapToken = true,
|
||||
role = "node",
|
||||
scopes = emptyList(),
|
||||
deviceTokenRetryBudgetUsed = false,
|
||||
pendingDeviceTokenRetry = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.5.12
|
||||
OPENCLAW_MARKETING_VERSION = 2026.5.12
|
||||
OPENCLAW_IOS_VERSION = 2026.5.16
|
||||
OPENCLAW_MARKETING_VERSION = 2026.5.16
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -4,15 +4,19 @@ import OpenClawKit
|
||||
|
||||
final class CalendarService: CalendarServicing {
|
||||
func events(params: OpenClawCalendarEventsParams) async throws -> OpenClawCalendarEventsPayload {
|
||||
let store = EKEventStore()
|
||||
let status = EKEventStore.authorizationStatus(for: .event)
|
||||
let authorized = EventKitAuthorization.allowsRead(status: status)
|
||||
let authorized: Bool = if status == .notDetermined || status == .writeOnly {
|
||||
await Self.requestFullEventAccess()
|
||||
} else {
|
||||
EventKitAuthorization.allowsRead(status: status)
|
||||
}
|
||||
guard authorized else {
|
||||
throw NSError(domain: "Calendar", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "CALENDAR_PERMISSION_REQUIRED: grant Calendar permission",
|
||||
])
|
||||
}
|
||||
|
||||
let store = EKEventStore()
|
||||
let (start, end) = Self.resolveRange(
|
||||
startISO: params.startISO,
|
||||
endISO: params.endISO)
|
||||
@@ -37,15 +41,19 @@ final class CalendarService: CalendarServicing {
|
||||
}
|
||||
|
||||
func add(params: OpenClawCalendarAddParams) async throws -> OpenClawCalendarAddPayload {
|
||||
let store = EKEventStore()
|
||||
let status = EKEventStore.authorizationStatus(for: .event)
|
||||
let authorized = EventKitAuthorization.allowsWrite(status: status)
|
||||
let authorized: Bool = if status == .notDetermined {
|
||||
await Self.requestWriteOnlyEventAccess()
|
||||
} else {
|
||||
EventKitAuthorization.allowsWrite(status: status)
|
||||
}
|
||||
guard authorized else {
|
||||
throw NSError(domain: "Calendar", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "CALENDAR_PERMISSION_REQUIRED: grant Calendar permission",
|
||||
])
|
||||
}
|
||||
|
||||
let store = EKEventStore()
|
||||
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !title.isEmpty else {
|
||||
throw NSError(domain: "Calendar", code: 3, userInfo: [
|
||||
@@ -95,6 +103,24 @@ final class CalendarService: CalendarServicing {
|
||||
return OpenClawCalendarAddPayload(event: payload)
|
||||
}
|
||||
|
||||
private static func requestFullEventAccess() async -> Bool {
|
||||
await PermissionRequestBridge.awaitRequest { completion in
|
||||
let store = EKEventStore()
|
||||
store.requestFullAccessToEvents { granted, _ in
|
||||
completion(granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func requestWriteOnlyEventAccess() async -> Bool {
|
||||
await PermissionRequestBridge.awaitRequest { completion in
|
||||
let store = EKEventStore()
|
||||
store.requestWriteOnlyAccessToEvents { granted, _ in
|
||||
completion(granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func resolveCalendar(
|
||||
store: EKEventStore,
|
||||
calendarId: String?,
|
||||
|
||||
@@ -97,14 +97,17 @@ final class ContactsService: ContactsServicing {
|
||||
return OpenClawContactsAddPayload(contact: Self.payload(from: persisted))
|
||||
}
|
||||
|
||||
private static func ensureAuthorization(store: CNContactStore, status: CNAuthorizationStatus) async -> Bool {
|
||||
private static func ensureAuthorization(status: CNAuthorizationStatus) async -> Bool {
|
||||
switch status {
|
||||
case .authorized, .limited:
|
||||
return true
|
||||
case .notDetermined:
|
||||
// Don’t prompt during node.invoke; the caller should instruct the user to grant permission.
|
||||
// Prompts block the invoke and lead to timeouts in headless flows.
|
||||
return false
|
||||
return await PermissionRequestBridge.awaitRequest { completion in
|
||||
let store = CNContactStore()
|
||||
store.requestAccess(for: .contacts) { granted, _ in
|
||||
completion(granted)
|
||||
}
|
||||
}
|
||||
case .restricted, .denied:
|
||||
return false
|
||||
@unknown default:
|
||||
@@ -113,15 +116,14 @@ final class ContactsService: ContactsServicing {
|
||||
}
|
||||
|
||||
private static func authorizedStore() async throws -> CNContactStore {
|
||||
let store = CNContactStore()
|
||||
let status = CNContactStore.authorizationStatus(for: .contacts)
|
||||
let authorized = await Self.ensureAuthorization(store: store, status: status)
|
||||
let authorized = await Self.ensureAuthorization(status: status)
|
||||
guard authorized else {
|
||||
throw NSError(domain: "Contacts", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "CONTACTS_PERMISSION_REQUIRED: grant Contacts permission",
|
||||
])
|
||||
}
|
||||
return store
|
||||
return CNContactStore()
|
||||
}
|
||||
|
||||
private static func normalizeStrings(_ values: [String]?, lowercased: Bool = false) -> [String] {
|
||||
|
||||
@@ -52,6 +52,14 @@
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>OpenClaw can capture photos or short video clips when requested via the gateway.</string>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.</string>
|
||||
<key>NSCalendarsFullAccessUsageDescription</key>
|
||||
<string>OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.</string>
|
||||
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
|
||||
<string>OpenClaw uses your calendars to add events when you enable calendar access.</string>
|
||||
<key>NSContactsUsageDescription</key>
|
||||
<string>OpenClaw uses your contacts so you can search and reference people while using the assistant.</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>OpenClaw discovers and connects to your OpenClaw gateway on the local network.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
@@ -64,6 +72,8 @@
|
||||
<string>OpenClaw may use motion data to support device-aware interactions and automations.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>OpenClaw needs photo library access when you choose existing photos to share with your assistant.</string>
|
||||
<key>NSRemindersFullAccessUsageDescription</key>
|
||||
<string>OpenClaw uses your reminders to list, add, and complete tasks when you enable reminders access.</string>
|
||||
<key>NSSpeechRecognitionUsageDescription</key>
|
||||
<string>OpenClaw uses on-device speech recognition for voice wake.</string>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import OpenClawKit
|
||||
import SwiftUI
|
||||
|
||||
struct GatewayOnboardingView: View {
|
||||
|
||||
64
apps/ios/Sources/Permissions/PermissionRequestBridge.swift
Normal file
64
apps/ios/Sources/Permissions/PermissionRequestBridge.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
import Foundation
|
||||
|
||||
enum PermissionRequestBridge {
|
||||
final class Box: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var continuation: CheckedContinuation<Bool, Never>?
|
||||
private var hasResumed = false
|
||||
|
||||
func install(_ continuation: CheckedContinuation<Bool, Never>) -> Bool {
|
||||
self.lock.lock()
|
||||
if self.hasResumed {
|
||||
self.lock.unlock()
|
||||
continuation.resume(returning: false)
|
||||
return false
|
||||
}
|
||||
self.continuation = continuation
|
||||
self.lock.unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
func resume(_ value: Bool) {
|
||||
self.lock.lock()
|
||||
guard !self.hasResumed else {
|
||||
self.lock.unlock()
|
||||
return
|
||||
}
|
||||
self.hasResumed = true
|
||||
let continuation = self.continuation
|
||||
self.continuation = nil
|
||||
self.lock.unlock()
|
||||
continuation?.resume(returning: value)
|
||||
}
|
||||
|
||||
func canStartRequest() -> Bool {
|
||||
self.lock.lock()
|
||||
let canStart = !self.hasResumed
|
||||
self.lock.unlock()
|
||||
return canStart
|
||||
}
|
||||
}
|
||||
|
||||
static func awaitRequest(
|
||||
_ start: @escaping @Sendable (@escaping @Sendable (Bool) -> Void) -> Void) async -> Bool
|
||||
{
|
||||
let box = Box()
|
||||
return await withTaskCancellationHandler {
|
||||
await withCheckedContinuation(isolation: nil) { continuation in
|
||||
guard !Task.isCancelled else {
|
||||
continuation.resume(returning: false)
|
||||
return
|
||||
}
|
||||
guard box.install(continuation) else { return }
|
||||
Task { @MainActor in
|
||||
guard box.canStartRequest() else { return }
|
||||
start { granted in
|
||||
box.resume(granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
} onCancel: {
|
||||
box.resume(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,19 @@ import OpenClawKit
|
||||
|
||||
final class RemindersService: RemindersServicing {
|
||||
func list(params: OpenClawRemindersListParams) async throws -> OpenClawRemindersListPayload {
|
||||
let store = EKEventStore()
|
||||
let status = EKEventStore.authorizationStatus(for: .reminder)
|
||||
let authorized = EventKitAuthorization.allowsRead(status: status)
|
||||
let authorized: Bool = if status == .notDetermined || status == .writeOnly {
|
||||
await Self.requestFullReminderAccess()
|
||||
} else {
|
||||
EventKitAuthorization.allowsRead(status: status)
|
||||
}
|
||||
guard authorized else {
|
||||
throw NSError(domain: "Reminders", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "REMINDERS_PERMISSION_REQUIRED: grant Reminders permission",
|
||||
])
|
||||
}
|
||||
|
||||
let store = EKEventStore()
|
||||
let limit = max(1, min(params.limit ?? 50, 500))
|
||||
let statusFilter = params.status ?? .incomplete
|
||||
|
||||
@@ -48,15 +52,19 @@ final class RemindersService: RemindersServicing {
|
||||
}
|
||||
|
||||
func add(params: OpenClawRemindersAddParams) async throws -> OpenClawRemindersAddPayload {
|
||||
let store = EKEventStore()
|
||||
let status = EKEventStore.authorizationStatus(for: .reminder)
|
||||
let authorized = EventKitAuthorization.allowsWrite(status: status)
|
||||
let authorized: Bool = if status == .notDetermined {
|
||||
await Self.requestFullReminderAccess()
|
||||
} else {
|
||||
EventKitAuthorization.allowsWrite(status: status)
|
||||
}
|
||||
guard authorized else {
|
||||
throw NSError(domain: "Reminders", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "REMINDERS_PERMISSION_REQUIRED: grant Reminders permission",
|
||||
])
|
||||
}
|
||||
|
||||
let store = EKEventStore()
|
||||
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !title.isEmpty else {
|
||||
throw NSError(domain: "Reminders", code: 3, userInfo: [
|
||||
@@ -100,6 +108,15 @@ final class RemindersService: RemindersServicing {
|
||||
return OpenClawRemindersAddPayload(reminder: payload)
|
||||
}
|
||||
|
||||
private static func requestFullReminderAccess() async -> Bool {
|
||||
await PermissionRequestBridge.awaitRequest { completion in
|
||||
let store = EKEventStore()
|
||||
store.requestFullAccessToReminders { granted, _ in
|
||||
completion(granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func resolveList(
|
||||
store: EKEventStore,
|
||||
listId: String?,
|
||||
|
||||
298
apps/ios/Sources/Settings/PrivacyAccessSectionView.swift
Normal file
298
apps/ios/Sources/Settings/PrivacyAccessSectionView.swift
Normal file
@@ -0,0 +1,298 @@
|
||||
import Contacts
|
||||
import EventKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct PrivacyAccessSectionView: View {
|
||||
@State private var contactsStatus: CNAuthorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
|
||||
@State private var calendarStatus: EKAuthorizationStatus = EKEventStore.authorizationStatus(for: .event)
|
||||
@State private var remindersStatus: EKAuthorizationStatus = EKEventStore.authorizationStatus(for: .reminder)
|
||||
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
var body: some View {
|
||||
DisclosureGroup("Privacy & Access") {
|
||||
self.permissionRow(
|
||||
title: "Contacts",
|
||||
icon: "person.crop.circle",
|
||||
status: self.statusText(for: self.contactsStatus),
|
||||
detail: "Search and add contacts from the assistant.",
|
||||
actionTitle: self.actionTitle(for: self.contactsStatus),
|
||||
action: self.handleContactsAction)
|
||||
|
||||
self.permissionRow(
|
||||
title: "Calendar (Add Events)",
|
||||
icon: "calendar.badge.plus",
|
||||
status: self.calendarWriteStatusText,
|
||||
detail: "Add events with least privilege.",
|
||||
actionTitle: self.calendarWriteActionTitle,
|
||||
action: self.handleCalendarWriteAction)
|
||||
|
||||
self.permissionRow(
|
||||
title: "Calendar (View Events)",
|
||||
icon: "calendar",
|
||||
status: self.calendarReadStatusText,
|
||||
detail: "List and read calendar events.",
|
||||
actionTitle: self.calendarReadActionTitle,
|
||||
action: self.handleCalendarReadAction)
|
||||
|
||||
self.permissionRow(
|
||||
title: "Reminders",
|
||||
icon: "checklist",
|
||||
status: self.remindersStatusText,
|
||||
detail: "List, add, and complete reminders.",
|
||||
actionTitle: self.remindersActionTitle,
|
||||
action: self.handleRemindersAction)
|
||||
}
|
||||
.onAppear { self.refreshAll() }
|
||||
.onChange(of: self.scenePhase) { _, phase in
|
||||
if phase == .active {
|
||||
self.refreshAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func permissionRow(
|
||||
title: String,
|
||||
icon: String,
|
||||
status: String,
|
||||
detail: String,
|
||||
actionTitle: String?,
|
||||
action: (() -> Void)?) -> some View
|
||||
{
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Label(title, systemImage: icon)
|
||||
Spacer()
|
||||
Text(status)
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundStyle(self.statusColor(for: status))
|
||||
}
|
||||
Text(detail)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
if let actionTitle, let action {
|
||||
Button(actionTitle, action: action)
|
||||
.font(.footnote)
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
private func statusColor(for status: String) -> Color {
|
||||
switch status {
|
||||
case "Allowed":
|
||||
.green
|
||||
case "Not Set":
|
||||
.orange
|
||||
case "Add-Only":
|
||||
.yellow
|
||||
default:
|
||||
.red
|
||||
}
|
||||
}
|
||||
|
||||
private func statusText(for cnStatus: CNAuthorizationStatus) -> String {
|
||||
switch cnStatus {
|
||||
case .authorized, .limited:
|
||||
"Allowed"
|
||||
case .notDetermined:
|
||||
"Not Set"
|
||||
case .denied, .restricted:
|
||||
"Not Allowed"
|
||||
@unknown default:
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private func actionTitle(for cnStatus: CNAuthorizationStatus) -> String? {
|
||||
switch cnStatus {
|
||||
case .notDetermined:
|
||||
"Request Access"
|
||||
case .denied, .restricted:
|
||||
"Open Settings"
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private func handleContactsAction() {
|
||||
switch self.contactsStatus {
|
||||
case .notDetermined:
|
||||
Task {
|
||||
_ = await PermissionRequestBridge.awaitRequest { completion in
|
||||
let store = CNContactStore()
|
||||
store.requestAccess(for: .contacts) { granted, _ in
|
||||
completion(granted)
|
||||
}
|
||||
}
|
||||
await MainActor.run { self.refreshAll() }
|
||||
}
|
||||
case .denied, .restricted:
|
||||
self.openSettings()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private var calendarWriteStatusText: String {
|
||||
switch self.calendarStatus {
|
||||
case .authorized, .fullAccess, .writeOnly:
|
||||
"Allowed"
|
||||
case .notDetermined:
|
||||
"Not Set"
|
||||
case .denied, .restricted:
|
||||
"Not Allowed"
|
||||
@unknown default:
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private var calendarWriteActionTitle: String? {
|
||||
switch self.calendarStatus {
|
||||
case .notDetermined:
|
||||
"Request Access"
|
||||
case .denied, .restricted:
|
||||
"Open Settings"
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCalendarWriteAction() {
|
||||
switch self.calendarStatus {
|
||||
case .notDetermined:
|
||||
Task {
|
||||
_ = await self.requestCalendarWriteOnly()
|
||||
await MainActor.run { self.refreshAll() }
|
||||
}
|
||||
case .denied, .restricted:
|
||||
self.openSettings()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private var calendarReadStatusText: String {
|
||||
switch self.calendarStatus {
|
||||
case .authorized, .fullAccess:
|
||||
"Allowed"
|
||||
case .writeOnly:
|
||||
"Add-Only"
|
||||
case .notDetermined:
|
||||
"Not Set"
|
||||
case .denied, .restricted:
|
||||
"Not Allowed"
|
||||
@unknown default:
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private var calendarReadActionTitle: String? {
|
||||
switch self.calendarStatus {
|
||||
case .notDetermined:
|
||||
"Request Full Access"
|
||||
case .writeOnly:
|
||||
"Upgrade to Full Access"
|
||||
case .denied, .restricted:
|
||||
"Open Settings"
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCalendarReadAction() {
|
||||
switch self.calendarStatus {
|
||||
case .notDetermined, .writeOnly:
|
||||
Task {
|
||||
_ = await self.requestCalendarFull()
|
||||
await MainActor.run { self.refreshAll() }
|
||||
}
|
||||
case .denied, .restricted:
|
||||
self.openSettings()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private var remindersStatusText: String {
|
||||
switch self.remindersStatus {
|
||||
case .authorized, .fullAccess:
|
||||
"Allowed"
|
||||
case .writeOnly:
|
||||
"Add-Only"
|
||||
case .notDetermined:
|
||||
"Not Set"
|
||||
case .denied, .restricted:
|
||||
"Not Allowed"
|
||||
@unknown default:
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private var remindersActionTitle: String? {
|
||||
switch self.remindersStatus {
|
||||
case .notDetermined:
|
||||
"Request Access"
|
||||
case .writeOnly:
|
||||
"Upgrade to Full Access"
|
||||
case .denied, .restricted:
|
||||
"Open Settings"
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private func handleRemindersAction() {
|
||||
switch self.remindersStatus {
|
||||
case .notDetermined, .writeOnly:
|
||||
Task {
|
||||
_ = await self.requestRemindersFull()
|
||||
await MainActor.run { self.refreshAll() }
|
||||
}
|
||||
case .denied, .restricted:
|
||||
self.openSettings()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshAll() {
|
||||
self.contactsStatus = CNContactStore.authorizationStatus(for: .contacts)
|
||||
self.calendarStatus = EKEventStore.authorizationStatus(for: .event)
|
||||
self.remindersStatus = EKEventStore.authorizationStatus(for: .reminder)
|
||||
}
|
||||
|
||||
private func requestCalendarWriteOnly() async -> Bool {
|
||||
await PermissionRequestBridge.awaitRequest { completion in
|
||||
let store = EKEventStore()
|
||||
store.requestWriteOnlyAccessToEvents { granted, _ in
|
||||
completion(granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func requestCalendarFull() async -> Bool {
|
||||
await PermissionRequestBridge.awaitRequest { completion in
|
||||
let store = EKEventStore()
|
||||
store.requestFullAccessToEvents { granted, _ in
|
||||
completion(granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func requestRemindersFull() async -> Bool {
|
||||
await PermissionRequestBridge.awaitRequest { completion in
|
||||
let store = EKEventStore()
|
||||
store.requestFullAccessToReminders { granted, _ in
|
||||
completion(granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openSettings() {
|
||||
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
@@ -405,6 +405,8 @@ struct SettingsTab: View {
|
||||
}
|
||||
}
|
||||
|
||||
AnyView(PrivacyAccessSectionView())
|
||||
|
||||
DisclosureGroup("Device Info") {
|
||||
TextField("Name", text: self.$displayName)
|
||||
Text(self.instanceId)
|
||||
@@ -419,16 +421,7 @@ struct SettingsTab: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
self.dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
}
|
||||
.accessibilityLabel("Close")
|
||||
}
|
||||
}
|
||||
.modifier(SettingsCloseToolbar())
|
||||
.sheet(isPresented: self.$showGatewayProblemDetails) {
|
||||
if let gatewayProblem = self.appModel.lastGatewayProblem {
|
||||
GatewayProblemDetailsSheet(
|
||||
@@ -488,90 +481,91 @@ struct SettingsTab: View {
|
||||
Text(self.scannerError ?? "")
|
||||
}
|
||||
.onAppear {
|
||||
self.lastLocationModeRaw = self.locationEnabledModeRaw
|
||||
self.syncManualPortText()
|
||||
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmedInstanceId.isEmpty {
|
||||
self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? ""
|
||||
self.gatewayPassword = GatewaySettingsStore.loadGatewayPassword(instanceId: trimmedInstanceId) ?? ""
|
||||
self.lastLocationModeRaw = self.locationEnabledModeRaw
|
||||
self.syncManualPortText()
|
||||
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmedInstanceId.isEmpty {
|
||||
self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? ""
|
||||
self.gatewayPassword = GatewaySettingsStore
|
||||
.loadGatewayPassword(instanceId: trimmedInstanceId) ?? ""
|
||||
}
|
||||
self.defaultShareInstruction = ShareToAgentSettings.loadDefaultInstruction()
|
||||
self.appModel.refreshLastShareEventFromRelay()
|
||||
// Keep setup front-and-center when disconnected; keep things compact once connected.
|
||||
self.gatewayExpanded = !self.isGatewayConnected
|
||||
self.selectedAgentPickerId = self.appModel.selectedAgentId ?? ""
|
||||
if self.isGatewayConnected {
|
||||
self.appModel.reloadTalkConfig()
|
||||
}
|
||||
}
|
||||
self.defaultShareInstruction = ShareToAgentSettings.loadDefaultInstruction()
|
||||
self.appModel.refreshLastShareEventFromRelay()
|
||||
// Keep setup front-and-center when disconnected; keep things compact once connected.
|
||||
self.gatewayExpanded = !self.isGatewayConnected
|
||||
self.selectedAgentPickerId = self.appModel.selectedAgentId ?? ""
|
||||
if self.isGatewayConnected {
|
||||
self.appModel.reloadTalkConfig()
|
||||
.onChange(of: self.selectedAgentPickerId) { _, newValue in
|
||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.appModel.setSelectedAgentId(trimmed.isEmpty ? nil : trimmed)
|
||||
}
|
||||
}
|
||||
.onChange(of: self.selectedAgentPickerId) { _, newValue in
|
||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.appModel.setSelectedAgentId(trimmed.isEmpty ? nil : trimmed)
|
||||
}
|
||||
.onChange(of: self.appModel.selectedAgentId ?? "") { _, newValue in
|
||||
if newValue != self.selectedAgentPickerId {
|
||||
self.selectedAgentPickerId = newValue
|
||||
.onChange(of: self.appModel.selectedAgentId ?? "") { _, newValue in
|
||||
if newValue != self.selectedAgentPickerId {
|
||||
self.selectedAgentPickerId = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: self.preferredGatewayStableID) { _, newValue in
|
||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
GatewaySettingsStore.savePreferredGatewayStableID(trimmed)
|
||||
}
|
||||
.onChange(of: self.gatewayToken) { _, newValue in
|
||||
guard !self.suppressCredentialPersist else { return }
|
||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !instanceId.isEmpty else { return }
|
||||
GatewaySettingsStore.saveGatewayToken(trimmed, instanceId: instanceId)
|
||||
}
|
||||
.onChange(of: self.gatewayPassword) { _, newValue in
|
||||
guard !self.suppressCredentialPersist else { return }
|
||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !instanceId.isEmpty else { return }
|
||||
GatewaySettingsStore.saveGatewayPassword(trimmed, instanceId: instanceId)
|
||||
}
|
||||
.onChange(of: self.defaultShareInstruction) { _, newValue in
|
||||
ShareToAgentSettings.saveDefaultInstruction(newValue)
|
||||
}
|
||||
.onChange(of: self.manualGatewayPort) { _, _ in
|
||||
self.syncManualPortText()
|
||||
}
|
||||
.onChange(of: self.appModel.gatewayServerName) { _, newValue in
|
||||
if newValue != nil {
|
||||
self.setupCode = ""
|
||||
self.setupStatusText = nil
|
||||
return
|
||||
.onChange(of: self.preferredGatewayStableID) { _, newValue in
|
||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
GatewaySettingsStore.savePreferredGatewayStableID(trimmed)
|
||||
}
|
||||
if self.manualGatewayEnabled {
|
||||
self.setupStatusText = self.appModel.gatewayStatusText
|
||||
.onChange(of: self.gatewayToken) { _, newValue in
|
||||
guard !self.suppressCredentialPersist else { return }
|
||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !instanceId.isEmpty else { return }
|
||||
GatewaySettingsStore.saveGatewayToken(trimmed, instanceId: instanceId)
|
||||
}
|
||||
}
|
||||
.onChange(of: self.appModel.gatewayStatusText) { _, newValue in
|
||||
guard self.manualGatewayEnabled || self.connectingGatewayID == "manual" else { return }
|
||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
self.setupStatusText = trimmed
|
||||
}
|
||||
.onChange(of: self.locationEnabledModeRaw) { _, newValue in
|
||||
let previous = self.lastLocationModeRaw
|
||||
self.lastLocationModeRaw = newValue
|
||||
guard let mode = OpenClawLocationMode(rawValue: newValue) else { return }
|
||||
Task {
|
||||
let granted = await self.appModel.requestLocationPermissions(mode: mode)
|
||||
if !granted {
|
||||
await MainActor.run {
|
||||
self.locationEnabledModeRaw = previous
|
||||
self.lastLocationModeRaw = previous
|
||||
}
|
||||
.onChange(of: self.gatewayPassword) { _, newValue in
|
||||
guard !self.suppressCredentialPersist else { return }
|
||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !instanceId.isEmpty else { return }
|
||||
GatewaySettingsStore.saveGatewayPassword(trimmed, instanceId: instanceId)
|
||||
}
|
||||
.onChange(of: self.defaultShareInstruction) { _, newValue in
|
||||
ShareToAgentSettings.saveDefaultInstruction(newValue)
|
||||
}
|
||||
.onChange(of: self.manualGatewayPort) { _, _ in
|
||||
self.syncManualPortText()
|
||||
}
|
||||
.onChange(of: self.appModel.gatewayServerName) { _, newValue in
|
||||
if newValue != nil {
|
||||
self.setupCode = ""
|
||||
self.setupStatusText = nil
|
||||
return
|
||||
}
|
||||
await MainActor.run {
|
||||
self.gatewayController.refreshActiveGatewayRegistrationFromSettings()
|
||||
if self.manualGatewayEnabled {
|
||||
self.setupStatusText = self.appModel.gatewayStatusText
|
||||
}
|
||||
}
|
||||
.onChange(of: self.appModel.gatewayStatusText) { _, newValue in
|
||||
guard self.manualGatewayEnabled || self.connectingGatewayID == "manual" else { return }
|
||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
self.setupStatusText = trimmed
|
||||
}
|
||||
.onChange(of: self.locationEnabledModeRaw) { _, newValue in
|
||||
let previous = self.lastLocationModeRaw
|
||||
self.lastLocationModeRaw = newValue
|
||||
guard let mode = OpenClawLocationMode(rawValue: newValue) else { return }
|
||||
Task {
|
||||
let granted = await self.appModel.requestLocationPermissions(mode: mode)
|
||||
if !granted {
|
||||
await MainActor.run {
|
||||
self.locationEnabledModeRaw = previous
|
||||
self.lastLocationModeRaw = previous
|
||||
}
|
||||
return
|
||||
}
|
||||
await MainActor.run {
|
||||
self.gatewayController.refreshActiveGatewayRegistrationFromSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.gatewayTrustPromptAlert()
|
||||
}
|
||||
@@ -1138,4 +1132,21 @@ struct SettingsTab: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct SettingsCloseToolbar: ViewModifier {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
self.dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
}
|
||||
.accessibilityLabel("Close")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable type_body_length
|
||||
|
||||
@@ -40,6 +40,7 @@ Sources/Onboarding/OnboardingStateStore.swift
|
||||
Sources/Onboarding/OnboardingWizardView.swift
|
||||
Sources/Onboarding/QRScannerView.swift
|
||||
Sources/OpenClawApp.swift
|
||||
Sources/Permissions/PermissionRequestBridge.swift
|
||||
Sources/Push/ExecApprovalNotificationBridge.swift
|
||||
Sources/Push/BackgroundAliveBeacon.swift
|
||||
Sources/Push/PushBuildConfig.swift
|
||||
@@ -60,6 +61,7 @@ Sources/Services/WatchConnectivityTransport.swift
|
||||
Sources/Services/WatchMessagingPayloadCodec.swift
|
||||
Sources/Services/WatchMessagingService.swift
|
||||
Sources/SessionKey.swift
|
||||
Sources/Settings/PrivacyAccessSectionView.swift
|
||||
Sources/Settings/SettingsNetworkingHelpers.swift
|
||||
Sources/Settings/SettingsTab.swift
|
||||
Sources/Settings/VoiceWakeWordsSettingsView.swift
|
||||
|
||||
26
apps/ios/Tests/PermissionRequestBridgeTests.swift
Normal file
26
apps/ios/Tests/PermissionRequestBridgeTests.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@Suite(.serialized) struct PermissionRequestBridgeTests {
|
||||
@Test func `box resumes immediately when cancelled before install`() async {
|
||||
let box = PermissionRequestBridge.Box()
|
||||
box.resume(false)
|
||||
let granted: Bool = await withCheckedContinuation { continuation in
|
||||
_ = box.install(continuation)
|
||||
}
|
||||
#expect(granted == false)
|
||||
#expect(box.canStartRequest() == false)
|
||||
}
|
||||
|
||||
@Test func `box resumes installed continuation once`() async {
|
||||
let box = PermissionRequestBridge.Box()
|
||||
|
||||
let granted: Bool = await withCheckedContinuation { continuation in
|
||||
_ = box.install(continuation)
|
||||
box.resume(true)
|
||||
box.resume(false)
|
||||
}
|
||||
|
||||
#expect(granted == true)
|
||||
}
|
||||
}
|
||||
@@ -136,11 +136,16 @@ targets:
|
||||
NSBonjourServices:
|
||||
- _openclaw-gw._tcp
|
||||
NSCameraUsageDescription: OpenClaw can capture photos or short video clips when requested via the gateway.
|
||||
NSCalendarsUsageDescription: OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.
|
||||
NSCalendarsFullAccessUsageDescription: OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.
|
||||
NSCalendarsWriteOnlyAccessUsageDescription: OpenClaw uses your calendars to add events when you enable calendar access.
|
||||
NSContactsUsageDescription: OpenClaw uses your contacts so you can search and reference people while using the assistant.
|
||||
NSLocationWhenInUseUsageDescription: OpenClaw uses your location when you allow location sharing.
|
||||
NSLocationAlwaysAndWhenInUseUsageDescription: OpenClaw can share your location in the background when you enable Always.
|
||||
NSMicrophoneUsageDescription: OpenClaw needs microphone access for voice wake.
|
||||
NSMotionUsageDescription: OpenClaw may use motion data to support device-aware interactions and automations.
|
||||
NSPhotoLibraryUsageDescription: OpenClaw needs photo library access when you choose existing photos to share with your assistant.
|
||||
NSRemindersFullAccessUsageDescription: OpenClaw uses your reminders to list, add, and complete tasks when you enable reminders access.
|
||||
NSSpeechRecognitionUsageDescription: OpenClaw uses on-device speech recognition for voice wake.
|
||||
NSSupportsLiveActivities: true
|
||||
ITSAppUsesNonExemptEncryption: false
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.5.12"
|
||||
"version": "2026.5.16"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,17 @@ enum GatewayRemoteConfig {
|
||||
}
|
||||
}
|
||||
|
||||
static func resolveTLSFingerprint(root: [String: Any]) -> String? {
|
||||
guard let gateway = root["gateway"] as? [String: Any],
|
||||
let remote = gateway["remote"] as? [String: Any],
|
||||
let raw = remote["tlsFingerprint"] as? String
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
|
||||
static func resolveGatewayUrl(root: [String: Any]) -> URL? {
|
||||
guard let raw = self.resolveUrlString(root: root) else { return nil }
|
||||
return self.normalizeGatewayUrl(raw)
|
||||
|
||||
@@ -83,7 +83,9 @@ final class MacNodeModeCoordinator {
|
||||
clientId: "openclaw-macos",
|
||||
clientMode: "node",
|
||||
clientDisplayName: InstanceIdentity.displayName)
|
||||
let sessionBox = self.buildSessionBox(url: config.url)
|
||||
let sessionBox = self.buildSessionBox(
|
||||
url: config.url,
|
||||
connectionMode: AppStateStore.shared.connectionMode)
|
||||
|
||||
try await self.session.connect(
|
||||
url: config.url,
|
||||
@@ -243,15 +245,35 @@ final class MacNodeModeCoordinator {
|
||||
return true
|
||||
}
|
||||
|
||||
private func buildSessionBox(url: URL) -> WebSocketSessionBox? {
|
||||
nonisolated static func tlsParams(
|
||||
for url: URL,
|
||||
connectionMode: AppState.ConnectionMode,
|
||||
root: [String: Any],
|
||||
storedFingerprint: String?) -> GatewayTLSParams?
|
||||
{
|
||||
guard url.scheme?.lowercased() == "wss" else { return nil }
|
||||
let stableID = Self.tlsPinStoreKey(for: url)
|
||||
let configuredFingerprint = connectionMode == .remote
|
||||
? GatewayRemoteConfig.resolveTLSFingerprint(root: root)
|
||||
: nil
|
||||
let expectedFingerprint = configuredFingerprint ?? storedFingerprint
|
||||
return GatewayTLSParams(
|
||||
required: true,
|
||||
expectedFingerprint: expectedFingerprint,
|
||||
allowTOFU: expectedFingerprint == nil,
|
||||
storeKey: stableID)
|
||||
}
|
||||
|
||||
private func buildSessionBox(url: URL, connectionMode: AppState.ConnectionMode) -> WebSocketSessionBox? {
|
||||
guard url.scheme?.lowercased() == "wss" else { return nil }
|
||||
let stableID = Self.tlsPinStoreKey(for: url)
|
||||
let stored = GatewayTLSStore.loadFingerprint(stableID: stableID)
|
||||
let params = GatewayTLSParams(
|
||||
required: true,
|
||||
expectedFingerprint: stored,
|
||||
allowTOFU: stored == nil,
|
||||
storeKey: stableID)
|
||||
guard let params = Self.tlsParams(
|
||||
for: url,
|
||||
connectionMode: connectionMode,
|
||||
root: OpenClawConfigFile.loadDict(),
|
||||
storedFingerprint: stored)
|
||||
else { return nil }
|
||||
let session = GatewayTLSPinningSession(params: params)
|
||||
return WebSocketSessionBox(session: session)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import OpenClawIPC
|
||||
import OpenClawKit
|
||||
|
||||
actor MacNodeRuntime {
|
||||
private static let maxGatewayPayloadBytes = 25 * 1024 * 1024
|
||||
private static let maxScreenSnapshotRawBytesBeforeBase64 = (maxGatewayPayloadBytes / 4) * 3
|
||||
private let cameraCapture = CameraCaptureService()
|
||||
private let makeMainActorServices: () async -> any MacNodeRuntimeMainActorServices
|
||||
private let browserProxyRequest: @Sendable (String?) async throws -> String
|
||||
@@ -363,15 +365,55 @@ actor MacNodeRuntime {
|
||||
}
|
||||
|
||||
private func handleScreenSnapshotInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
let params = (try? Self.decodeParams(MacNodeScreenSnapshotParams.self, from: req.paramsJSON)) ??
|
||||
MacNodeScreenSnapshotParams()
|
||||
let params: MacNodeScreenSnapshotParams
|
||||
if let paramsJSON = req.paramsJSON {
|
||||
do {
|
||||
params = try Self.decodeParams(MacNodeScreenSnapshotParams.self, from: paramsJSON)
|
||||
} catch {
|
||||
return Self.errorResponse(
|
||||
req,
|
||||
code: .invalidRequest,
|
||||
message: "INVALID_REQUEST: invalid screen snapshot params")
|
||||
}
|
||||
} else {
|
||||
params = MacNodeScreenSnapshotParams()
|
||||
}
|
||||
let services = await self.mainActorServices()
|
||||
let capturedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
let res = try await services.snapshotScreen(
|
||||
screenIndex: params.screenIndex,
|
||||
maxWidth: params.maxWidth,
|
||||
quality: params.quality,
|
||||
format: params.format)
|
||||
let res: (data: Data, format: OpenClawScreenSnapshotFormat, width: Int, height: Int)
|
||||
do {
|
||||
res = try await services.snapshotScreen(
|
||||
screenIndex: params.screenIndex,
|
||||
maxWidth: params.maxWidth,
|
||||
quality: params.quality,
|
||||
format: params.format)
|
||||
} catch let error as ScreenSnapshotService.ScreenSnapshotError {
|
||||
switch error {
|
||||
case .noDisplays:
|
||||
return Self.errorResponse(
|
||||
req,
|
||||
code: .invalidRequest,
|
||||
message: "INVALID_REQUEST: no displays available for screen snapshot")
|
||||
case let .invalidScreenIndex(idx):
|
||||
return Self.errorResponse(
|
||||
req,
|
||||
code: .invalidRequest,
|
||||
message: "INVALID_REQUEST: invalid screen index \(idx)")
|
||||
case .captureFailed, .encodeFailed:
|
||||
return Self.errorResponse(
|
||||
req,
|
||||
code: .unavailable,
|
||||
message: "UNAVAILABLE: screen snapshot failed")
|
||||
}
|
||||
} catch {
|
||||
return Self.errorResponse(
|
||||
req,
|
||||
code: .unavailable,
|
||||
message: "UNAVAILABLE: screen snapshot failed")
|
||||
}
|
||||
if res.data.count > Self.maxScreenSnapshotRawBytesBeforeBase64 {
|
||||
return Self.screenSnapshotPayloadTooLarge(req)
|
||||
}
|
||||
struct ScreenSnapshotPayload: Encodable {
|
||||
var format: String
|
||||
var base64: String
|
||||
@@ -387,6 +429,13 @@ actor MacNodeRuntime {
|
||||
height: res.height,
|
||||
screenIndex: params.screenIndex,
|
||||
capturedAtMs: capturedAtMs))
|
||||
if try Self.projectedOuterFrameBytes(
|
||||
forPayloadJSON: payload,
|
||||
requestId: req.id,
|
||||
nodeId: req.nodeId) > Self.maxGatewayPayloadBytes
|
||||
{
|
||||
return Self.screenSnapshotPayloadTooLarge(req)
|
||||
}
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||
}
|
||||
|
||||
@@ -521,7 +570,8 @@ actor MacNodeRuntime {
|
||||
let sessionKey = (params.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? params.sessionKey!.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
: self.mainSessionKey
|
||||
let runId = UUID().uuidString
|
||||
let providedRunId = params.runId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let runId = providedRunId.isEmpty ? UUID().uuidString : providedRunId
|
||||
let envOverrideDiagnostics = HostEnvSanitizer.inspectOverrides(
|
||||
overrides: params.env,
|
||||
blockPathOverrides: true)
|
||||
@@ -1003,6 +1053,40 @@ extension MacNodeRuntime {
|
||||
return json
|
||||
}
|
||||
|
||||
static func projectedOuterFrameBytes(
|
||||
forPayloadJSON payloadJSON: String,
|
||||
requestId: String,
|
||||
nodeId: String?) throws -> Int
|
||||
{
|
||||
struct InvokeResultFrame: Encodable {
|
||||
let type = "req"
|
||||
let id = "00000000-0000-0000-0000-000000000000"
|
||||
let method = "node.invoke.result"
|
||||
let params: Params
|
||||
|
||||
struct Params: Encodable {
|
||||
let id: String
|
||||
let nodeId: String
|
||||
let ok: Bool
|
||||
let payloadJSON: String
|
||||
}
|
||||
}
|
||||
|
||||
let frame = InvokeResultFrame(params: InvokeResultFrame.Params(
|
||||
id: requestId,
|
||||
nodeId: nodeId ?? "",
|
||||
ok: true,
|
||||
payloadJSON: payloadJSON))
|
||||
return try JSONEncoder().encode(frame).count
|
||||
}
|
||||
|
||||
private static func screenSnapshotPayloadTooLarge(_ req: BridgeInvokeRequest) -> BridgeInvokeResponse {
|
||||
self.errorResponse(
|
||||
req,
|
||||
code: .unavailable,
|
||||
message: "UNAVAILABLE: screen snapshot payload too large; reduce maxWidth or use jpeg")
|
||||
}
|
||||
|
||||
private nonisolated static func canvasEnabled() -> Bool {
|
||||
UserDefaults.standard.object(forKey: canvasEnabledKey) as? Bool ?? true
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.5.12</string>
|
||||
<string>2026.5.16</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026051200</string>
|
||||
<string>2026051600</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -63,7 +63,7 @@ final class ScreenSnapshotService {
|
||||
contentFilter: filter,
|
||||
configuration: config)
|
||||
} catch {
|
||||
throw ScreenSnapshotError.captureFailed(error.localizedDescription)
|
||||
throw ScreenSnapshotError.captureFailed("screen capture failed")
|
||||
}
|
||||
|
||||
let bitmap = NSBitmapImageRep(cgImage: cgImage)
|
||||
|
||||
@@ -287,4 +287,36 @@ struct GatewayEndpointStoreTests {
|
||||
let url = GatewayRemoteConfig.normalizeGatewayUrl("ws://127.attacker.example")
|
||||
#expect(url == nil)
|
||||
}
|
||||
|
||||
@Test func `resolve tls fingerprint trims remote config value`() {
|
||||
let root: [String: Any] = [
|
||||
"gateway": [
|
||||
"remote": [
|
||||
"tlsFingerprint": " sha256:ABC123 ",
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
#expect(GatewayRemoteConfig.resolveTLSFingerprint(root: root) == "sha256:ABC123")
|
||||
}
|
||||
|
||||
@Test func `resolve tls fingerprint ignores blank or non string values`() {
|
||||
let blank: [String: Any] = [
|
||||
"gateway": [
|
||||
"remote": [
|
||||
"tlsFingerprint": " ",
|
||||
],
|
||||
],
|
||||
]
|
||||
let nonString: [String: Any] = [
|
||||
"gateway": [
|
||||
"remote": [
|
||||
"tlsFingerprint": 123,
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
#expect(GatewayRemoteConfig.resolveTLSFingerprint(root: blank) == nil)
|
||||
#expect(GatewayRemoteConfig.resolveTLSFingerprint(root: nonString) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,60 @@ struct MacNodeModeCoordinatorTests {
|
||||
#expect(MacNodeModeCoordinator.tlsPinStoreKey(for: url) == "gateway.example.ts.net:443")
|
||||
}
|
||||
|
||||
@Test func `remote tls params prefer configured fingerprint over stored pin`() throws {
|
||||
let url = try #require(URL(string: "wss://gateway.example.com"))
|
||||
let root: [String: Any] = [
|
||||
"gateway": [
|
||||
"remote": [
|
||||
"tlsFingerprint": "sha256:configured",
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
let params = try #require(MacNodeModeCoordinator.tlsParams(
|
||||
for: url,
|
||||
connectionMode: .remote,
|
||||
root: root,
|
||||
storedFingerprint: "stored"))
|
||||
|
||||
#expect(params.expectedFingerprint == "sha256:configured")
|
||||
#expect(params.allowTOFU == false)
|
||||
#expect(params.storeKey == "gateway.example.com:443")
|
||||
}
|
||||
|
||||
@Test func `remote tls params allow first use only when no configured or stored pin exists`() throws {
|
||||
let url = try #require(URL(string: "wss://gateway.example.com"))
|
||||
|
||||
let params = try #require(MacNodeModeCoordinator.tlsParams(
|
||||
for: url,
|
||||
connectionMode: .remote,
|
||||
root: [:],
|
||||
storedFingerprint: nil))
|
||||
|
||||
#expect(params.expectedFingerprint == nil)
|
||||
#expect(params.allowTOFU == true)
|
||||
}
|
||||
|
||||
@Test func `local tls params ignore remote configured fingerprint`() throws {
|
||||
let url = try #require(URL(string: "wss://127.0.0.1:18789"))
|
||||
let root: [String: Any] = [
|
||||
"gateway": [
|
||||
"remote": [
|
||||
"tlsFingerprint": "sha256:remote",
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
let params = try #require(MacNodeModeCoordinator.tlsParams(
|
||||
for: url,
|
||||
connectionMode: .local,
|
||||
root: root,
|
||||
storedFingerprint: "stored-local"))
|
||||
|
||||
#expect(params.expectedFingerprint == "stored-local")
|
||||
#expect(params.allowTOFU == false)
|
||||
}
|
||||
|
||||
@Test func `auto repairs trusted tailscale serve pin mismatch`() throws {
|
||||
let url = try #require(URL(string: "wss://gateway.example.ts.net"))
|
||||
let failure = GatewayTLSValidationFailure(
|
||||
|
||||
@@ -14,6 +14,90 @@ struct MacNodeRuntimeTests {
|
||||
}
|
||||
}
|
||||
|
||||
actor ExecEventProbe {
|
||||
private var captured: [(event: String, json: String)] = []
|
||||
|
||||
func append(event: String, json: String?) {
|
||||
self.captured.append((event: event, json: json ?? ""))
|
||||
}
|
||||
|
||||
func events() -> [(event: String, json: String)] {
|
||||
self.captured
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class ScreenSnapshotProbeServices: MacNodeRuntimeMainActorServices, @unchecked Sendable {
|
||||
typealias SnapshotResult = (
|
||||
data: Data,
|
||||
format: OpenClawScreenSnapshotFormat,
|
||||
width: Int,
|
||||
height: Int)
|
||||
|
||||
var snapshotCallCount = 0
|
||||
var receivedSnapshotParams: MacNodeScreenSnapshotParams?
|
||||
var snapshotResult: SnapshotResult
|
||||
var snapshotError: Error?
|
||||
|
||||
init(
|
||||
snapshotResult: SnapshotResult = (Data("ok".utf8), .jpeg, 10, 10),
|
||||
snapshotError: Error? = nil)
|
||||
{
|
||||
self.snapshotResult = snapshotResult
|
||||
self.snapshotError = snapshotError
|
||||
}
|
||||
|
||||
func snapshotScreen(
|
||||
screenIndex: Int?,
|
||||
maxWidth: Int?,
|
||||
quality: Double?,
|
||||
format: OpenClawScreenSnapshotFormat?) async throws -> SnapshotResult
|
||||
{
|
||||
self.snapshotCallCount += 1
|
||||
self.receivedSnapshotParams = MacNodeScreenSnapshotParams(
|
||||
screenIndex: screenIndex,
|
||||
maxWidth: maxWidth,
|
||||
quality: quality,
|
||||
format: format)
|
||||
if let snapshotError {
|
||||
throw snapshotError
|
||||
}
|
||||
return self.snapshotResult
|
||||
}
|
||||
|
||||
func recordScreen(
|
||||
screenIndex: Int?,
|
||||
durationMs: Int?,
|
||||
fps: Double?,
|
||||
includeAudio: Bool?,
|
||||
outPath: String?) async throws -> (path: String, hasAudio: Bool)
|
||||
{
|
||||
let url = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-test-screen-record-\(UUID().uuidString).mp4")
|
||||
try Data("ok".utf8).write(to: url)
|
||||
return (path: url.path, hasAudio: false)
|
||||
}
|
||||
|
||||
func locationAuthorizationStatus() -> CLAuthorizationStatus {
|
||||
.authorizedAlways
|
||||
}
|
||||
|
||||
func locationAccuracyAuthorization() -> CLAccuracyAuthorization {
|
||||
.fullAccuracy
|
||||
}
|
||||
|
||||
func currentLocation(
|
||||
desiredAccuracy: OpenClawLocationAccuracy,
|
||||
maxAgeMs: Int?,
|
||||
timeoutMs: Int?) async throws -> CLLocation
|
||||
{
|
||||
_ = desiredAccuracy
|
||||
_ = maxAgeMs
|
||||
_ = timeoutMs
|
||||
return CLLocation(latitude: 0, longitude: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `handle invoke rejects unknown command`() async {
|
||||
let runtime = MacNodeRuntime()
|
||||
let response = await runtime.handleInvoke(
|
||||
@@ -45,6 +129,40 @@ struct MacNodeRuntimeTests {
|
||||
#expect(response.ok == false)
|
||||
}
|
||||
|
||||
@Test func `system run denied event preserves gateway run id`() async throws {
|
||||
let stateDir = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
|
||||
defer { try? FileManager().removeItem(at: stateDir) }
|
||||
|
||||
try await TestIsolation.withEnvValues(["OPENCLAW_STATE_DIR": stateDir.path]) {
|
||||
let probe = ExecEventProbe()
|
||||
let runtime = MacNodeRuntime()
|
||||
await runtime.setEventSender { event, json in
|
||||
await probe.append(event: event, json: json)
|
||||
}
|
||||
let params = OpenClawSystemRunParams(
|
||||
command: ["/bin/sh", "-lc", "printf ok"],
|
||||
sessionKey: "agent:main:main",
|
||||
runId: "gateway-run-1")
|
||||
let json = try String(data: JSONEncoder().encode(params), encoding: .utf8)
|
||||
let response = await runtime.handleInvoke(
|
||||
BridgeInvokeRequest(
|
||||
id: "req-run-id",
|
||||
command: OpenClawSystemCommand.run.rawValue,
|
||||
paramsJSON: json))
|
||||
|
||||
#expect(response.ok == false)
|
||||
let denied = try #require((await probe.events()).first { $0.event == "exec.denied" })
|
||||
struct Payload: Decodable {
|
||||
var sessionKey: String
|
||||
var runId: String
|
||||
}
|
||||
let payload = try JSONDecoder().decode(Payload.self, from: Data(denied.json.utf8))
|
||||
#expect(payload.sessionKey == "agent:main:main")
|
||||
#expect(payload.runId == "gateway-run-1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `handle invoke rejects blocked system run env override before execution`() async throws {
|
||||
let runtime = MacNodeRuntime()
|
||||
let params = OpenClawSystemRunParams(
|
||||
@@ -252,6 +370,199 @@ struct MacNodeRuntimeTests {
|
||||
#expect(payload.capturedAtMs <= snapshotCalledAtMs!)
|
||||
}
|
||||
|
||||
@Test func `handle invoke screen snapshot rejects malformed params before capture`() async throws {
|
||||
let services = await MainActor.run { ScreenSnapshotProbeServices() }
|
||||
let runtime = MacNodeRuntime(makeMainActorServices: { services })
|
||||
|
||||
let response = await runtime.handleInvoke(
|
||||
BridgeInvokeRequest(
|
||||
id: "req-screen-snapshot-invalid",
|
||||
command: MacNodeScreenCommand.snapshot.rawValue,
|
||||
paramsJSON: #"{"screenIndex":"#))
|
||||
|
||||
#expect(response.ok == false)
|
||||
#expect(response.error?.code == .invalidRequest)
|
||||
#expect(response.error?.message == "INVALID_REQUEST: invalid screen snapshot params")
|
||||
let snapshotCallCount = await MainActor.run { services.snapshotCallCount }
|
||||
#expect(snapshotCallCount == 0)
|
||||
}
|
||||
|
||||
@Test func `handle invoke screen snapshot keeps nil params as defaults`() async throws {
|
||||
let services = await MainActor.run { ScreenSnapshotProbeServices() }
|
||||
let runtime = MacNodeRuntime(makeMainActorServices: { services })
|
||||
|
||||
let response = await runtime.handleInvoke(
|
||||
BridgeInvokeRequest(
|
||||
id: "req-screen-snapshot-defaults",
|
||||
command: MacNodeScreenCommand.snapshot.rawValue))
|
||||
|
||||
#expect(response.ok == true)
|
||||
let received = await MainActor.run { services.receivedSnapshotParams }
|
||||
#expect(received == MacNodeScreenSnapshotParams())
|
||||
}
|
||||
|
||||
@Test func `handle invoke screen snapshot sanitizes capture failures`() async throws {
|
||||
struct SensitiveError: LocalizedError {
|
||||
let detail: String
|
||||
var errorDescription: String? { detail }
|
||||
}
|
||||
|
||||
let services = await MainActor.run {
|
||||
ScreenSnapshotProbeServices(snapshotError: SensitiveError(detail: "TCC_DENIED display-id=ABC123"))
|
||||
}
|
||||
let runtime = MacNodeRuntime(makeMainActorServices: { services })
|
||||
|
||||
let response = await runtime.handleInvoke(
|
||||
BridgeInvokeRequest(
|
||||
id: "req-screen-snapshot-error",
|
||||
command: MacNodeScreenCommand.snapshot.rawValue))
|
||||
|
||||
#expect(response.ok == false)
|
||||
#expect(response.error?.code == .unavailable)
|
||||
#expect(response.error?.message == "UNAVAILABLE: screen snapshot failed")
|
||||
}
|
||||
|
||||
@Test func `handle invoke screen snapshot reports validation failures as invalid request`() async throws {
|
||||
let invalidIndexServices = await MainActor.run {
|
||||
ScreenSnapshotProbeServices(
|
||||
snapshotError: ScreenSnapshotService.ScreenSnapshotError.invalidScreenIndex(4))
|
||||
}
|
||||
let invalidIndexRuntime = MacNodeRuntime(makeMainActorServices: { invalidIndexServices })
|
||||
let invalidIndexResponse = await invalidIndexRuntime.handleInvoke(
|
||||
BridgeInvokeRequest(
|
||||
id: "req-screen-snapshot-bad-index",
|
||||
command: MacNodeScreenCommand.snapshot.rawValue))
|
||||
|
||||
#expect(invalidIndexResponse.ok == false)
|
||||
#expect(invalidIndexResponse.error?.code == .invalidRequest)
|
||||
#expect(invalidIndexResponse.error?.message == "INVALID_REQUEST: invalid screen index 4")
|
||||
|
||||
let noDisplaysServices = await MainActor.run {
|
||||
ScreenSnapshotProbeServices(snapshotError: ScreenSnapshotService.ScreenSnapshotError.noDisplays)
|
||||
}
|
||||
let noDisplaysRuntime = MacNodeRuntime(makeMainActorServices: { noDisplaysServices })
|
||||
let noDisplaysResponse = await noDisplaysRuntime.handleInvoke(
|
||||
BridgeInvokeRequest(
|
||||
id: "req-screen-snapshot-no-displays",
|
||||
command: MacNodeScreenCommand.snapshot.rawValue))
|
||||
|
||||
#expect(noDisplaysResponse.ok == false)
|
||||
#expect(noDisplaysResponse.error?.code == .invalidRequest)
|
||||
#expect(
|
||||
noDisplaysResponse.error?.message ==
|
||||
"INVALID_REQUEST: no displays available for screen snapshot")
|
||||
}
|
||||
|
||||
@Test func `handle invoke screen snapshot rejects raw payloads above base64 ceiling`() async throws {
|
||||
let payloadSize = 19_660_801
|
||||
let services = await MainActor.run {
|
||||
ScreenSnapshotProbeServices(snapshotResult: (
|
||||
Data(repeating: 0x41, count: payloadSize),
|
||||
.jpeg,
|
||||
4000,
|
||||
3000))
|
||||
}
|
||||
let runtime = MacNodeRuntime(makeMainActorServices: { services })
|
||||
|
||||
let response = await runtime.handleInvoke(
|
||||
BridgeInvokeRequest(
|
||||
id: "req-screen-snapshot-too-large",
|
||||
command: MacNodeScreenCommand.snapshot.rawValue))
|
||||
|
||||
#expect(response.ok == false)
|
||||
#expect(response.payloadJSON == nil)
|
||||
#expect(response.error?.code == .unavailable)
|
||||
#expect(
|
||||
response.error?.message ==
|
||||
"UNAVAILABLE: screen snapshot payload too large; reduce maxWidth or use jpeg")
|
||||
}
|
||||
|
||||
@Test func `handle invoke screen snapshot rejects escaped oversized outer frames`() async throws {
|
||||
let payloadSize = 12 * 1024 * 1024
|
||||
let services = await MainActor.run {
|
||||
ScreenSnapshotProbeServices(snapshotResult: (
|
||||
Data(repeating: 0xFF, count: payloadSize),
|
||||
.png,
|
||||
4000,
|
||||
3000))
|
||||
}
|
||||
let runtime = MacNodeRuntime(makeMainActorServices: { services })
|
||||
|
||||
let response = await runtime.handleInvoke(
|
||||
BridgeInvokeRequest(
|
||||
id: "req-screen-snapshot-slash-heavy",
|
||||
command: MacNodeScreenCommand.snapshot.rawValue,
|
||||
nodeId: "node-slash-heavy"))
|
||||
|
||||
#expect(response.ok == false)
|
||||
#expect(response.error?.code == .unavailable)
|
||||
#expect(
|
||||
response.error?.message ==
|
||||
"UNAVAILABLE: screen snapshot payload too large; reduce maxWidth or use jpeg")
|
||||
}
|
||||
|
||||
@Test func `handle invoke screen snapshot accepts near-limit frames that fit`() async throws {
|
||||
let payloadSize = 19_660_100
|
||||
let services = await MainActor.run {
|
||||
ScreenSnapshotProbeServices(snapshotResult: (
|
||||
Data(repeating: 0x00, count: payloadSize),
|
||||
.jpeg,
|
||||
4000,
|
||||
3000))
|
||||
}
|
||||
let runtime = MacNodeRuntime(makeMainActorServices: { services })
|
||||
|
||||
let response = await runtime.handleInvoke(
|
||||
BridgeInvokeRequest(
|
||||
id: "req-fit",
|
||||
command: MacNodeScreenCommand.snapshot.rawValue,
|
||||
nodeId: "node-fit"))
|
||||
|
||||
#expect(response.ok == true)
|
||||
let payloadJSON = try #require(response.payloadJSON)
|
||||
let projected = try MacNodeRuntime.projectedOuterFrameBytes(
|
||||
forPayloadJSON: payloadJSON,
|
||||
requestId: "req-fit",
|
||||
nodeId: "node-fit")
|
||||
#expect(projected < 25 * 1024 * 1024)
|
||||
}
|
||||
|
||||
@Test func `projected outer frame bytes accounts for dynamic node id escaping`() throws {
|
||||
let inner = "{\"format\":\"png\",\"note\":\"\u{0001}\u{0002}\n\t\\\"raw\\\"\",\"width\":1,\"height\":1,\"capturedAtMs\":0}"
|
||||
let projected = try MacNodeRuntime.projectedOuterFrameBytes(
|
||||
forPayloadJSON: inner,
|
||||
requestId: "req-control",
|
||||
nodeId: "node-\u{0001}\u{0002}\u{0003}\n\t-id")
|
||||
|
||||
struct Frame: Encodable {
|
||||
let type = "req"
|
||||
let id = "00000000-0000-0000-0000-000000000000"
|
||||
let method = "node.invoke.result"
|
||||
let params: Params
|
||||
|
||||
struct Params: Encodable {
|
||||
let id: String
|
||||
let nodeId: String
|
||||
let ok: Bool
|
||||
let payloadJSON: String
|
||||
}
|
||||
}
|
||||
let serialized = try JSONEncoder().encode(Frame(params: Frame.Params(
|
||||
id: "req-control",
|
||||
nodeId: "node-\u{0001}\u{0002}\u{0003}\n\t-id",
|
||||
ok: true,
|
||||
payloadJSON: inner)))
|
||||
|
||||
#expect(projected == serialized.count)
|
||||
|
||||
let controlHeavyNodeId = String(repeating: "\u{0001}", count: 5 * 1024 * 1024)
|
||||
let controlHeavyProjection = try MacNodeRuntime.projectedOuterFrameBytes(
|
||||
forPayloadJSON: "{}",
|
||||
requestId: "req-control",
|
||||
nodeId: controlHeavyNodeId)
|
||||
#expect(controlHeavyProjection > 25 * 1024 * 1024)
|
||||
}
|
||||
|
||||
@Test func `handle invoke browser proxy uses injected request`() async {
|
||||
let runtime = MacNodeRuntime(browserProxyRequest: { paramsJSON in
|
||||
#expect(paramsJSON?.contains("/tabs") == true)
|
||||
|
||||
@@ -253,7 +253,11 @@ private struct ChatMessageBody: View {
|
||||
guard kind == "text" || kind.isEmpty else { return nil }
|
||||
return content.text
|
||||
}
|
||||
return parts.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return OpenClawChatMessage.displayText(
|
||||
contentText: parts.joined(separator: "\n"),
|
||||
role: self.message.role,
|
||||
stopReason: self.message.stopReason,
|
||||
errorMessage: self.message.errorMessage)
|
||||
}
|
||||
|
||||
private var inlineAttachments: [OpenClawChatMessageContent] {
|
||||
|
||||
@@ -144,6 +144,7 @@ public struct OpenClawChatMessage: Codable, Identifiable, Sendable {
|
||||
public let toolName: String?
|
||||
public let usage: OpenClawChatUsage?
|
||||
public let stopReason: String?
|
||||
public let errorMessage: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case role
|
||||
@@ -155,6 +156,7 @@ public struct OpenClawChatMessage: Codable, Identifiable, Sendable {
|
||||
case tool_name
|
||||
case usage
|
||||
case stopReason
|
||||
case errorMessage
|
||||
}
|
||||
|
||||
public init(
|
||||
@@ -165,7 +167,8 @@ public struct OpenClawChatMessage: Codable, Identifiable, Sendable {
|
||||
toolCallId: String? = nil,
|
||||
toolName: String? = nil,
|
||||
usage: OpenClawChatUsage? = nil,
|
||||
stopReason: String? = nil)
|
||||
stopReason: String? = nil,
|
||||
errorMessage: String? = nil)
|
||||
{
|
||||
self.id = id
|
||||
self.role = role
|
||||
@@ -175,20 +178,30 @@ public struct OpenClawChatMessage: Codable, Identifiable, Sendable {
|
||||
self.toolName = toolName
|
||||
self.usage = usage
|
||||
self.stopReason = stopReason
|
||||
self.errorMessage = errorMessage
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.role = try container.decode(String.self, forKey: .role)
|
||||
self.timestamp = try container.decodeIfPresent(Double.self, forKey: .timestamp)
|
||||
self.toolCallId =
|
||||
let decodedRole = try container.decode(String.self, forKey: .role)
|
||||
let decodedTimestamp = try container.decodeIfPresent(Double.self, forKey: .timestamp)
|
||||
let decodedToolCallId =
|
||||
try container.decodeIfPresent(String.self, forKey: .toolCallId) ??
|
||||
container.decodeIfPresent(String.self, forKey: .tool_call_id)
|
||||
self.toolName =
|
||||
let decodedToolName =
|
||||
try container.decodeIfPresent(String.self, forKey: .toolName) ??
|
||||
container.decodeIfPresent(String.self, forKey: .tool_name)
|
||||
self.usage = try container.decodeIfPresent(OpenClawChatUsage.self, forKey: .usage)
|
||||
self.stopReason = try container.decodeIfPresent(String.self, forKey: .stopReason)
|
||||
let decodedUsage = try container.decodeIfPresent(OpenClawChatUsage.self, forKey: .usage)
|
||||
let decodedStopReason = try container.decodeIfPresent(String.self, forKey: .stopReason)
|
||||
let decodedErrorMessage = try container.decodeIfPresent(String.self, forKey: .errorMessage)
|
||||
|
||||
self.role = decodedRole
|
||||
self.timestamp = decodedTimestamp
|
||||
self.toolCallId = decodedToolCallId
|
||||
self.toolName = decodedToolName
|
||||
self.usage = decodedUsage
|
||||
self.stopReason = decodedStopReason
|
||||
self.errorMessage = decodedErrorMessage
|
||||
|
||||
if let decoded = try? container.decode([OpenClawChatMessageContent].self, forKey: .content) {
|
||||
self.content = decoded
|
||||
@@ -216,6 +229,41 @@ public struct OpenClawChatMessage: Codable, Identifiable, Sendable {
|
||||
self.content = []
|
||||
}
|
||||
|
||||
static func displayText(
|
||||
contentText: String,
|
||||
role: String,
|
||||
stopReason: String?,
|
||||
errorMessage: String?) -> String
|
||||
{
|
||||
let text = contentText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let errorText = Self.errorDisplayText(
|
||||
role: role,
|
||||
stopReason: stopReason,
|
||||
errorMessage: errorMessage)
|
||||
else {
|
||||
return text
|
||||
}
|
||||
if text.isEmpty || text == Self.streamErrorFallbackText {
|
||||
return errorText
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
static func errorDisplayText(role: String, stopReason: String?, errorMessage: String?) -> String? {
|
||||
let normalizedRole = role.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
let normalizedStopReason = stopReason?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
guard normalizedRole == "assistant",
|
||||
normalizedStopReason == "error",
|
||||
let text = errorMessage?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!text.isEmpty
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
private static let streamErrorFallbackText = "[assistant turn failed before producing content]"
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.role, forKey: .role)
|
||||
@@ -224,6 +272,7 @@ public struct OpenClawChatMessage: Codable, Identifiable, Sendable {
|
||||
try container.encodeIfPresent(self.toolName, forKey: .toolName)
|
||||
try container.encodeIfPresent(self.usage, forKey: .usage)
|
||||
try container.encodeIfPresent(self.stopReason, forKey: .stopReason)
|
||||
try container.encodeIfPresent(self.errorMessage, forKey: .errorMessage)
|
||||
try container.encode(self.content, forKey: .content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,7 +389,8 @@ public struct OpenClawChatView: View {
|
||||
toolCallId: last.toolCallId,
|
||||
toolName: last.toolName,
|
||||
usage: last.usage,
|
||||
stopReason: last.stopReason)
|
||||
stopReason: last.stopReason,
|
||||
errorMessage: last.errorMessage)
|
||||
result[result.count - 1] = merged
|
||||
}
|
||||
|
||||
@@ -433,7 +434,11 @@ public struct OpenClawChatView: View {
|
||||
guard kind == "text" || kind.isEmpty else { return nil }
|
||||
return content.text
|
||||
}
|
||||
return parts.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return OpenClawChatMessage.displayText(
|
||||
contentText: parts.joined(separator: "\n"),
|
||||
role: message.role,
|
||||
stopReason: message.stopReason,
|
||||
errorMessage: message.errorMessage)
|
||||
}
|
||||
|
||||
private func hasInlineAttachments(in message: OpenClawChatMessage) -> Bool {
|
||||
|
||||
@@ -17,6 +17,7 @@ private let chatUILogger = Logger(subsystem: "ai.openclaw", category: "OpenClawC
|
||||
// swiftlint:disable:next type_body_length
|
||||
public final class OpenClawChatViewModel {
|
||||
public static let defaultModelSelectionID = "__default__"
|
||||
private static let maxAttachmentBytes = 5_000_000
|
||||
|
||||
public private(set) var messages: [OpenClawChatMessage] = []
|
||||
public var input: String = ""
|
||||
@@ -304,7 +305,8 @@ public final class OpenClawChatViewModel {
|
||||
toolCallId: message.toolCallId,
|
||||
toolName: message.toolName,
|
||||
usage: message.usage,
|
||||
stopReason: message.stopReason)
|
||||
stopReason: message.stopReason,
|
||||
errorMessage: message.errorMessage)
|
||||
}
|
||||
|
||||
private static func messageContentFingerprint(for message: OpenClawChatMessage) -> String {
|
||||
@@ -383,7 +385,8 @@ public final class OpenClawChatViewModel {
|
||||
toolCallId: message.toolCallId,
|
||||
toolName: message.toolName,
|
||||
usage: message.usage,
|
||||
stopReason: message.stopReason)
|
||||
stopReason: message.stopReason,
|
||||
errorMessage: message.errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1298,11 +1301,6 @@ public final class OpenClawChatViewModel {
|
||||
}
|
||||
|
||||
private func addImageAttachment(url: URL?, data: Data, fileName: String, mimeType: String) async {
|
||||
if data.count > 5_000_000 {
|
||||
self.errorText = "Attachment \(fileName) exceeds 5 MB limit"
|
||||
return
|
||||
}
|
||||
|
||||
let uti: UTType = {
|
||||
if let url {
|
||||
return UTType(filenameExtension: url.pathExtension) ?? .data
|
||||
@@ -1314,13 +1312,33 @@ public final class OpenClawChatViewModel {
|
||||
return
|
||||
}
|
||||
|
||||
let preview = Self.previewImage(data: data)
|
||||
let processed: Data
|
||||
do {
|
||||
processed = try await Task.detached(priority: .userInitiated) {
|
||||
try ChatImageProcessor.processForUpload(data: data)
|
||||
}.value
|
||||
} catch {
|
||||
self.errorText = "Could not process \(fileName): \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
if processed.count > Self.maxAttachmentBytes {
|
||||
self.errorText = "Attachment \(fileName) exceeds 5 MB limit after resizing"
|
||||
return
|
||||
}
|
||||
|
||||
let outputFileName: String = {
|
||||
let baseName = (fileName as NSString).deletingPathExtension
|
||||
return baseName.isEmpty ? "image.jpg" : "\(baseName).jpg"
|
||||
}()
|
||||
|
||||
let preview = Self.previewImage(data: processed)
|
||||
self.attachments.append(
|
||||
OpenClawPendingAttachment(
|
||||
url: url,
|
||||
data: data,
|
||||
fileName: fileName,
|
||||
mimeType: mimeType,
|
||||
data: processed,
|
||||
fileName: outputFileName,
|
||||
mimeType: "image/jpeg",
|
||||
preview: preview))
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,20 @@ public struct BridgeInvokeRequest: Codable, Sendable {
|
||||
public let id: String
|
||||
public let command: String
|
||||
public let paramsJSON: String?
|
||||
public let nodeId: String?
|
||||
|
||||
public init(type: String = "invoke", id: String, command: String, paramsJSON: String? = nil) {
|
||||
public init(
|
||||
type: String = "invoke",
|
||||
id: String,
|
||||
command: String,
|
||||
paramsJSON: String? = nil,
|
||||
nodeId: String? = nil)
|
||||
{
|
||||
self.type = type
|
||||
self.id = id
|
||||
self.command = command
|
||||
self.paramsJSON = paramsJSON
|
||||
self.nodeId = nodeId
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import Foundation
|
||||
|
||||
/// Chat-specific image upload policy built on the shared JPEG transcoder.
|
||||
public enum ChatImageProcessor {
|
||||
public static let maxLongEdgePx = 1600
|
||||
public static let jpegQuality = 0.8
|
||||
public static let maxPayloadBytes = 3_500_000
|
||||
|
||||
public enum ProcessError: Error, LocalizedError, Sendable {
|
||||
case notAnImage
|
||||
case decodeFailed
|
||||
case encodeFailed
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .notAnImage:
|
||||
"The data is not a recognizable image."
|
||||
case .decodeFailed:
|
||||
"The image could not be decoded."
|
||||
case .encodeFailed:
|
||||
"The image could not be resized to fit the chat upload limit."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func processForUpload(data: Data) throws -> Data {
|
||||
do {
|
||||
let result = try JPEGTranscoder.transcodeToJPEG(
|
||||
imageData: data,
|
||||
maxLongEdgePx: self.maxLongEdgePx,
|
||||
quality: self.jpegQuality,
|
||||
maxBytes: self.maxPayloadBytes)
|
||||
return result.data
|
||||
} catch JPEGTranscodeError.decodeFailed {
|
||||
throw ProcessError.notAnImage
|
||||
} catch JPEGTranscodeError.propertiesMissing {
|
||||
throw ProcessError.decodeFailed
|
||||
} catch JPEGTranscodeError.sizeLimitExceeded {
|
||||
throw ProcessError.encodeFailed
|
||||
} catch {
|
||||
throw ProcessError.encodeFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
|
||||
{
|
||||
return link
|
||||
}
|
||||
return fromGatewayURLString(
|
||||
return self.fromGatewayURLString(
|
||||
trimmed,
|
||||
bootstrapToken: nil,
|
||||
token: nil,
|
||||
@@ -89,7 +89,7 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
|
||||
{
|
||||
return link
|
||||
}
|
||||
for candidate in setupCodeCandidates(in: trimmed) where candidate != trimmed {
|
||||
for candidate in self.setupCodeCandidates(in: trimmed) where candidate != trimmed {
|
||||
if let data = decodeBase64Url(candidate),
|
||||
let link = decodeSetupPayload(from: data)
|
||||
{
|
||||
@@ -104,7 +104,7 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
|
||||
if let urlString = payload.url?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!urlString.isEmpty
|
||||
{
|
||||
return fromGatewayURLString(
|
||||
return self.fromGatewayURLString(
|
||||
urlString,
|
||||
bootstrapToken: payload.bootstrapToken,
|
||||
token: payload.token,
|
||||
|
||||
@@ -457,7 +457,8 @@ public actor GatewayNodeSession {
|
||||
let req = BridgeInvokeRequest(
|
||||
id: request.id,
|
||||
command: request.command,
|
||||
paramsJSON: request.paramsJSON)
|
||||
paramsJSON: request.paramsJSON,
|
||||
nodeId: request.nodeId)
|
||||
self.logger.info("node invoke executing id=\(request.id, privacy: .public)")
|
||||
let response = await Self.invokeWithTimeout(
|
||||
request: req,
|
||||
|
||||
@@ -79,6 +79,12 @@ public protocol GatewayDeviceTokenRetryTrustProviding: AnyObject {
|
||||
var allowsDeviceTokenRetryAuth: Bool { get }
|
||||
}
|
||||
|
||||
enum GatewayTLSFirstUsePolicy {
|
||||
static func allowsFirstUsePin(systemTrustOk: Bool) -> Bool {
|
||||
systemTrustOk
|
||||
}
|
||||
}
|
||||
|
||||
public enum GatewayTLSStore {
|
||||
private static let keychainService = "ai.openclaw.tls-pinning"
|
||||
|
||||
@@ -159,7 +165,8 @@ public enum GatewayTLSStore {
|
||||
}
|
||||
}
|
||||
|
||||
public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLSessionDelegate, GatewayTLSFailureProviding, GatewayDeviceTokenRetryTrustProviding, @unchecked Sendable {
|
||||
public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLSessionDelegate,
|
||||
GatewayTLSFailureProviding, GatewayDeviceTokenRetryTrustProviding, @unchecked Sendable {
|
||||
private let params: GatewayTLSParams
|
||||
private let failureLock = NSLock()
|
||||
private var lastTLSFailure: GatewayTLSValidationFailure?
|
||||
@@ -238,12 +245,14 @@ public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLS
|
||||
return
|
||||
}
|
||||
if self.params.allowTOFU {
|
||||
if let storeKey = params.storeKey {
|
||||
GatewayTLSStore.saveFingerprint(fingerprint, stableID: storeKey)
|
||||
if GatewayTLSFirstUsePolicy.allowsFirstUsePin(systemTrustOk: systemTrustOk) {
|
||||
if let storeKey = params.storeKey {
|
||||
GatewayTLSStore.saveFingerprint(fingerprint, stableID: storeKey)
|
||||
}
|
||||
self.clearTLSFailure()
|
||||
completionHandler(.useCredential, URLCredential(trust: trust))
|
||||
return
|
||||
}
|
||||
self.clearTLSFailure()
|
||||
completionHandler(.useCredential, URLCredential(trust: trust))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,26 @@ public struct JPEGTranscoder: Sendable {
|
||||
maxWidthPx: Int?,
|
||||
quality: Double,
|
||||
maxBytes: Int? = nil) throws -> (data: Data, widthPx: Int, heightPx: Int)
|
||||
{
|
||||
try self.transcodeToJPEG(
|
||||
imageData: imageData,
|
||||
maxWidthPx: maxWidthPx,
|
||||
maxLongEdgePx: nil,
|
||||
quality: quality,
|
||||
maxBytes: maxBytes)
|
||||
}
|
||||
|
||||
/// Re-encodes image data to JPEG, optionally downscaling so the *oriented* longest edge is <= `maxLongEdgePx`.
|
||||
///
|
||||
/// When `maxLongEdgePx` is provided it takes precedence over `maxWidthPx`.
|
||||
/// - Important: This normalizes EXIF orientation (the output pixels are rotated if needed; orientation tag is not
|
||||
/// relied on).
|
||||
public static func transcodeToJPEG(
|
||||
imageData: Data,
|
||||
maxWidthPx: Int? = nil,
|
||||
maxLongEdgePx: Int?,
|
||||
quality: Double,
|
||||
maxBytes: Int? = nil) throws -> (data: Data, widthPx: Int, heightPx: Int)
|
||||
{
|
||||
guard let src = CGImageSourceCreateWithData(imageData as CFData, nil) else {
|
||||
throw JPEGTranscodeError.decodeFailed
|
||||
@@ -63,6 +83,10 @@ public struct JPEGTranscoder: Sendable {
|
||||
|
||||
let maxDim = max(orientedWidth, orientedHeight)
|
||||
var targetMaxPixelSize: Int = {
|
||||
if let maxLongEdgePx, maxLongEdgePx > 0 {
|
||||
guard maxDim > maxLongEdgePx else { return maxDim } // never upscale
|
||||
return maxLongEdgePx
|
||||
}
|
||||
guard let maxWidthPx, maxWidthPx > 0 else { return maxDim }
|
||||
guard orientedWidth > maxWidthPx else { return maxDim } // never upscale
|
||||
|
||||
@@ -81,6 +105,7 @@ public struct JPEGTranscoder: Sendable {
|
||||
guard let img = CGImageSourceCreateThumbnailAtIndex(src, 0, thumbOpts as CFDictionary) else {
|
||||
throw JPEGTranscodeError.decodeFailed
|
||||
}
|
||||
let opaqueImage = Self.flattenAlphaIfNeeded(img)
|
||||
|
||||
let out = NSMutableData()
|
||||
guard let dest = CGImageDestinationCreateWithData(out, UTType.jpeg.identifier as CFString, 1, nil) else {
|
||||
@@ -88,12 +113,12 @@ public struct JPEGTranscoder: Sendable {
|
||||
}
|
||||
let q = self.clampQuality(quality)
|
||||
let encodeProps = [kCGImageDestinationLossyCompressionQuality: q] as CFDictionary
|
||||
CGImageDestinationAddImage(dest, img, encodeProps)
|
||||
CGImageDestinationAddImage(dest, opaqueImage, encodeProps)
|
||||
guard CGImageDestinationFinalize(dest) else {
|
||||
throw JPEGTranscodeError.encodeFailed
|
||||
}
|
||||
|
||||
return (out as Data, img.width, img.height)
|
||||
return (out as Data, opaqueImage.width, opaqueImage.height)
|
||||
}
|
||||
|
||||
guard let maxBytes, maxBytes > 0 else {
|
||||
@@ -132,4 +157,34 @@ public struct JPEGTranscoder: Sendable {
|
||||
|
||||
return best
|
||||
}
|
||||
|
||||
/// JPEG cannot store alpha. Flatten transparent sources over white before encoding so ImageIO does not composite
|
||||
/// transparent pixels onto black by default.
|
||||
private static func flattenAlphaIfNeeded(_ image: CGImage) -> CGImage {
|
||||
switch image.alphaInfo {
|
||||
case .none, .noneSkipFirst, .noneSkipLast:
|
||||
return image
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
guard
|
||||
let context = CGContext(
|
||||
data: nil,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: 0,
|
||||
space: CGColorSpaceCreateDeviceRGB(),
|
||||
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
|
||||
else {
|
||||
return image
|
||||
}
|
||||
|
||||
let rect = CGRect(x: 0, y: 0, width: image.width, height: image.height)
|
||||
context.setFillColor(CGColor(red: 1, green: 1, blue: 1, alpha: 1))
|
||||
context.fill(rect)
|
||||
context.draw(image, in: rect)
|
||||
return context.makeImage() ?? image
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public struct OpenClawSystemRunParams: Codable, Sendable, Equatable {
|
||||
public var needsScreenRecording: Bool?
|
||||
public var agentId: String?
|
||||
public var sessionKey: String?
|
||||
public var runId: String?
|
||||
public var approved: Bool?
|
||||
public var approvalDecision: String?
|
||||
|
||||
@@ -41,6 +42,7 @@ public struct OpenClawSystemRunParams: Codable, Sendable, Equatable {
|
||||
needsScreenRecording: Bool? = nil,
|
||||
agentId: String? = nil,
|
||||
sessionKey: String? = nil,
|
||||
runId: String? = nil,
|
||||
approved: Bool? = nil,
|
||||
approvalDecision: String? = nil)
|
||||
{
|
||||
@@ -52,6 +54,7 @@ public struct OpenClawSystemRunParams: Codable, Sendable, Equatable {
|
||||
self.needsScreenRecording = needsScreenRecording
|
||||
self.agentId = agentId
|
||||
self.sessionKey = sessionKey
|
||||
self.runId = runId
|
||||
self.approved = approved
|
||||
self.approvalDecision = approvalDecision
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Foundation
|
||||
|
||||
public let GATEWAY_PROTOCOL_VERSION = 4
|
||||
public let GATEWAY_MIN_PROTOCOL_VERSION = 3
|
||||
public let GATEWAY_MIN_PROTOCOL_VERSION = 4
|
||||
|
||||
private struct GatewayAnyCodingKey: CodingKey, Hashable {
|
||||
let stringValue: String
|
||||
@@ -541,6 +541,7 @@ public struct MessageActionParams: Codable, Sendable {
|
||||
public let senderisowner: Bool?
|
||||
public let sessionkey: String?
|
||||
public let sessionid: String?
|
||||
public let inboundturnkind: String?
|
||||
public let agentid: String?
|
||||
public let toolcontext: [String: AnyCodable]?
|
||||
public let idempotencykey: String
|
||||
@@ -554,6 +555,7 @@ public struct MessageActionParams: Codable, Sendable {
|
||||
senderisowner: Bool?,
|
||||
sessionkey: String?,
|
||||
sessionid: String?,
|
||||
inboundturnkind: String? = nil,
|
||||
agentid: String?,
|
||||
toolcontext: [String: AnyCodable]?,
|
||||
idempotencykey: String)
|
||||
@@ -566,6 +568,7 @@ public struct MessageActionParams: Codable, Sendable {
|
||||
self.senderisowner = senderisowner
|
||||
self.sessionkey = sessionkey
|
||||
self.sessionid = sessionid
|
||||
self.inboundturnkind = inboundturnkind
|
||||
self.agentid = agentid
|
||||
self.toolcontext = toolcontext
|
||||
self.idempotencykey = idempotencykey
|
||||
@@ -580,6 +583,7 @@ public struct MessageActionParams: Codable, Sendable {
|
||||
case senderisowner = "senderIsOwner"
|
||||
case sessionkey = "sessionKey"
|
||||
case sessionid = "sessionId"
|
||||
case inboundturnkind = "inboundTurnKind"
|
||||
case agentid = "agentId"
|
||||
case toolcontext = "toolContext"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
@@ -751,6 +755,7 @@ public struct AgentParams: Codable, Sendable {
|
||||
public let internalruntimehandoffid: String?
|
||||
public let internalevents: [[String: AnyCodable]]?
|
||||
public let inputprovenance: [String: AnyCodable]?
|
||||
public let sourcereplydeliverymode: AnyCodable?
|
||||
public let voicewaketrigger: String?
|
||||
public let idempotencykey: String
|
||||
public let label: String?
|
||||
@@ -788,6 +793,7 @@ public struct AgentParams: Codable, Sendable {
|
||||
internalruntimehandoffid: String?,
|
||||
internalevents: [[String: AnyCodable]]?,
|
||||
inputprovenance: [String: AnyCodable]?,
|
||||
sourcereplydeliverymode: AnyCodable?,
|
||||
voicewaketrigger: String?,
|
||||
idempotencykey: String,
|
||||
label: String?)
|
||||
@@ -824,6 +830,7 @@ public struct AgentParams: Codable, Sendable {
|
||||
self.internalruntimehandoffid = internalruntimehandoffid
|
||||
self.internalevents = internalevents
|
||||
self.inputprovenance = inputprovenance
|
||||
self.sourcereplydeliverymode = sourcereplydeliverymode
|
||||
self.voicewaketrigger = voicewaketrigger
|
||||
self.idempotencykey = idempotencykey
|
||||
self.label = label
|
||||
@@ -862,6 +869,7 @@ public struct AgentParams: Codable, Sendable {
|
||||
case internalruntimehandoffid = "internalRuntimeHandoffId"
|
||||
case internalevents = "internalEvents"
|
||||
case inputprovenance = "inputProvenance"
|
||||
case sourcereplydeliverymode = "sourceReplyDeliveryMode"
|
||||
case voicewaketrigger = "voiceWakeTrigger"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
case label
|
||||
@@ -2098,6 +2106,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
public let spawndepth: AnyCodable?
|
||||
public let subagentrole: AnyCodable?
|
||||
public let subagentcontrolscope: AnyCodable?
|
||||
public let inheritedtoolallow: AnyCodable?
|
||||
public let inheritedtooldeny: AnyCodable?
|
||||
public let sendpolicy: AnyCodable?
|
||||
public let groupactivation: AnyCodable?
|
||||
|
||||
@@ -2121,6 +2131,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
spawndepth: AnyCodable?,
|
||||
subagentrole: AnyCodable?,
|
||||
subagentcontrolscope: AnyCodable?,
|
||||
inheritedtoolallow: AnyCodable?,
|
||||
inheritedtooldeny: AnyCodable?,
|
||||
sendpolicy: AnyCodable?,
|
||||
groupactivation: AnyCodable?)
|
||||
{
|
||||
@@ -2143,6 +2155,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
self.spawndepth = spawndepth
|
||||
self.subagentrole = subagentrole
|
||||
self.subagentcontrolscope = subagentcontrolscope
|
||||
self.inheritedtoolallow = inheritedtoolallow
|
||||
self.inheritedtooldeny = inheritedtooldeny
|
||||
self.sendpolicy = sendpolicy
|
||||
self.groupactivation = groupactivation
|
||||
}
|
||||
@@ -2167,6 +2181,8 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
case spawndepth = "spawnDepth"
|
||||
case subagentrole = "subagentRole"
|
||||
case subagentcontrolscope = "subagentControlScope"
|
||||
case inheritedtoolallow = "inheritedToolAllow"
|
||||
case inheritedtooldeny = "inheritedToolDeny"
|
||||
case sendpolicy = "sendPolicy"
|
||||
case groupactivation = "groupActivation"
|
||||
}
|
||||
@@ -3220,6 +3236,7 @@ public struct TalkSessionCancelTurnParams: Codable, Sendable {
|
||||
|
||||
public struct TalkSessionCreateParams: Codable, Sendable {
|
||||
public let sessionkey: String?
|
||||
public let spawnedby: String?
|
||||
public let provider: String?
|
||||
public let model: String?
|
||||
public let voice: String?
|
||||
@@ -3234,6 +3251,7 @@ public struct TalkSessionCreateParams: Codable, Sendable {
|
||||
|
||||
public init(
|
||||
sessionkey: String?,
|
||||
spawnedby: String?,
|
||||
provider: String?,
|
||||
model: String?,
|
||||
voice: String?,
|
||||
@@ -3247,6 +3265,7 @@ public struct TalkSessionCreateParams: Codable, Sendable {
|
||||
ttlms: Int?)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.spawnedby = spawnedby
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.voice = voice
|
||||
@@ -3262,6 +3281,7 @@ public struct TalkSessionCreateParams: Codable, Sendable {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case spawnedby = "spawnedBy"
|
||||
case provider
|
||||
case model
|
||||
case voice
|
||||
@@ -5212,6 +5232,7 @@ public struct CronRunsParams: Codable, Sendable {
|
||||
public let scope: AnyCodable?
|
||||
public let id: String?
|
||||
public let jobid: String?
|
||||
public let runid: String?
|
||||
public let limit: Int?
|
||||
public let offset: Int?
|
||||
public let statuses: [AnyCodable]?
|
||||
@@ -5225,6 +5246,7 @@ public struct CronRunsParams: Codable, Sendable {
|
||||
scope: AnyCodable?,
|
||||
id: String?,
|
||||
jobid: String?,
|
||||
runid: String?,
|
||||
limit: Int?,
|
||||
offset: Int?,
|
||||
statuses: [AnyCodable]?,
|
||||
@@ -5237,6 +5259,7 @@ public struct CronRunsParams: Codable, Sendable {
|
||||
self.scope = scope
|
||||
self.id = id
|
||||
self.jobid = jobid
|
||||
self.runid = runid
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
self.statuses = statuses
|
||||
@@ -5251,6 +5274,7 @@ public struct CronRunsParams: Codable, Sendable {
|
||||
case scope
|
||||
case id
|
||||
case jobid = "jobId"
|
||||
case runid = "runId"
|
||||
case limit
|
||||
case offset
|
||||
case statuses
|
||||
@@ -5273,6 +5297,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
public let delivered: Bool?
|
||||
public let deliverystatus: AnyCodable?
|
||||
public let deliveryerror: String?
|
||||
public let failurenotificationdelivery: [String: AnyCodable]?
|
||||
public let sessionid: String?
|
||||
public let sessionkey: String?
|
||||
public let runid: String?
|
||||
@@ -5295,6 +5320,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
delivered: Bool?,
|
||||
deliverystatus: AnyCodable?,
|
||||
deliveryerror: String?,
|
||||
failurenotificationdelivery: [String: AnyCodable]? = nil,
|
||||
sessionid: String?,
|
||||
sessionkey: String?,
|
||||
runid: String?,
|
||||
@@ -5316,6 +5342,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
self.delivered = delivered
|
||||
self.deliverystatus = deliverystatus
|
||||
self.deliveryerror = deliveryerror
|
||||
self.failurenotificationdelivery = failurenotificationdelivery
|
||||
self.sessionid = sessionid
|
||||
self.sessionkey = sessionkey
|
||||
self.runid = runid
|
||||
@@ -5339,6 +5366,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
case delivered
|
||||
case deliverystatus = "deliveryStatus"
|
||||
case deliveryerror = "deliveryError"
|
||||
case failurenotificationdelivery = "failureNotificationDelivery"
|
||||
case sessionid = "sessionId"
|
||||
case sessionkey = "sessionKey"
|
||||
case runid = "runId"
|
||||
@@ -6232,12 +6260,138 @@ public struct ChatInjectParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatEvent: Codable, Sendable {
|
||||
public struct ChatDeltaEvent: Codable, Sendable {
|
||||
public let runid: String
|
||||
public let sessionkey: String
|
||||
public let spawnedby: String?
|
||||
public let seq: Int
|
||||
public let state: AnyCodable
|
||||
public let state: String
|
||||
public let message: AnyCodable?
|
||||
public let deltatext: String
|
||||
public let replace: Bool?
|
||||
public let usage: AnyCodable?
|
||||
|
||||
public init(
|
||||
runid: String,
|
||||
sessionkey: String,
|
||||
spawnedby: String?,
|
||||
seq: Int,
|
||||
state: String,
|
||||
message: AnyCodable?,
|
||||
deltatext: String,
|
||||
replace: Bool?,
|
||||
usage: AnyCodable?)
|
||||
{
|
||||
self.runid = runid
|
||||
self.sessionkey = sessionkey
|
||||
self.spawnedby = spawnedby
|
||||
self.seq = seq
|
||||
self.state = state
|
||||
self.message = message
|
||||
self.deltatext = deltatext
|
||||
self.replace = replace
|
||||
self.usage = usage
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case runid = "runId"
|
||||
case sessionkey = "sessionKey"
|
||||
case spawnedby = "spawnedBy"
|
||||
case seq
|
||||
case state
|
||||
case message
|
||||
case deltatext = "deltaText"
|
||||
case replace
|
||||
case usage
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatFinalEvent: Codable, Sendable {
|
||||
public let runid: String
|
||||
public let sessionkey: String
|
||||
public let spawnedby: String?
|
||||
public let seq: Int
|
||||
public let state: String
|
||||
public let message: AnyCodable?
|
||||
public let usage: AnyCodable?
|
||||
public let stopreason: String?
|
||||
|
||||
public init(
|
||||
runid: String,
|
||||
sessionkey: String,
|
||||
spawnedby: String?,
|
||||
seq: Int,
|
||||
state: String,
|
||||
message: AnyCodable?,
|
||||
usage: AnyCodable?,
|
||||
stopreason: String?)
|
||||
{
|
||||
self.runid = runid
|
||||
self.sessionkey = sessionkey
|
||||
self.spawnedby = spawnedby
|
||||
self.seq = seq
|
||||
self.state = state
|
||||
self.message = message
|
||||
self.usage = usage
|
||||
self.stopreason = stopreason
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case runid = "runId"
|
||||
case sessionkey = "sessionKey"
|
||||
case spawnedby = "spawnedBy"
|
||||
case seq
|
||||
case state
|
||||
case message
|
||||
case usage
|
||||
case stopreason = "stopReason"
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatAbortedEvent: Codable, Sendable {
|
||||
public let runid: String
|
||||
public let sessionkey: String
|
||||
public let spawnedby: String?
|
||||
public let seq: Int
|
||||
public let state: String
|
||||
public let message: AnyCodable?
|
||||
public let stopreason: String?
|
||||
|
||||
public init(
|
||||
runid: String,
|
||||
sessionkey: String,
|
||||
spawnedby: String?,
|
||||
seq: Int,
|
||||
state: String,
|
||||
message: AnyCodable?,
|
||||
stopreason: String?)
|
||||
{
|
||||
self.runid = runid
|
||||
self.sessionkey = sessionkey
|
||||
self.spawnedby = spawnedby
|
||||
self.seq = seq
|
||||
self.state = state
|
||||
self.message = message
|
||||
self.stopreason = stopreason
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case runid = "runId"
|
||||
case sessionkey = "sessionKey"
|
||||
case spawnedby = "spawnedBy"
|
||||
case seq
|
||||
case state
|
||||
case message
|
||||
case stopreason = "stopReason"
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatErrorEvent: Codable, Sendable {
|
||||
public let runid: String
|
||||
public let sessionkey: String
|
||||
public let spawnedby: String?
|
||||
public let seq: Int
|
||||
public let state: String
|
||||
public let message: AnyCodable?
|
||||
public let errormessage: String?
|
||||
public let errorkind: AnyCodable?
|
||||
@@ -6249,7 +6403,7 @@ public struct ChatEvent: Codable, Sendable {
|
||||
sessionkey: String,
|
||||
spawnedby: String?,
|
||||
seq: Int,
|
||||
state: AnyCodable,
|
||||
state: String,
|
||||
message: AnyCodable?,
|
||||
errormessage: String?,
|
||||
errorkind: AnyCodable?,
|
||||
@@ -6381,6 +6535,43 @@ public enum PluginsSessionActionResult: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatEvent: Codable, Sendable {
|
||||
case delta(ChatDeltaEvent)
|
||||
case final(ChatFinalEvent)
|
||||
case aborted(ChatAbortedEvent)
|
||||
case error(ChatErrorEvent)
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case discriminator = "state"
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let discriminator = try container.decode(String.self, forKey: .discriminator)
|
||||
switch discriminator {
|
||||
case "delta": self = try .delta(ChatDeltaEvent(from: decoder))
|
||||
case "final": self = try .final(ChatFinalEvent(from: decoder))
|
||||
case "aborted": self = try .aborted(ChatAbortedEvent(from: decoder))
|
||||
case "error": self = try .error(ChatErrorEvent(from: decoder))
|
||||
default:
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .discriminator,
|
||||
in: container,
|
||||
debugDescription: "Unknown ChatEvent discriminator value"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
switch self {
|
||||
case .delta(let value): try value.encode(to: encoder)
|
||||
case .final(let value): try value.encode(to: encoder)
|
||||
case .aborted(let value): try value.encode(to: encoder)
|
||||
case .error(let value): try value.encode(to: encoder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum GatewayFrame: Codable, Sendable {
|
||||
case req(RequestFrame)
|
||||
case res(ResponseFrame)
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import Testing
|
||||
import UniformTypeIdentifiers
|
||||
@testable import OpenClawKit
|
||||
|
||||
struct ChatImageProcessorTests {
|
||||
private func syntheticJPEG(width: Int, height: Int) throws -> Data {
|
||||
guard
|
||||
let context = CGContext(
|
||||
data: nil,
|
||||
width: width,
|
||||
height: height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: width * 4,
|
||||
space: CGColorSpaceCreateDeviceRGB(),
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
|
||||
else {
|
||||
throw NSError(domain: "ChatImageProcessorTests", code: 1)
|
||||
}
|
||||
|
||||
context.setFillColor(CGColor(red: 0.8, green: 0.2, blue: 0.4, alpha: 1))
|
||||
context.fill(CGRect(x: 0, y: 0, width: width, height: height))
|
||||
context.setFillColor(CGColor(red: 0.1, green: 0.7, blue: 0.3, alpha: 1))
|
||||
context.fill(CGRect(x: 0, y: 0, width: width / 2, height: height / 2))
|
||||
|
||||
guard let image = context.makeImage() else {
|
||||
throw NSError(domain: "ChatImageProcessorTests", code: 2)
|
||||
}
|
||||
|
||||
let data = NSMutableData()
|
||||
guard let destination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil)
|
||||
else {
|
||||
throw NSError(domain: "ChatImageProcessorTests", code: 3)
|
||||
}
|
||||
|
||||
let properties: [CFString: Any] = [
|
||||
kCGImageDestinationLossyCompressionQuality: 0.95,
|
||||
kCGImagePropertyExifDictionary: [
|
||||
kCGImagePropertyExifDateTimeOriginal: "2026:04:20 16:30:00",
|
||||
kCGImagePropertyExifLensModel: "Leaky Lens 50mm f/1.4",
|
||||
] as CFDictionary,
|
||||
kCGImagePropertyGPSDictionary: [
|
||||
kCGImagePropertyGPSLatitude: 60.02,
|
||||
kCGImagePropertyGPSLatitudeRef: "N",
|
||||
kCGImagePropertyGPSLongitude: 10.95,
|
||||
kCGImagePropertyGPSLongitudeRef: "E",
|
||||
] as CFDictionary,
|
||||
kCGImagePropertyTIFFDictionary: [
|
||||
kCGImagePropertyTIFFMake: "LeakCorp",
|
||||
kCGImagePropertyTIFFModel: "Privacy-Leaker-1",
|
||||
] as CFDictionary,
|
||||
]
|
||||
CGImageDestinationAddImage(destination, image, properties as CFDictionary)
|
||||
guard CGImageDestinationFinalize(destination) else {
|
||||
throw NSError(domain: "ChatImageProcessorTests", code: 4)
|
||||
}
|
||||
return data as Data
|
||||
}
|
||||
|
||||
private func syntheticPNGWithAlpha(width: Int, height: Int) throws -> Data {
|
||||
guard
|
||||
let context = CGContext(
|
||||
data: nil,
|
||||
width: width,
|
||||
height: height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: width * 4,
|
||||
space: CGColorSpaceCreateDeviceRGB(),
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
|
||||
else {
|
||||
throw NSError(domain: "ChatImageProcessorTests", code: 5)
|
||||
}
|
||||
|
||||
context.clear(CGRect(x: 0, y: 0, width: width, height: height))
|
||||
context.setFillColor(CGColor(red: 1, green: 0, blue: 0, alpha: 1))
|
||||
context.fill(CGRect(x: width / 4, y: height / 4, width: width / 2, height: height / 2))
|
||||
|
||||
guard let image = context.makeImage() else {
|
||||
throw NSError(domain: "ChatImageProcessorTests", code: 6)
|
||||
}
|
||||
|
||||
let data = NSMutableData()
|
||||
guard let destination = CGImageDestinationCreateWithData(data, UTType.png.identifier as CFString, 1, nil)
|
||||
else {
|
||||
throw NSError(domain: "ChatImageProcessorTests", code: 7)
|
||||
}
|
||||
CGImageDestinationAddImage(destination, image, nil)
|
||||
guard CGImageDestinationFinalize(destination) else {
|
||||
throw NSError(domain: "ChatImageProcessorTests", code: 8)
|
||||
}
|
||||
return data as Data
|
||||
}
|
||||
|
||||
private func properties(for data: Data) -> [CFString: Any] {
|
||||
guard
|
||||
let source = CGImageSourceCreateWithData(data as CFData, nil),
|
||||
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any]
|
||||
else {
|
||||
return [:]
|
||||
}
|
||||
return properties
|
||||
}
|
||||
|
||||
private func dimensions(for data: Data) -> (width: Int, height: Int)? {
|
||||
let properties = self.properties(for: data)
|
||||
guard
|
||||
let width = properties[kCGImagePropertyPixelWidth] as? NSNumber,
|
||||
let height = properties[kCGImagePropertyPixelHeight] as? NSNumber
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return (width.intValue, height.intValue)
|
||||
}
|
||||
|
||||
@Test func `resizes landscape long edge to upload limit`() throws {
|
||||
let source = try self.syntheticJPEG(width: 4000, height: 3000)
|
||||
let output = try ChatImageProcessor.processForUpload(data: source)
|
||||
let dimensions = try #require(self.dimensions(for: output))
|
||||
|
||||
#expect(max(dimensions.width, dimensions.height) <= ChatImageProcessor.maxLongEdgePx)
|
||||
#expect(abs((Double(dimensions.width) / Double(dimensions.height)) - (4000.0 / 3000.0)) <= 0.02)
|
||||
}
|
||||
|
||||
@Test func `resizes portrait long edge to upload limit`() throws {
|
||||
let source = try self.syntheticJPEG(width: 3000, height: 4000)
|
||||
let output = try ChatImageProcessor.processForUpload(data: source)
|
||||
let dimensions = try #require(self.dimensions(for: output))
|
||||
|
||||
#expect(max(dimensions.width, dimensions.height) <= ChatImageProcessor.maxLongEdgePx)
|
||||
#expect(abs((Double(dimensions.width) / Double(dimensions.height)) - (3000.0 / 4000.0)) <= 0.02)
|
||||
}
|
||||
|
||||
@Test func `resizes narrow tall long edge to upload limit`() throws {
|
||||
let source = try self.syntheticJPEG(width: 1080, height: 2400)
|
||||
let output = try ChatImageProcessor.processForUpload(data: source)
|
||||
let dimensions = try #require(self.dimensions(for: output))
|
||||
|
||||
#expect(max(dimensions.width, dimensions.height) <= ChatImageProcessor.maxLongEdgePx)
|
||||
#expect(abs((Double(dimensions.width) / Double(dimensions.height)) - (1080.0 / 2400.0)) <= 0.02)
|
||||
}
|
||||
|
||||
@Test func `small image is not upscaled`() throws {
|
||||
let source = try self.syntheticJPEG(width: 400, height: 300)
|
||||
let output = try ChatImageProcessor.processForUpload(data: source)
|
||||
let dimensions = try #require(self.dimensions(for: output))
|
||||
|
||||
#expect(max(dimensions.width, dimensions.height) <= 400)
|
||||
}
|
||||
|
||||
@Test func `output fits payload budget`() throws {
|
||||
let source = try self.syntheticJPEG(width: 4000, height: 3000)
|
||||
let output = try ChatImageProcessor.processForUpload(data: source)
|
||||
|
||||
#expect(output.count <= ChatImageProcessor.maxPayloadBytes)
|
||||
}
|
||||
|
||||
@Test func `rejects non image data`() {
|
||||
let garbage = Data("not an image".utf8)
|
||||
|
||||
#expect(throws: ChatImageProcessor.ProcessError.self) {
|
||||
_ = try ChatImageProcessor.processForUpload(data: garbage)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `strips source metadata from output`() throws {
|
||||
let source = try self.syntheticJPEG(width: 3000, height: 2000)
|
||||
let output = try ChatImageProcessor.processForUpload(data: source)
|
||||
let properties = self.properties(for: output)
|
||||
let gps = properties[kCGImagePropertyGPSDictionary] as? [CFString: Any] ?? [:]
|
||||
|
||||
#expect(gps.isEmpty)
|
||||
for needle in ["Leaky Lens", "LeakCorp", "Privacy-Leaker", "2026:04:20"] {
|
||||
#expect(output.range(of: Data(needle.utf8)) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `flattens transparent sources to opaque JPEG`() throws {
|
||||
let source = try self.syntheticPNGWithAlpha(width: 800, height: 600)
|
||||
let output = try ChatImageProcessor.processForUpload(data: source)
|
||||
let imageSource = try #require(CGImageSourceCreateWithData(output as CFData, nil))
|
||||
let image = try #require(CGImageSourceCreateImageAtIndex(imageSource, 0, nil))
|
||||
|
||||
#expect([.none, .noneSkipFirst, .noneSkipLast].contains(image.alphaInfo))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import OpenClawKit
|
||||
import UniformTypeIdentifiers
|
||||
import XCTest
|
||||
@testable import OpenClawChatUI
|
||||
|
||||
private struct AttachmentProcessingTransport: OpenClawChatTransport {
|
||||
func requestHistory(sessionKey _: String) async throws -> OpenClawChatHistoryPayload {
|
||||
throw NSError(domain: "ChatViewModelAttachmentTests", code: 1)
|
||||
}
|
||||
|
||||
func sendMessage(
|
||||
sessionKey _: String,
|
||||
message _: String,
|
||||
thinking _: String,
|
||||
idempotencyKey _: String,
|
||||
attachments _: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse
|
||||
{
|
||||
throw NSError(domain: "ChatViewModelAttachmentTests", code: 2)
|
||||
}
|
||||
|
||||
func requestHealth(timeoutMs _: Int) async throws -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func events() -> AsyncStream<OpenClawChatTransportEvent> {
|
||||
AsyncStream { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
private func makeChatAttachmentJPEG(width: Int, height: Int) throws -> Data {
|
||||
guard
|
||||
let context = CGContext(
|
||||
data: nil,
|
||||
width: width,
|
||||
height: height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: width * 4,
|
||||
space: CGColorSpaceCreateDeviceRGB(),
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
|
||||
else {
|
||||
throw NSError(domain: "ChatViewModelAttachmentTests", code: 3)
|
||||
}
|
||||
|
||||
context.setFillColor(CGColor(red: 0.2, green: 0.4, blue: 0.8, alpha: 1))
|
||||
context.fill(CGRect(x: 0, y: 0, width: width, height: height))
|
||||
context.setFillColor(CGColor(red: 0.9, green: 0.5, blue: 0.1, alpha: 1))
|
||||
context.fill(CGRect(x: 0, y: 0, width: width / 2, height: height / 2))
|
||||
|
||||
guard let image = context.makeImage() else {
|
||||
throw NSError(domain: "ChatViewModelAttachmentTests", code: 4)
|
||||
}
|
||||
|
||||
let data = NSMutableData()
|
||||
guard let destination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil) else {
|
||||
throw NSError(domain: "ChatViewModelAttachmentTests", code: 5)
|
||||
}
|
||||
CGImageDestinationAddImage(destination, image, [kCGImageDestinationLossyCompressionQuality: 0.95] as CFDictionary)
|
||||
guard CGImageDestinationFinalize(destination) else {
|
||||
throw NSError(domain: "ChatViewModelAttachmentTests", code: 6)
|
||||
}
|
||||
return data as Data
|
||||
}
|
||||
|
||||
private func chatAttachmentDimensions(for data: Data) -> (width: Int, height: Int)? {
|
||||
guard
|
||||
let source = CGImageSourceCreateWithData(data as CFData, nil),
|
||||
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any],
|
||||
let width = properties[kCGImagePropertyPixelWidth] as? NSNumber,
|
||||
let height = properties[kCGImagePropertyPixelHeight] as? NSNumber
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return (width.intValue, height.intValue)
|
||||
}
|
||||
|
||||
final class ChatViewModelAttachmentTests: XCTestCase {
|
||||
func testImageAttachmentsAreProcessedBeforeStaging() async throws {
|
||||
let imageData = try makeChatAttachmentJPEG(width: 3000, height: 4000)
|
||||
let viewModel = await MainActor.run {
|
||||
OpenClawChatViewModel(sessionKey: "main", transport: AttachmentProcessingTransport())
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
viewModel.addImageAttachment(data: imageData, fileName: "camera.heic", mimeType: "image/jpeg")
|
||||
}
|
||||
|
||||
try await waitUntil("attachment processed") {
|
||||
await MainActor.run { !viewModel.attachments.isEmpty || viewModel.errorText != nil }
|
||||
}
|
||||
|
||||
let attachment = try await MainActor.run {
|
||||
guard let attachment = viewModel.attachments.first else {
|
||||
throw NSError(domain: "ChatViewModelAttachmentTests", code: 7)
|
||||
}
|
||||
return (attachment.fileName, attachment.mimeType, attachment.data)
|
||||
}
|
||||
let dimensions = try XCTUnwrap(chatAttachmentDimensions(for: attachment.2))
|
||||
|
||||
XCTAssertEqual(attachment.0, "camera.jpg")
|
||||
XCTAssertEqual(attachment.1, "image/jpeg")
|
||||
XCTAssertLessThanOrEqual(attachment.2.count, ChatImageProcessor.maxPayloadBytes)
|
||||
XCTAssertLessThanOrEqual(max(dimensions.width, dimensions.height), ChatImageProcessor.maxLongEdgePx)
|
||||
let errorText = await MainActor.run { viewModel.errorText }
|
||||
XCTAssertNil(errorText)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,16 @@ private func chatTextMessage(role: String, text: String, timestamp: Double) -> A
|
||||
])
|
||||
}
|
||||
|
||||
private func chatErrorMessage(role: String, errorMessage: String, timestamp: Double) -> AnyCodable {
|
||||
AnyCodable([
|
||||
"role": role,
|
||||
"content": [],
|
||||
"timestamp": timestamp,
|
||||
"stopReason": "error",
|
||||
"errorMessage": errorMessage,
|
||||
])
|
||||
}
|
||||
|
||||
private func historyPayload(
|
||||
sessionKey: String = "main",
|
||||
sessionId: String? = "sess-main",
|
||||
@@ -454,6 +464,76 @@ extension TestChatTransportState {
|
||||
}
|
||||
|
||||
@Suite struct ChatViewModelTests {
|
||||
@Test func displaysErrorMessageFallbackOnlyForAssistantErrorTurns() throws {
|
||||
func decodeMessage(role: String, stopReason: String, contentText: String? = nil) throws -> OpenClawChatMessage {
|
||||
let contentJSON = contentText.map { #"[{"type":"text","text":"\#($0)"}]"# } ?? "[]"
|
||||
let data = """
|
||||
{
|
||||
"role": "\(role)",
|
||||
"content": \(contentJSON),
|
||||
"timestamp": 1,
|
||||
"stopReason": "\(stopReason)",
|
||||
"errorMessage": "stale provider failure"
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
return try JSONDecoder().decode(OpenClawChatMessage.self, from: data)
|
||||
}
|
||||
|
||||
let assistantError = try decodeMessage(role: "assistant", stopReason: "error")
|
||||
#expect(assistantError.content.isEmpty)
|
||||
#expect(
|
||||
OpenClawChatMessage.errorDisplayText(
|
||||
role: assistantError.role,
|
||||
stopReason: assistantError.stopReason,
|
||||
errorMessage: assistantError.errorMessage) == "stale provider failure")
|
||||
#expect(
|
||||
OpenClawChatMessage.displayText(
|
||||
contentText: "",
|
||||
role: assistantError.role,
|
||||
stopReason: assistantError.stopReason,
|
||||
errorMessage: assistantError.errorMessage) == "stale provider failure")
|
||||
|
||||
let sentinelAssistant = try decodeMessage(
|
||||
role: "assistant",
|
||||
stopReason: "error",
|
||||
contentText: "[assistant turn failed before producing content]")
|
||||
#expect(
|
||||
OpenClawChatMessage.displayText(
|
||||
contentText: sentinelAssistant.content.compactMap(\.text).joined(separator: "\n"),
|
||||
role: sentinelAssistant.role,
|
||||
stopReason: sentinelAssistant.stopReason,
|
||||
errorMessage: sentinelAssistant.errorMessage) == "stale provider failure")
|
||||
|
||||
let partialAssistant = try decodeMessage(
|
||||
role: "assistant",
|
||||
stopReason: "error",
|
||||
contentText: "partial answer")
|
||||
#expect(
|
||||
OpenClawChatMessage.displayText(
|
||||
contentText: partialAssistant.content.compactMap(\.text).joined(separator: "\n"),
|
||||
role: partialAssistant.role,
|
||||
stopReason: partialAssistant.stopReason,
|
||||
errorMessage: partialAssistant.errorMessage) == "partial answer")
|
||||
|
||||
let stoppedAssistant = try decodeMessage(role: "assistant", stopReason: "stop")
|
||||
#expect(stoppedAssistant.errorMessage == "stale provider failure")
|
||||
#expect(stoppedAssistant.content.isEmpty)
|
||||
#expect(
|
||||
OpenClawChatMessage.errorDisplayText(
|
||||
role: stoppedAssistant.role,
|
||||
stopReason: stoppedAssistant.stopReason,
|
||||
errorMessage: stoppedAssistant.errorMessage) == nil)
|
||||
|
||||
let toolUseAssistant = try decodeMessage(role: "assistant", stopReason: "toolUse")
|
||||
#expect(toolUseAssistant.errorMessage == "stale provider failure")
|
||||
#expect(toolUseAssistant.content.isEmpty)
|
||||
#expect(
|
||||
OpenClawChatMessage.errorDisplayText(
|
||||
role: toolUseAssistant.role,
|
||||
stopReason: toolUseAssistant.stopReason,
|
||||
errorMessage: toolUseAssistant.errorMessage) == nil)
|
||||
}
|
||||
|
||||
@Test func streamsAssistantAndClearsOnFinal() async throws {
|
||||
let sessionId = "sess-main"
|
||||
let history1 = historyPayload(sessionId: sessionId)
|
||||
@@ -665,6 +745,51 @@ extension TestChatTransportState {
|
||||
}
|
||||
}
|
||||
|
||||
@Test func surfacesAssistantErrorMessageAfterOwnRunRefresh() async throws {
|
||||
let now = Date().timeIntervalSince1970 * 1000
|
||||
let history1 = historyPayload()
|
||||
let history2 = historyPayload(
|
||||
messages: [
|
||||
chatErrorMessage(
|
||||
role: "assistant",
|
||||
errorMessage: "You have hit your ChatGPT usage limit (plus plan). Try again in ~28 min.",
|
||||
timestamp: now),
|
||||
])
|
||||
|
||||
let (transport, vm) = await makeViewModel(historyResponses: [history1, history2])
|
||||
try await loadAndWaitBootstrap(vm: vm)
|
||||
|
||||
await sendUserMessage(vm)
|
||||
try await waitUntil("pending run starts") { await MainActor.run { vm.pendingRunCount == 1 } }
|
||||
|
||||
let runId = try #require(await transport.lastSentRunId())
|
||||
transport.emit(
|
||||
.chat(
|
||||
OpenClawChatEventPayload(
|
||||
runId: runId,
|
||||
sessionKey: "main",
|
||||
state: "error",
|
||||
message: nil,
|
||||
errorMessage: "You have hit your ChatGPT usage limit (plus plan). Try again in ~28 min.")))
|
||||
|
||||
try await waitUntil("pending run clears after error") {
|
||||
await MainActor.run { vm.pendingRunCount == 0 }
|
||||
}
|
||||
try await waitUntil("history refresh shows assistant error message") {
|
||||
await MainActor.run {
|
||||
vm.messages.contains(where: { message in
|
||||
message.role == "assistant" &&
|
||||
OpenClawChatMessage.displayText(
|
||||
contentText: message.content.compactMap(\.text).joined(separator: "\n"),
|
||||
role: message.role,
|
||||
stopReason: message.stopReason,
|
||||
errorMessage: message.errorMessage)
|
||||
.contains("You have hit your ChatGPT usage limit")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func acceptsCanonicalSessionKeyEventsForExternalRuns() async throws {
|
||||
let now = Date().timeIntervalSince1970 * 1000
|
||||
let history1 = historyPayload(messages: [chatTextMessage(role: "user", text: "first", timestamp: now)])
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import Testing
|
||||
@testable import OpenClawKit
|
||||
|
||||
struct GatewayTLSPinningTests {
|
||||
@Test func `first use pinning requires system trust`() {
|
||||
#expect(GatewayTLSFirstUsePolicy.allowsFirstUsePin(systemTrustOk: true))
|
||||
#expect(!GatewayTLSFirstUsePolicy.allowsFirstUsePin(systemTrustOk: false))
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ const rootEntries = [
|
||||
"src/index.ts!",
|
||||
"src/entry.ts!",
|
||||
"src/cli/daemon-cli.ts!",
|
||||
"src/agents/code-mode.worker.ts!",
|
||||
"src/infra/kysely-node-sqlite.ts!",
|
||||
"src/infra/warning-filter.ts!",
|
||||
"src/infra/command-explainer/index.ts!",
|
||||
@@ -66,7 +67,6 @@ const rootBundledPluginRuntimeDependencies = [
|
||||
"@slack/bolt",
|
||||
"@slack/types",
|
||||
"@slack/web-api",
|
||||
"audio-decode",
|
||||
"grammy",
|
||||
"linkedom",
|
||||
"minimatch",
|
||||
|
||||
@@ -7,13 +7,16 @@ services:
|
||||
required: false
|
||||
environment:
|
||||
HOME: /home/node
|
||||
OPENCLAW_HOME: /home/node
|
||||
TERM: xterm-256color
|
||||
# Pin container-side workspace and config paths so host values written to
|
||||
# Pin container-side state, workspace, and config paths so host values written to
|
||||
# `.env` (used by Compose for the bind-mount source below) cannot leak
|
||||
# into runtime code that resolves these env vars inside the container.
|
||||
# Without this override, a macOS host path like /Users/<you>/.openclaw/...
|
||||
# imported from .env caused first-reply `mkdir '/Users'` EACCES failures
|
||||
# in Linux Docker (#77436).
|
||||
OPENCLAW_STATE_DIR: /home/node/.openclaw
|
||||
OPENCLAW_CONFIG_PATH: /home/node/.openclaw/openclaw.json
|
||||
OPENCLAW_CONFIG_DIR: /home/node/.openclaw
|
||||
OPENCLAW_WORKSPACE_DIR: /home/node/.openclaw/workspace
|
||||
OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}
|
||||
@@ -98,9 +101,12 @@ services:
|
||||
- no-new-privileges:true
|
||||
environment:
|
||||
HOME: /home/node
|
||||
OPENCLAW_HOME: /home/node
|
||||
TERM: xterm-256color
|
||||
# Pin container-side workspace and config paths so host values written to
|
||||
# Pin container-side state, workspace, and config paths so host values written to
|
||||
# `.env` cannot leak into runtime code via the env_file import (#77436).
|
||||
OPENCLAW_STATE_DIR: /home/node/.openclaw
|
||||
OPENCLAW_CONFIG_PATH: /home/node/.openclaw/openclaw.json
|
||||
OPENCLAW_CONFIG_DIR: /home/node/.openclaw
|
||||
OPENCLAW_WORKSPACE_DIR: /home/node/.openclaw/workspace
|
||||
OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
f95819d93e9bec5d059440ab54fb4ccb487425cb91d647c8688cd18ef1d4d848 config-baseline.json
|
||||
3325af3a6292959bb38166e9136c638dce5d2093d2339076742890848088a972 config-baseline.core.json
|
||||
ad1d3cb596115d66c21e93de95e229c14c585f0dd4799b4ae3cc29b84761adc6 config-baseline.channel.json
|
||||
0dac8944a0d51ae96f97e3809907f8a04d08413434a1a1190240f7e13bb11c4d config-baseline.plugin.json
|
||||
c79ffc4fd2a7aa7e33a3fa12f22b3d42658a3148f9927c638afdc0fee5512f00 config-baseline.json
|
||||
b2c0fbfdbfc23bd617233e9b78740aa2b5e0f272f5c666e90c2504a8887ae6b8 config-baseline.core.json
|
||||
fe4f1cb00d7d1dee9746779ec3cf14236e5f672c91502268a12ad6e467a2c4ad config-baseline.channel.json
|
||||
e9049ce0154f484f44bb0ac174a44198269256044da5ba62a6e107e78bfd7a70 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
981f125194293842b7a45b1de0ae2ec134f037f63a6cc672ee2a28648251b4c9 plugin-sdk-api-baseline.json
|
||||
4c56ce2cb5bfae526557479a6cc19f8b0042d14f6c717996f8f86da5d5b159df plugin-sdk-api-baseline.jsonl
|
||||
56a29bf137ba67d2c4a428c9d45bf207bc61278f83a28fea972c583f698be62e plugin-sdk-api-baseline.json
|
||||
0f1c320de15ec315e95acfc4b3acb3333c8b7f86cd14df03070bc540ab4a598e plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -131,6 +131,10 @@
|
||||
"source": "Agent Runtimes",
|
||||
"target": "Agent Runtimes"
|
||||
},
|
||||
{
|
||||
"source": "Code mode",
|
||||
"target": "代码模式"
|
||||
},
|
||||
{
|
||||
"source": "Codex harness",
|
||||
"target": "Codex harness"
|
||||
@@ -651,6 +655,26 @@
|
||||
"source": "Manage plugins",
|
||||
"target": "管理插件"
|
||||
},
|
||||
{
|
||||
"source": "Plugin inventory",
|
||||
"target": "插件清单"
|
||||
},
|
||||
{
|
||||
"source": "Plugin reference",
|
||||
"target": "插件参考"
|
||||
},
|
||||
{
|
||||
"source": "Community plugins",
|
||||
"target": "社区插件"
|
||||
},
|
||||
{
|
||||
"source": "ClawHub publishing",
|
||||
"target": "ClawHub 发布"
|
||||
},
|
||||
{
|
||||
"source": "Plugin dependency resolution",
|
||||
"target": "插件依赖解析"
|
||||
},
|
||||
{
|
||||
"source": "Plugin path ownership",
|
||||
"target": "插件路径所有权"
|
||||
@@ -935,6 +959,10 @@
|
||||
"source": "Tool Search",
|
||||
"target": "工具搜索"
|
||||
},
|
||||
{
|
||||
"source": "Code execution",
|
||||
"target": "代码执行"
|
||||
},
|
||||
{
|
||||
"source": "Tools and plugins",
|
||||
"target": "工具和插件"
|
||||
@@ -950,5 +978,9 @@
|
||||
{
|
||||
"source": "ACP agents setup",
|
||||
"target": "ACP Agents 设置"
|
||||
},
|
||||
{
|
||||
"source": "ds4 (local DeepSeek V4)",
|
||||
"target": "ds4(本地 DeepSeek V4)"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -372,12 +372,18 @@ openclaw cron edit <jobId> --message "Updated prompt" --model "opus"
|
||||
# Force run a job now
|
||||
openclaw cron run <jobId>
|
||||
|
||||
# Force run a job now and wait for its terminal status
|
||||
openclaw cron run <jobId> --wait --wait-timeout 10m --poll-interval 2s
|
||||
|
||||
# Run only if due
|
||||
openclaw cron run <jobId> --due
|
||||
|
||||
# View run history
|
||||
openclaw cron runs --id <jobId> --limit 50
|
||||
|
||||
# View one exact run
|
||||
openclaw cron runs --id <jobId> --run-id <runId>
|
||||
|
||||
# Delete a job
|
||||
openclaw cron remove <jobId>
|
||||
|
||||
@@ -386,6 +392,8 @@ openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --mes
|
||||
openclaw cron edit <jobId> --clear-agent
|
||||
```
|
||||
|
||||
`openclaw cron run <jobId>` returns after enqueueing the manual run. Use `--wait` for shutdown hooks, maintenance scripts, or other automation that must block until the queued run finishes. Wait mode polls the exact returned `runId`; it exits `0` for status `ok` and non-zero for `error`, `skipped`, or a wait timeout.
|
||||
|
||||
<Note>
|
||||
Model override note:
|
||||
|
||||
|
||||
@@ -133,7 +133,32 @@ lifecycle, not an agent-finalization gate. Plugins that need to inspect a
|
||||
natural final answer and ask the agent for one more pass should use the typed
|
||||
plugin hook `before_agent_finalize` instead. See [Plugin hooks](/plugins/hooks).
|
||||
|
||||
**Gateway lifecycle events**: `gateway:shutdown` includes `reason` and `restartExpectedMs` and fires when gateway shutdown begins. `gateway:pre-restart` includes the same context but only fires when shutdown is part of an expected restart and a finite `restartExpectedMs` value is supplied. During shutdown, each lifecycle hook wait is best-effort and bounded so shutdown continues if a handler stalls.
|
||||
**Gateway lifecycle events**: `gateway:shutdown` includes `reason` and `restartExpectedMs` and fires when gateway shutdown begins. `gateway:pre-restart` includes the same context but only fires when shutdown is part of an expected restart and a finite `restartExpectedMs` value is supplied. During shutdown, each lifecycle hook wait is best-effort and bounded so shutdown continues if a handler stalls. The default wait budget is 5 seconds for `gateway:shutdown` and 10 seconds for `gateway:pre-restart`.
|
||||
|
||||
Use `gateway:pre-restart` for short restart notices while channels are still available:
|
||||
|
||||
```typescript
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export default async function handler(event) {
|
||||
if (event.type !== "gateway" || event.action !== "pre-restart") {
|
||||
return;
|
||||
}
|
||||
|
||||
const restartInSeconds = Math.ceil(event.context.restartExpectedMs / 1000);
|
||||
await execFileAsync("openclaw", [
|
||||
"system",
|
||||
"event",
|
||||
"--mode",
|
||||
"now",
|
||||
"--text",
|
||||
`Gateway restarting in ~${restartInSeconds}s (${event.context.reason}). Checkpoint now.`,
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
Between the `gateway:shutdown` (or `gateway:pre-restart`) event and the rest of the shutdown sequence, the gateway also fires a typed `session_end` plugin hook for every session that was still active when the process stopped. The event's `reason` is `shutdown` for a plain SIGTERM/SIGINT stop and `restart` when the close was scheduled as part of an expected restart. This drain is bounded so a slow `session_end` handler cannot block process exit, and sessions that have already been finalized through replace / reset / delete / compaction are skipped to avoid double-firing.
|
||||
|
||||
|
||||
131
docs/channels/bot-loop-protection.md
Normal file
131
docs/channels/bot-loop-protection.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
summary: "Bot-to-bot loop protection defaults and channel overrides"
|
||||
read_when:
|
||||
- Configuring bot-authored channel messages
|
||||
- Tuning bot-to-bot loop protection
|
||||
title: "Bot loop protection"
|
||||
sidebarTitle: "Bot loop protection"
|
||||
---
|
||||
|
||||
# Bot loop protection
|
||||
|
||||
OpenClaw can accept messages written by other bots on channels that support `allowBots`.
|
||||
When that path is enabled, pair loop protection prevents two bot identities from
|
||||
replying to each other indefinitely.
|
||||
|
||||
The guard is enforced by the core channel-turn kernel. Each supporting channel
|
||||
maps its own inbound event into generic facts: account or scope, conversation id,
|
||||
sender bot id, and receiver bot id. Core then tracks the participant pair in both
|
||||
directions, applies a sliding-window budget, and suppresses the pair during a
|
||||
cooldown after the budget is exceeded.
|
||||
|
||||
## Defaults
|
||||
|
||||
Pair loop protection is active when a channel lets bot-authored messages reach
|
||||
dispatch. Built-in defaults are:
|
||||
|
||||
- `maxEventsPerWindow: 20` - a bot pair can exchange 20 events within the window
|
||||
- `windowSeconds: 60` - sliding window length
|
||||
- `cooldownSeconds: 60` - suppression time after the pair exceeds the budget
|
||||
|
||||
The guard does not affect normal human-authored messages, single-bot deployments,
|
||||
self-message filtering, or one-shot bot replies that stay under the budget.
|
||||
|
||||
## Configure shared defaults
|
||||
|
||||
Set `channels.defaults.botLoopProtection` once to give every supporting channel
|
||||
the same baseline. Channel and account overrides can still tune individual
|
||||
surfaces.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
defaults: {
|
||||
botLoopProtection: {
|
||||
maxEventsPerWindow: 20,
|
||||
windowSeconds: 60,
|
||||
cooldownSeconds: 60,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Set `enabled: false` only when your channel policy intentionally allows
|
||||
bot-to-bot conversations without automatic suppression.
|
||||
|
||||
## Override per channel or account
|
||||
|
||||
Supporting channels layer their own config over the shared default. Precedence is:
|
||||
|
||||
- `channels.<channel>.<room-or-space>.botLoopProtection`, when the channel supports per-conversation overrides
|
||||
- `channels.<channel>.accounts.<account>.botLoopProtection`, when the channel supports accounts
|
||||
- `channels.<channel>.botLoopProtection`, when the channel supports top-level defaults
|
||||
- `channels.defaults.botLoopProtection`
|
||||
- built-in defaults
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
defaults: {
|
||||
botLoopProtection: {
|
||||
maxEventsPerWindow: 20,
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
botLoopProtection: {
|
||||
maxEventsPerWindow: 8,
|
||||
},
|
||||
accounts: {
|
||||
molty: {
|
||||
allowBots: "mentions",
|
||||
botLoopProtection: {
|
||||
maxEventsPerWindow: 5,
|
||||
cooldownSeconds: 90,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
slack: {
|
||||
allowBots: "mentions",
|
||||
botLoopProtection: {
|
||||
maxEventsPerWindow: 8,
|
||||
},
|
||||
},
|
||||
matrix: {
|
||||
allowBots: "mentions",
|
||||
groups: {
|
||||
"!roomid:example.org": {
|
||||
botLoopProtection: {
|
||||
maxEventsPerWindow: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
googlechat: {
|
||||
allowBots: true,
|
||||
groups: {
|
||||
"spaces/AAAA": {
|
||||
botLoopProtection: {
|
||||
maxEventsPerWindow: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Channel support
|
||||
|
||||
- Discord: native `author.bot` facts, keyed by Discord account, channel, and bot pair.
|
||||
- Slack: native `bot_id` facts for accepted bot-authored messages, keyed by Slack account, channel, and bot pair.
|
||||
- Matrix: configured Matrix bot accounts, keyed by Matrix account, room, and configured bot pair.
|
||||
- Google Chat: native `sender.type=BOT` facts for accepted bot-authored messages, keyed by account, space, and bot pair.
|
||||
|
||||
Channels that do not expose a reliable inbound bot identity keep using their
|
||||
normal self-message and access-policy filters. They should not opt into this
|
||||
guard until they can identify both participants in the bot pair.
|
||||
|
||||
See [SDK runtime](/plugins/sdk-runtime#reusable-runtime-utilities) for plugin
|
||||
implementation details.
|
||||
@@ -661,6 +661,23 @@ Default slash command settings:
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Link previews">
|
||||
Discord generates rich link embeds for URLs by default. OpenClaw suppresses those generated embeds on outbound Discord messages by default, so agent-sent URLs stay as plain links unless you opt in:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
suppressEmbeds: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Set `channels.discord.accounts.<id>.suppressEmbeds` to override one account. Agent message-tool sends can also pass `suppressEmbeds: false` for a single message. Explicit Discord `embeds` payloads are not suppressed by the default link-preview setting.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Live stream preview">
|
||||
OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. `channels.discord.streaming` takes `off` | `partial` | `block` | `progress` (default). `progress` keeps one editable status draft and updates it with tool progress until final delivery; the shared starter label is a rolling line, so it scrolls away like the rest once enough work appears. `streamMode` is a legacy runtime alias. Run `openclaw doctor --fix` to rewrite persisted config to the canonical key.
|
||||
|
||||
@@ -1118,6 +1135,7 @@ OpenClaw uses Discord components v2 for exec approvals and cross-context markers
|
||||
- `channels.discord.ui.components.accentColor` sets the accent color used by Discord component containers (hex).
|
||||
- Set per account with `channels.discord.accounts.<id>.ui.components.accentColor`.
|
||||
- `embeds` are ignored when components v2 are present.
|
||||
- Plain URL previews are suppressed by default. Set `suppressEmbeds: false` on a message action when a single outbound link should expand.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -1569,10 +1587,39 @@ openclaw logs --follow
|
||||
If you set `channels.discord.allowBots=true`, use strict mention and allowlist rules to avoid loop behavior.
|
||||
Prefer `channels.discord.allowBots="mentions"` to only accept bot messages that mention the bot.
|
||||
|
||||
OpenClaw also ships shared [bot loop protection](/channels/bot-loop-protection). Whenever `allowBots` lets bot-authored messages reach dispatch, Discord maps the inbound event to `(account, channel, bot pair)` facts and the generic pair guard suppresses the pair after it crosses the configured event budget. The guard prevents runaway two-bot loops that previously had to be stopped by Discord rate limits; it does not affect single-bot deployments or one-shot bot replies that stay under the budget.
|
||||
|
||||
Default settings (active when `allowBots` is set):
|
||||
|
||||
- `maxEventsPerWindow: 20` -- bot pair can exchange 20 messages within the sliding window
|
||||
- `windowSeconds: 60` -- sliding window length
|
||||
- `cooldownSeconds: 60` -- once the budget trips, every additional bot-to-bot message in either direction is dropped for one minute
|
||||
|
||||
Configure the shared default once under `channels.defaults.botLoopProtection`, then override Discord when a legitimate workflow needs more headroom. Precedence is:
|
||||
|
||||
- `channels.discord.accounts.<account>.botLoopProtection`
|
||||
- `channels.discord.botLoopProtection`
|
||||
- `channels.defaults.botLoopProtection`
|
||||
- built-in defaults
|
||||
|
||||
Discord uses the generic `maxEventsPerWindow`, `windowSeconds`, and `cooldownSeconds` keys.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
defaults: {
|
||||
botLoopProtection: {
|
||||
maxEventsPerWindow: 20,
|
||||
windowSeconds: 60,
|
||||
cooldownSeconds: 60,
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
// Optional Discord-wide override. Account blocks override individual
|
||||
// fields and inherit omitted fields from here.
|
||||
botLoopProtection: {
|
||||
maxEventsPerWindow: 4,
|
||||
},
|
||||
accounts: {
|
||||
mantis: {
|
||||
// Mantis listens to other bots only when they mention her.
|
||||
@@ -1585,6 +1632,12 @@ openclaw logs --follow
|
||||
// Lets Molty write "@Mantis" and send a real Discord mention.
|
||||
Mantis: "MANTIS_DISCORD_USER_ID",
|
||||
},
|
||||
botLoopProtection: {
|
||||
// Allow up to five messages per minute before suppressing the pair.
|
||||
maxEventsPerWindow: 5,
|
||||
windowSeconds: 60,
|
||||
cooldownSeconds: 90,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -185,6 +185,7 @@ Use these identifiers for delivery and allowlists:
|
||||
audience: "https://gateway.example.com/googlechat",
|
||||
webhookPath: "/googlechat",
|
||||
botUser: "users/1234567890", // optional; helps mention detection
|
||||
allowBots: false,
|
||||
dm: {
|
||||
policy: "pairing",
|
||||
allowFrom: ["users/1234567890"],
|
||||
@@ -216,6 +217,7 @@ Notes:
|
||||
- Message actions expose `send` for text and `upload-file` for explicit attachment sends. `upload-file` accepts `media` / `filePath` / `path` plus optional `message`, `filename`, and thread targeting.
|
||||
- `typingIndicator` supports `none`, `message` (default), and `reaction` (reaction requires user OAuth).
|
||||
- Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`).
|
||||
- Bot-authored Google Chat messages are ignored by default. If you intentionally set `allowBots: true`, accepted bot-authored messages use shared [bot loop protection](/channels/bot-loop-protection). Configure `channels.defaults.botLoopProtection`, then override with `channels.googlechat.botLoopProtection` or `channels.googlechat.groups.<space>.botLoopProtection` when one space needs a different budget.
|
||||
|
||||
Secrets reference details: [Secrets Management](/gateway/secrets).
|
||||
|
||||
|
||||
@@ -35,7 +35,8 @@ Quick flow (what happens to a group message):
|
||||
groupPolicy? disabled -> drop
|
||||
groupPolicy? allowlist -> group allowed? no -> drop
|
||||
requireMention? yes -> mentioned? no -> store for context only
|
||||
otherwise -> reply
|
||||
mention/reply/command/DM -> user request
|
||||
always-on group chatter -> user request, or room event when configured
|
||||
```
|
||||
|
||||
## Visible replies
|
||||
@@ -50,7 +51,7 @@ privately instead of calling the message tool. That is not a
|
||||
Discord/Slack/Telegram send failure. Use a tool-call-reliable model for
|
||||
group/channel sessions, or set
|
||||
`messages.groupChat.visibleReplies: "automatic"` to restore legacy visible
|
||||
final replies.
|
||||
final replies for group requests.
|
||||
|
||||
If the message tool is unavailable under the active tool policy, OpenClaw falls
|
||||
back to automatic visible replies instead of silently suppressing the response.
|
||||
@@ -60,9 +61,23 @@ For direct chats and any other source turn, use `messages.visibleReplies: "messa
|
||||
|
||||
This replaces the old pattern of forcing the model to answer `NO_REPLY` for most lurk-mode turns. In tool-only mode, doing nothing visible simply means not calling the message tool.
|
||||
|
||||
Typing indicators are still sent while the agent works in tool-only mode. The default group typing mode is upgraded from "message" to "instant" for these turns because there may never be normal assistant message text before the agent decides whether to call the message tool. Explicit typing-mode config still wins.
|
||||
Typing indicators are still sent for direct group requests. Ambient always-on room events, when enabled, stay quiet unless the agent calls the message tool.
|
||||
|
||||
To restore legacy automatic final replies for group/channel rooms:
|
||||
To submit always-on ambient group chatter as quiet room context instead of legacy user requests:
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
groupChat: {
|
||||
ambientTurns: "room_event",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The default is `ambientTurns: "user_request"` for compatibility.
|
||||
|
||||
To restore legacy automatic final replies for group/channel requests:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -349,8 +364,10 @@ Replying to a bot message counts as an implicit mention when the channel support
|
||||
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
|
||||
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
|
||||
- Allowlisting a group or sender does not disable mention gating; set that group's `requireMention` to `false` when all messages should trigger.
|
||||
- Group chat prompt context carries the resolved silent-reply instruction every turn; workspace files should not duplicate `NO_REPLY` mechanics.
|
||||
- Groups where silent replies are allowed treat clean empty or reasoning-only model turns as silent, equivalent to `NO_REPLY`. Direct chats do the same only when direct silent replies are explicitly allowed; otherwise empty replies remain failed agent turns.
|
||||
- Automatic group chat prompt context carries the resolved silent-reply instruction every turn; workspace files should not duplicate `NO_REPLY` mechanics.
|
||||
- Groups where automatic silent replies are allowed treat clean empty or reasoning-only model turns as silent, equivalent to `NO_REPLY`. Direct chats never receive `NO_REPLY` guidance, and message-tool-only group replies stay quiet by not calling `message(action=send)`.
|
||||
- Ambient always-on group chatter uses legacy user-request semantics by default. Set `messages.groupChat.ambientTurns: "room_event"` to submit it as quiet context instead.
|
||||
- Room events are not stored as fake user requests, and private assistant text from no-message-tool room events is not replayed as chat history.
|
||||
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
|
||||
- Group history context is wrapped uniformly across channels. Mention-gated groups keep pending skipped messages; always-on groups may also retain recent processed room messages when the channel supports it. Use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user