Compare commits
518 Commits
packager-r
...
fix/discor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27a7b5a60b | ||
|
|
857424bbae | ||
|
|
fec4be8dec | ||
|
|
095d522099 | ||
|
|
6931f0fb50 | ||
|
|
b4fa10ae67 | ||
|
|
7b8cce0910 | ||
|
|
5b8bfd261b | ||
|
|
f4b2fd00bc | ||
|
|
dddb1bc942 | ||
|
|
553d17f8af | ||
|
|
e3e8046a93 | ||
|
|
cb391f4bdc | ||
|
|
3a277e394e | ||
|
|
d224776ffb | ||
|
|
c2a0cf0c28 | ||
|
|
39bb1b3322 | ||
|
|
244ed9db39 | ||
|
|
b2aa6e094d | ||
|
|
bc67af6ad8 | ||
|
|
d841c9b26b | ||
|
|
597f956a4f | ||
|
|
f043f2d8c9 | ||
|
|
a4e7f256db | ||
|
|
893f56b87d | ||
|
|
4da68afc73 | ||
|
|
7cfd0aed5f | ||
|
|
d611db8049 | ||
|
|
3eb9c2105c | ||
|
|
9f6462bd56 | ||
|
|
2d03473072 | ||
|
|
dbcdcc5d19 | ||
|
|
c4297a8d60 | ||
|
|
deef9f91bf | ||
|
|
523193a91f | ||
|
|
cd04385f9f | ||
|
|
82fa526bb0 | ||
|
|
3fb4a7eb53 | ||
|
|
7a6928712b | ||
|
|
9b351fcbd8 | ||
|
|
d3ddf893c2 | ||
|
|
a597bd26d4 | ||
|
|
6fa150a890 | ||
|
|
93ad783c1b | ||
|
|
acc6b62289 | ||
|
|
fec1566f04 | ||
|
|
ced5148afd | ||
|
|
c0973f24c6 | ||
|
|
6a392b8493 | ||
|
|
f8ae538985 | ||
|
|
b63f9b7066 | ||
|
|
4e3429ae6e | ||
|
|
78976d3f6f | ||
|
|
e30900f93e | ||
|
|
cef02df9d5 | ||
|
|
0f7ad51020 | ||
|
|
4e16893c61 | ||
|
|
192dbc3ba9 | ||
|
|
d0b0ca9fcf | ||
|
|
22c53af604 | ||
|
|
54948a1d44 | ||
|
|
22a1a56e7e | ||
|
|
15f8c57797 | ||
|
|
404a8bc35f | ||
|
|
7a4c131d6b | ||
|
|
b156aafab9 | ||
|
|
838d875fcb | ||
|
|
7932387df2 | ||
|
|
4d2ba58da5 | ||
|
|
7d26eae3ee | ||
|
|
5dc02aa55e | ||
|
|
c8704297b2 | ||
|
|
eb7b5c02c3 | ||
|
|
314f193030 | ||
|
|
d5bc5ab7ba | ||
|
|
fecd623431 | ||
|
|
1e4cf489e0 | ||
|
|
5d8f43ae8e | ||
|
|
896f9efcb7 | ||
|
|
f448e4bf77 | ||
|
|
ada7a6289f | ||
|
|
731d72e119 | ||
|
|
bf801f5159 | ||
|
|
929a96c2f8 | ||
|
|
2983ef0243 | ||
|
|
b5183c93d6 | ||
|
|
bd0e7d3d22 | ||
|
|
19dfdfe5a8 | ||
|
|
2d6b605cc3 | ||
|
|
025d4152d1 | ||
|
|
f9419e26bb | ||
|
|
a4f86dc433 | ||
|
|
0c035c85ab | ||
|
|
aabc09bb9b | ||
|
|
0d2e13fb73 | ||
|
|
4f05d045b9 | ||
|
|
3daaa19426 | ||
|
|
ec00efb38d | ||
|
|
83a5f7ba8c | ||
|
|
6a759c9191 | ||
|
|
f6b7736744 | ||
|
|
b6a9741ba4 | ||
|
|
1f607bec49 | ||
|
|
3dbb69da05 | ||
|
|
49d383ba7c | ||
|
|
e72e8ebe62 | ||
|
|
374ad8c813 | ||
|
|
6f4da72cb5 | ||
|
|
facf53cc3f | ||
|
|
eaec65656f | ||
|
|
dfaca933c6 | ||
|
|
aaf308d7ec | ||
|
|
d115d48a72 | ||
|
|
d174c38737 | ||
|
|
005dbdd13e | ||
|
|
c31e33cd18 | ||
|
|
f4fbfae97e | ||
|
|
f349d40e62 | ||
|
|
d71779b46f | ||
|
|
df062fdb63 | ||
|
|
52ddaed795 | ||
|
|
b7a20d8e8d | ||
|
|
5cb228fdd0 | ||
|
|
8fd6d4d6dd | ||
|
|
242e8f5c43 | ||
|
|
4aab640fd1 | ||
|
|
35b6ccd62c | ||
|
|
7e1f542233 | ||
|
|
5927c53630 | ||
|
|
8505577218 | ||
|
|
b18b85dc77 | ||
|
|
f3eb003db9 | ||
|
|
0448693f8f | ||
|
|
e86647889c | ||
|
|
993a5e63a1 | ||
|
|
c01e97f124 | ||
|
|
bf2d78505e | ||
|
|
91337b4b6f | ||
|
|
f74d56bd3b | ||
|
|
56d0ad6942 | ||
|
|
5997a4b0ef | ||
|
|
720aa3c1e6 | ||
|
|
223e2a7127 | ||
|
|
31ab8ad46d | ||
|
|
82a8fc0bc7 | ||
|
|
227e31d791 | ||
|
|
357b1e8fee | ||
|
|
4c46c23ca8 | ||
|
|
189b2e0588 | ||
|
|
a39c2263e5 | ||
|
|
0490d0e173 | ||
|
|
64a0339d58 | ||
|
|
077130bdb8 | ||
|
|
12d6b3b0c9 | ||
|
|
3028a1bd3e | ||
|
|
57e055ddb5 | ||
|
|
4fd008e918 | ||
|
|
d39b8541f8 | ||
|
|
ac4183edd7 | ||
|
|
838963d66c | ||
|
|
4852dd4503 | ||
|
|
4d1cb661fc | ||
|
|
3bd961f00a | ||
|
|
583345fdfe | ||
|
|
3d550ed4c3 | ||
|
|
c37cc5ffad | ||
|
|
b83ccfba13 | ||
|
|
8ea890e8fb | ||
|
|
ae6060d777 | ||
|
|
ec708b6ab5 | ||
|
|
944a32cf02 | ||
|
|
c4880675e1 | ||
|
|
8b6537d857 | ||
|
|
12c3821acb | ||
|
|
a69c06e3cc | ||
|
|
67aa7eefe5 | ||
|
|
425c715a05 | ||
|
|
dcba3e5699 | ||
|
|
27083e6f1a | ||
|
|
18bb242316 | ||
|
|
68ea063958 | ||
|
|
eefda1314f | ||
|
|
a8a22920f1 | ||
|
|
a8084b24d6 | ||
|
|
97d5ff3500 | ||
|
|
abb7618b0f | ||
|
|
1ec0f3b81d | ||
|
|
6c3e7896c5 | ||
|
|
2a5fa426f2 | ||
|
|
29203884c2 | ||
|
|
91e120870f | ||
|
|
6a9ead3813 | ||
|
|
cb998aa7f9 | ||
|
|
ac02e45a88 | ||
|
|
8f603ec03d | ||
|
|
84e0ee3c31 | ||
|
|
da2bdbef7e | ||
|
|
1be7c4ba8e | ||
|
|
a82df1015b | ||
|
|
a0b459b8f9 | ||
|
|
28118ca051 | ||
|
|
d374a64658 | ||
|
|
0895bb6de6 | ||
|
|
189cba0100 | ||
|
|
108ebc380f | ||
|
|
93e62d8e3e | ||
|
|
ed28ad2822 | ||
|
|
00bbddeef5 | ||
|
|
5ac59e6e02 | ||
|
|
bfb5a44089 | ||
|
|
599195fb31 | ||
|
|
705d83aec7 | ||
|
|
c80017e704 | ||
|
|
9e67f9d889 | ||
|
|
9383f85046 | ||
|
|
5212d1c79e | ||
|
|
7aa7b04fb0 | ||
|
|
b3d3f36360 | ||
|
|
2b6f8548c9 | ||
|
|
9684ae4c6d | ||
|
|
39fa81dc96 | ||
|
|
f1654b4ba2 | ||
|
|
0b780789bc | ||
|
|
795874711b | ||
|
|
17d8e2a1c8 | ||
|
|
c53e4e6c8f | ||
|
|
4f5bc0a493 | ||
|
|
92ec3ddc14 | ||
|
|
510889d439 | ||
|
|
ceddb4a593 | ||
|
|
794808b169 | ||
|
|
fb6dba2058 | ||
|
|
c62b90a2b7 | ||
|
|
1b223dbdd8 | ||
|
|
e7ccbd1445 | ||
|
|
bc65e787c8 | ||
|
|
3e1986f119 | ||
|
|
b32ae6fa0c | ||
|
|
5d436f48b2 | ||
|
|
90800cd23e | ||
|
|
f52805a783 | ||
|
|
a7385aa8ac | ||
|
|
e8a50e41a5 | ||
|
|
83ce48302f | ||
|
|
25dc4293bf | ||
|
|
3fe22ea2fd | ||
|
|
31939397a9 | ||
|
|
9b2e1769c5 | ||
|
|
61a865031f | ||
|
|
ae6fe67550 | ||
|
|
702b94fe8f | ||
|
|
b5a63e18f9 | ||
|
|
78277152ca | ||
|
|
d1fca442b4 | ||
|
|
3c467baa2d | ||
|
|
c8e110e2e3 | ||
|
|
41ded303b4 | ||
|
|
cbf58d2e1c | ||
|
|
559c8d9930 | ||
|
|
aef1d55300 | ||
|
|
6754a926ee | ||
|
|
18c6f40d32 | ||
|
|
c7bc7249c3 | ||
|
|
beb77229c0 | ||
|
|
d19b746928 | ||
|
|
9df21da129 | ||
|
|
3087657679 | ||
|
|
ffbcb37342 | ||
|
|
a61c2dc4bd | ||
|
|
b4a9eacd76 | ||
|
|
ac2ede5bb1 | ||
|
|
7089885ac4 | ||
|
|
501e893676 | ||
|
|
82631d225c | ||
|
|
799049f586 | ||
|
|
ab1dc89a2d | ||
|
|
0b3d4b8e57 | ||
|
|
eed02a2b57 | ||
|
|
a203430aa3 | ||
|
|
6cf7c02d4a | ||
|
|
6957354d48 | ||
|
|
ee10feb80e | ||
|
|
61c9935264 | ||
|
|
e5dbfde7e1 | ||
|
|
053affffec | ||
|
|
e1e46dc11b | ||
|
|
7cd288a8a0 | ||
|
|
38ac4b8083 | ||
|
|
e7a053b4dd | ||
|
|
85430c8495 | ||
|
|
8e162d9319 | ||
|
|
bed8e7abe6 | ||
|
|
82333add95 | ||
|
|
7a4a068124 | ||
|
|
1a03aad246 | ||
|
|
2f621876f1 | ||
|
|
6dfefa1be1 | ||
|
|
c876d24d89 | ||
|
|
6802b155a8 | ||
|
|
17a148c8a8 | ||
|
|
abd26b6e54 | ||
|
|
8985f23de7 | ||
|
|
c6e6023e3a | ||
|
|
6e7c1c16e7 | ||
|
|
52e240d10d | ||
|
|
b6305e9725 | ||
|
|
2363e1b085 | ||
|
|
c6c53437f7 | ||
|
|
fbe6d7c701 | ||
|
|
2a53eff856 | ||
|
|
cd37c52624 | ||
|
|
17e5a5015c | ||
|
|
8515ae6eea | ||
|
|
d1de66b6cf | ||
|
|
7eeba3de85 | ||
|
|
38c91c5a13 | ||
|
|
88033002ba | ||
|
|
f835301aed | ||
|
|
4d4f693f92 | ||
|
|
d95be2c384 | ||
|
|
014d45f7ee | ||
|
|
4d9e310dad | ||
|
|
f50e1e8015 | ||
|
|
aa1e4962da | ||
|
|
ea07d3fdd8 | ||
|
|
f142048293 | ||
|
|
5fe47e7be6 | ||
|
|
3f44ea244f | ||
|
|
6b2f40652f | ||
|
|
00e79ac897 | ||
|
|
efe530acdd | ||
|
|
d69496c449 | ||
|
|
9386075b7b | ||
|
|
e64dd7b56a | ||
|
|
0e4eada580 | ||
|
|
65b5dbd6c1 | ||
|
|
e770728cb5 | ||
|
|
cb80901cf9 | ||
|
|
3e0076c9ce | ||
|
|
02124094bf | ||
|
|
37f030a671 | ||
|
|
be3c431011 | ||
|
|
b97c5d6158 | ||
|
|
2b4ebcb570 | ||
|
|
2acc0b0f47 | ||
|
|
056070c2bf | ||
|
|
04004c5663 | ||
|
|
e1350ff976 | ||
|
|
8d2029a03d | ||
|
|
58ab60c0fc | ||
|
|
7c27c2d659 | ||
|
|
c1655982d4 | ||
|
|
91c49dd0ea | ||
|
|
8eecf97cc5 | ||
|
|
46e714058c | ||
|
|
1547bb6a07 | ||
|
|
0c8bb361ca | ||
|
|
ddcc7a1a5d | ||
|
|
d9d5b53b42 | ||
|
|
9805ce0097 | ||
|
|
cf69907015 | ||
|
|
09566b1693 | ||
|
|
39d5590230 | ||
|
|
35c5d2be5c | ||
|
|
b88f377623 | ||
|
|
ba84b12535 | ||
|
|
dc9808a674 | ||
|
|
60ad2c2e96 | ||
|
|
a7cbce1b3d | ||
|
|
a74251d415 | ||
|
|
cbc3de6c97 | ||
|
|
01b1e350b2 | ||
|
|
3b4096e02e | ||
|
|
adc818db4a | ||
|
|
568fd337be | ||
|
|
d9ca051a1d | ||
|
|
1b6704ef53 | ||
|
|
887b209db4 | ||
|
|
d4bdcda324 | ||
|
|
966957fc66 | ||
|
|
555eb3f62c | ||
|
|
93b9f1ec5f | ||
|
|
6254e96acf | ||
|
|
19f53543d2 | ||
|
|
618008b483 | ||
|
|
31d1ed351f | ||
|
|
22c1210a16 | ||
|
|
273d70741f | ||
|
|
07be14c02d | ||
|
|
5b2cb8ba11 | ||
|
|
1d7b2bc9c8 | ||
|
|
a881bd41eb | ||
|
|
cd44a0d01e | ||
|
|
a73e7786e7 | ||
|
|
2679089e9e | ||
|
|
c95a61aa9d | ||
|
|
73a97ee255 | ||
|
|
b1dca644bc | ||
|
|
b743e652c0 | ||
|
|
71cee673b2 | ||
|
|
dc5d234848 | ||
|
|
a5cbd036de | ||
|
|
260a514467 | ||
|
|
067509fa44 | ||
|
|
e84b20a527 | ||
|
|
4aaafe5322 | ||
|
|
d5ee766afe | ||
|
|
00c91c3678 | ||
|
|
4ab25a2889 | ||
|
|
14fb2c05b1 | ||
|
|
57d5a8df86 | ||
|
|
b6871d9c0f | ||
|
|
f03ea76db3 | ||
|
|
753491ab80 | ||
|
|
d00adfe98c | ||
|
|
2b2c3a071b | ||
|
|
f8fbeb52b0 | ||
|
|
cb46ea037f | ||
|
|
dece9e8b07 | ||
|
|
32221e194a | ||
|
|
5ecc364d55 | ||
|
|
0dbc51aa55 | ||
|
|
58cf37ceeb | ||
|
|
652318e56a | ||
|
|
d8691ff4ec | ||
|
|
8251f7c235 | ||
|
|
ae1880acf6 | ||
|
|
fddf8a6f4a | ||
|
|
412c1d0af1 | ||
|
|
166cf6a3e0 | ||
|
|
fd3d452f1f | ||
|
|
fdd0e78d1b | ||
|
|
60ce38d216 | ||
|
|
acb2a1ce37 | ||
|
|
ba3a0e7adb | ||
|
|
3a7b1b36b6 | ||
|
|
3830a4b58e | ||
|
|
6288c51774 | ||
|
|
a508c34731 | ||
|
|
5baa08ed13 | ||
|
|
55fd88e967 | ||
|
|
725f63f724 | ||
|
|
c82dc02b4d | ||
|
|
2cf060f774 | ||
|
|
5529473af9 | ||
|
|
5e3b211d93 | ||
|
|
3fd40fc5a3 | ||
|
|
f934725ccd | ||
|
|
5709b30700 | ||
|
|
2d5004cee4 | ||
|
|
1287abe0b5 | ||
|
|
a91bcd2cf4 | ||
|
|
67bfe8fb80 | ||
|
|
be4a490c23 | ||
|
|
e9ed5febc5 | ||
|
|
72baa58edd | ||
|
|
76015aab23 | ||
|
|
115cfb4430 | ||
|
|
ab000bc411 | ||
|
|
e3a93d6705 | ||
|
|
7857096d29 | ||
|
|
cedd520f25 | ||
|
|
4bdb857eca | ||
|
|
997b9ad232 | ||
|
|
e075a33ca3 | ||
|
|
c07036e813 | ||
|
|
b562aa6625 | ||
|
|
fe73878dfc | ||
|
|
ee2fa5f411 | ||
|
|
07dea4c6cc | ||
|
|
7bb0b7d1fc | ||
|
|
a90e007d50 | ||
|
|
94e84e6f75 | ||
|
|
e9c8540e21 | ||
|
|
961ca61b0e | ||
|
|
f809ff5e55 | ||
|
|
d27a763eec | ||
|
|
abd009b092 | ||
|
|
f0e373b82e | ||
|
|
35ab521e07 | ||
|
|
d8d9d3724f | ||
|
|
e3445f59c9 | ||
|
|
a68ed3f64c | ||
|
|
31980bcaf1 | ||
|
|
70f86e326d | ||
|
|
bed0e07620 | ||
|
|
632b71c7f8 | ||
|
|
eef13235ad | ||
|
|
89155aa6c6 | ||
|
|
bbcbabab74 | ||
|
|
0e2d8b8a1e | ||
|
|
c5288300a1 | ||
|
|
a7f6c95675 | ||
|
|
74294a4653 | ||
|
|
c59a472ca2 | ||
|
|
722bfaa9c9 | ||
|
|
37086d0c3e | ||
|
|
a1c50b4ee3 | ||
|
|
d75cd40787 | ||
|
|
34b088ede6 | ||
|
|
36b5f0c9a8 | ||
|
|
704c8ed530 | ||
|
|
2158b09b9d | ||
|
|
ed276d3e50 | ||
|
|
53ec78319d | ||
|
|
51709c63fe | ||
|
|
f8925b7588 | ||
|
|
023091ded3 | ||
|
|
ce922915ab | ||
|
|
f749365b1c | ||
|
|
4fc72226fa | ||
|
|
def74465eb | ||
|
|
a91553c7cf | ||
|
|
65ea200c31 | ||
|
|
0b56472cf5 | ||
|
|
8ba16a894f | ||
|
|
99909f7bc7 | ||
|
|
1b455b6d9f |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -6,14 +6,14 @@ on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
# Detect docs-only changes to skip heavy jobs (test, build, Windows, macOS, Android).
|
||||
# Lint and format always run. Fail-safe: if detection fails, run everything.
|
||||
docs-scope:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
outputs:
|
||||
docs_only: ${{ steps.check.outputs.docs_only }}
|
||||
docs_changed: ${{ steps.check.outputs.docs_changed }}
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
changed-scope:
|
||||
needs: [docs-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
outputs:
|
||||
run_node: ${{ steps.scope.outputs.run_node }}
|
||||
run_macos: ${{ steps.scope.outputs.run_macos }}
|
||||
@@ -672,7 +672,8 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
# setup-android's sdkmanager currently crashes on JDK 21 in CI.
|
||||
java-version: 17
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
4
.github/workflows/docker-release.yml
vendored
4
.github/workflows/docker-release.yml
vendored
@@ -13,6 +13,10 @@ on:
|
||||
- ".agents/**"
|
||||
- "skills/**"
|
||||
|
||||
concurrency:
|
||||
group: docker-release-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
1
.github/workflows/formal-conformance.yml
vendored
1
.github/workflows/formal-conformance.yml
vendored
@@ -108,6 +108,7 @@ jobs:
|
||||
|
||||
- name: Comment on PR (informational)
|
||||
if: steps.drift.outputs.drift == 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
|
||||
28
.github/workflows/install-smoke.yml
vendored
28
.github/workflows/install-smoke.yml
vendored
@@ -7,8 +7,8 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: install-smoke-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
group: install-smoke-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
docs-scope:
|
||||
@@ -33,19 +33,17 @@ jobs:
|
||||
- name: Checkout CLI
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm (corepack retry)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
corepack enable
|
||||
for attempt in 1 2 3; do
|
||||
if corepack prepare pnpm@10.23.0 --activate; then
|
||||
pnpm -v
|
||||
exit 0
|
||||
fi
|
||||
echo "corepack prepare failed (attempt $attempt/3). Retrying..."
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
exit 1
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
check-latest: true
|
||||
|
||||
- name: Setup pnpm + cache store
|
||||
uses: ./.github/actions/setup-pnpm-store-cache
|
||||
with:
|
||||
pnpm-version: "10.23.0"
|
||||
cache-key-suffix: "node22"
|
||||
|
||||
- name: Install pnpm deps (minimal)
|
||||
run: pnpm install --ignore-scripts --frozen-lockfile
|
||||
|
||||
4
.github/workflows/sandbox-common-smoke.yml
vendored
4
.github/workflows/sandbox-common-smoke.yml
vendored
@@ -14,8 +14,8 @@ on:
|
||||
- scripts/sandbox-common-setup.sh
|
||||
|
||||
concurrency:
|
||||
group: sandbox-common-smoke-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
group: sandbox-common-smoke-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
sandbox-common-smoke:
|
||||
|
||||
4
.github/workflows/workflow-sanity.yml
vendored
4
.github/workflows/workflow-sanity.yml
vendored
@@ -6,8 +6,8 @@ on:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: workflow-sanity-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
group: workflow-sanity-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
no-tabs:
|
||||
|
||||
49
AGENTS.md
49
AGENTS.md
@@ -119,6 +119,19 @@
|
||||
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
|
||||
- Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them.
|
||||
|
||||
## GHSA (Repo Advisory) Patch/Publish
|
||||
|
||||
- Fetch: `gh api /repos/openclaw/openclaw/security-advisories/<GHSA>`
|
||||
- Latest npm: `npm view openclaw version --userconfig "$(mktemp)"`
|
||||
- Private fork PRs must be closed:
|
||||
`fork=$(gh api /repos/openclaw/openclaw/security-advisories/<GHSA> | jq -r .private_fork.full_name)`
|
||||
`gh pr list -R "$fork" --state open` (must be empty)
|
||||
- Description newline footgun: write Markdown via heredoc to `/tmp/ghsa.desc.md` (no `"\\n"` strings)
|
||||
- Build patch JSON via jq: `jq -n --rawfile desc /tmp/ghsa.desc.md '{summary,severity,description:$desc,vulnerabilities:[...]}' > /tmp/ghsa.patch.json`
|
||||
- Patch + publish: `gh api -X PATCH /repos/openclaw/openclaw/security-advisories/<GHSA> --input /tmp/ghsa.patch.json` (publish = include `"state":"published"`; no `/publish` endpoint)
|
||||
- If publish fails (HTTP 422): missing `severity`/`description`/`vulnerabilities[]`, or private fork has open PRs
|
||||
- Verify: re-fetch; ensure `state=published`, `published_at` set; `jq -r .description | rg '\\\\n'` returns nothing
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Rebrand/migration issues or legacy config/service warnings: run `openclaw doctor` (see `docs/gateway/doctor.md`).
|
||||
@@ -182,3 +195,39 @@
|
||||
- Publish: `npm publish --access public --otp="<otp>"` (run from the package dir).
|
||||
- Verify without local npmrc side effects: `npm view <pkg> version --userconfig "$(mktemp)"`.
|
||||
- Kill the tmux session after publish.
|
||||
|
||||
## Plugin Release Fast Path (no core `openclaw` publish)
|
||||
|
||||
- Release only already-on-npm plugins. Source list is in `docs/reference/RELEASING.md` under "Current npm plugin list".
|
||||
- Run all CLI `op` calls and `npm publish` inside tmux to avoid hangs/interruption:
|
||||
- `tmux new -d -s release-plugins-$(date +%Y%m%d-%H%M%S)`
|
||||
- `eval "$(op signin --account my.1password.com)"`
|
||||
- 1Password helpers:
|
||||
- password used by `npm login`:
|
||||
`op item get Npmjs --format=json | jq -r '.fields[] | select(.id=="password").value'`
|
||||
- OTP:
|
||||
`op read 'op://Private/Npmjs/one-time password?attribute=otp'`
|
||||
- Fast publish loop (local helper script in `/tmp` is fine; keep repo clean):
|
||||
- compare local plugin `version` to `npm view <name> version`
|
||||
- only run `npm publish --access public --otp="<otp>"` when versions differ
|
||||
- skip if package is missing on npm or version already matches.
|
||||
- Keep `openclaw` untouched: never run publish from repo root unless explicitly requested.
|
||||
- Post-check for each release:
|
||||
- per-plugin: `npm view @openclaw/<name> version --userconfig "$(mktemp)"` should be `2026.2.16`
|
||||
- core guard: `npm view openclaw version --userconfig "$(mktemp)"` should stay at previous version unless explicitly requested.
|
||||
|
||||
## Changelog Release Notes
|
||||
|
||||
- When cutting a mac release with beta GitHub prerelease:
|
||||
- Tag `vYYYY.M.D-beta.N` from the release commit (example: `v2026.2.15-beta.1`).
|
||||
- Create prerelease with title `openclaw YYYY.M.D-beta.N`.
|
||||
- Use release notes from `CHANGELOG.md` version section (`Changes` + `Fixes`, no title duplicate).
|
||||
- Attach at least `OpenClaw-YYYY.M.D.zip` and `OpenClaw-YYYY.M.D.dSYM.zip`; include `.dmg` if available.
|
||||
|
||||
- Keep top version entries in `CHANGELOG.md` sorted by impact:
|
||||
- `### Changes` first.
|
||||
- `### Fixes` deduped and ranked with user-facing fixes first.
|
||||
- Before tagging/publishing, run:
|
||||
- `node --import tsx scripts/release-check.ts`
|
||||
- `pnpm release:check`
|
||||
- `pnpm test:install:smoke` or `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke` for non-root smoke path.
|
||||
|
||||
79
CHANGELOG.md
79
CHANGELOG.md
@@ -2,42 +2,76 @@
|
||||
|
||||
Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.2.15 (Unreleased)
|
||||
## 2026.2.16 (Unreleased)
|
||||
|
||||
### Changes
|
||||
|
||||
- Discord: unlock rich interactive agent prompts with Components v2 (buttons, selects, modals, and attachment-backed file blocks) so for native interaction through Discord. Thanks @thewilloftheshadow.
|
||||
- Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow.
|
||||
- Plugins: expose `llm_input` and `llm_output` hook payloads so extensions can observe prompt/input context and model output usage details. (#16724) Thanks @SecondThread.
|
||||
- Subagents: nested sub-agents (sub-sub-agents) with configurable depth. Set `agents.defaults.subagents.maxSpawnDepth: 2` to allow sub-agents to spawn their own children. Includes `maxChildrenPerAgent` limit (default 5), depth-aware tool policy, and proper announce chain routing. (#14447) Thanks @tyler6204.
|
||||
- Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow.
|
||||
- Slack/Discord/Telegram: add per-channel ack reaction overrides (account/channel-level) to support platform-specific emoji formats. (#17092) Thanks @zerone0x.
|
||||
- Cron/Gateway: add finished-run webhook delivery toggle (`notify`) and dedicated webhook auth token support (`cron.webhookToken`) for outbound cron webhook posts. (#14535) Thanks @advaitpaliwal.
|
||||
- Cron/Gateway: separate per-job webhook delivery (`delivery.mode = "webhook"`) from announce delivery, enforce valid HTTP(S) webhook URLs, and keep a temporary legacy `notify + cron.webhook` fallback for stored jobs. (#17901) Thanks @advaitpaliwal.
|
||||
- Channels: deduplicate probe/token resolution base types across core + extensions while preserving per-channel error typing. (#16986) Thanks @iyoda and @thewilloftheshadow.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Web UI/Agents: hide `BOOTSTRAP.md` in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.
|
||||
- Telegram: omit `message_thread_id` for DM sends/draft previews and keep forum-topic handling (`id=1` general omitted, non-general kept), preventing DM failures with `400 Bad Request: message thread not found`. (#10942) Thanks @garnetlyx.
|
||||
- Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.
|
||||
- Discord: send initial content when creating non-forum threads so `thread-create` content is delivered. (#18117) Thanks @zerone0x.
|
||||
- Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh.
|
||||
- Security/Sessions: create new session transcript JSONL files with user-only (`0o600`) permissions and extend `openclaw security audit --fix` to remediate existing transcript file permissions.
|
||||
- Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent.
|
||||
- Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent.
|
||||
- Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh.
|
||||
- Gateway/Security: redact sensitive session/path details from `status` responses for non-admin clients; full details remain available to `operator.admin`. (#8590) Thanks @fr33d3m0n.
|
||||
- Gateway/Control UI: preserve requested operator scopes for Control UI bypass modes (`allowInsecureAuth` / `dangerouslyDisableDeviceAuth`) when device identity is unavailable, preventing false `missing scope` failures on authenticated LAN/HTTP operator sessions. (#17682) Thanks @leafbird.
|
||||
- LINE/Security: fail closed on webhook startup when channel token or channel secret is missing, and treat LINE accounts as configured only when both are present. (#17587) Thanks @davidahmann.
|
||||
- Skills/Security: restrict `download` installer `targetDir` to the per-skill tools directory to prevent arbitrary file writes. Thanks @Adam55A-code.
|
||||
- Skills/Linux: harden go installer fallback on apt-based systems by handling root/no-sudo environments safely, doing best-effort apt index refresh, and returning actionable errors instead of failing with spawn errors. (#17687) Thanks @mcrolly.
|
||||
- Web Fetch/Security: cap downloaded response body size before HTML parsing to prevent memory exhaustion from oversized or deeply nested pages. Thanks @xuemian168.
|
||||
- Infra/Fetch: ensure foreign abort-signal listener cleanup never masks original fetch successes/failures, while still preventing detached-finally unhandled rejection noise in `wrapFetchWithAbortSignal`. Thanks @Jackten.
|
||||
- Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving `passwordFile` path exemptions, preventing accidental redaction of non-secret config values like `maxTokens` and IRC password-file paths. (#16042) Thanks @akramcodez.
|
||||
- Gateway/Config: prevent `config.patch` object-array merges from falling back to full-array replacement when some patch entries lack `id`, so partial `agents.list` updates no longer drop unrelated agents. (#17989) Thanks @stakeswky.
|
||||
- Dev tooling: harden git `pre-commit` hook against option injection from malicious filenames (for example `--force`), preventing accidental staging of ignored files. Thanks @mrthankyou.
|
||||
- Gateway/Agent: reject malformed `agent:`-prefixed session keys (for example, `agent:main`) in `agent` and `agent.identity.get` instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz.
|
||||
- Gateway/Chat: harden `chat.send` inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.
|
||||
- Gateway/Send: return an actionable error when `send` targets internal-only `webchat`, guiding callers to use `chat.send` or a deliverable channel. (#15703) Thanks @rodrigouroz.
|
||||
- Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing `script-src 'self'`. Thanks @Adam55A-code.
|
||||
- Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent.
|
||||
- Agents/Sandbox: clarify system prompt path guidance so sandbox `bash/exec` uses container paths (for example `/workspace`) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot.
|
||||
- Agents/Context: apply configured model `contextWindow` overrides after provider discovery so `lookupContextTokens()` honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.
|
||||
- Agents/Context: derive `lookupContextTokens()` from auth-available model metadata and keep the smallest discovered context window for duplicate model ids, preventing cross-provider cache collisions from overestimating session context limits. (#17586) Thanks @githabideri and @vignesh07.
|
||||
- Agents/OpenAI: force `store=true` for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.
|
||||
- Memory/FTS: make `buildFtsQuery` Unicode-aware so non-ASCII queries (including CJK) produce keyword tokens instead of falling back to vector-only search. (#17672) Thanks @KinGP5471.
|
||||
- Auto-reply/Compaction: resolve `memory/YYYY-MM-DD.md` placeholders with timezone-aware runtime dates and append a `Current time:` line to memory-flush turns, preventing wrong-year memory filenames without making the system prompt time-variant. (#17603, #17633) Thanks @nicholaspapadam-wq and @vignesh07.
|
||||
- Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.
|
||||
- Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204.
|
||||
- Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone.
|
||||
- Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.
|
||||
- Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.
|
||||
- Agents/Models: probe the primary model when its auth-profile cooldown is near expiry (with per-provider throttling), so runs recover from temporary rate limits without staying on fallback models until restart. (#17478) Thanks @PlayerGhost.
|
||||
- Telegram: omit `message_thread_id` for DM sends/draft previews and keep forum-topic handling (`id=1` general omitted, non-general kept), preventing DM failures with `400 Bad Request: message thread not found`. (#10942) Thanks @garnetlyx.
|
||||
- Telegram: replace inbound `<media:audio>` placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.
|
||||
- Telegram: retry inbound media `getFile` calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.
|
||||
- Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.
|
||||
- Telegram: keep draft-stream preview replies attached to the user message for `replyToMode: "all"` in groups and DMs, preserving threaded reply context from preview through finalization. (#17880) Thanks @yinghaosang.
|
||||
- Telegram: disable block streaming when `channels.telegram.streamMode` is `off`, preventing newline/content-block replies from splitting into multiple messages. (#17679) Thanks @saivarunk.
|
||||
- Telegram: route non-abort slash commands on the normal chat/topic sequential lane while keeping true abort requests (`/stop`, `stop`) on the control lane, preventing command/reply race conditions from control-lane bypass. (#17899) Thanks @obviyus.
|
||||
- Telegram: prevent streaming final replies from being overwritten by later final/error payloads, and suppress fallback tool-error warnings when a recovered assistant answer already exists after tool calls. (#17883) Thanks @Marvae and @obviyus.
|
||||
- Discord: preserve channel session continuity when runtime payloads omit `message.channelId` by falling back to event/raw `channel_id` values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as `sessionKey=unknown`. (#17622) Thanks @shakkernerd.
|
||||
- Discord: dedupe native skill commands by skill name in multi-agent setups to prevent duplicated slash commands with `_2` suffixes. (#17365) Thanks @seewhyme.
|
||||
- Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu.
|
||||
- Web UI/Agents: hide `BOOTSTRAP.md` in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.
|
||||
- Memory/QMD: scope managed collection names per agent and precreate glob-backed collection directories before registration, preventing cross-agent collection clobbering and startup ENOENT failures in fresh workspaces. (#17194) Thanks @jonathanadams96.
|
||||
- Auto-reply/WhatsApp/TUI/Web: when a final assistant message is `NO_REPLY` and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show `NO_REPLY` placeholders. (#7010) Thanks @Morrowind-Xie.
|
||||
- Auto-reply/TTS: keep tool-result media delivery enabled in group chats and native command sessions (while still suppressing tool summary text) so `NO_REPLY` follow-ups do not drop successful TTS audio. (#17991) Thanks @zerone0x.
|
||||
- Cron: infer `payload.kind="agentTurn"` for model-only `cron.update` payload patches, so partial agent-turn updates do not fail validation when `kind` is omitted. (#15664) Thanks @rodrigouroz.
|
||||
- Cron: preserve per-job schedule-error isolation in post-run maintenance recompute so malformed sibling jobs no longer abort persistence of successful runs. (#17852) Thanks @pierreeurope.
|
||||
- TUI: make searchable-select filtering and highlight rendering ANSI-aware so queries ignore hidden escape codes and no longer corrupt ANSI styling sequences during match highlighting. (#4519) Thanks @bee4come.
|
||||
- TUI/Windows: coalesce rapid single-line submit bursts in Git Bash into one multiline message as a fallback when bracketed paste is unavailable, preventing pasted multiline text from being split into multiple sends. (#4986) Thanks @adamkane.
|
||||
- TUI: suppress false `(no output)` placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07.
|
||||
- TUI: preserve copy-sensitive long tokens (URLs/paths/file-like identifiers) during wrapping and overflow sanitization so wrapped output no longer inserts spaces that corrupt copy/paste values. (#17515, #17466, #17505) Thanks @abe238, @trevorpan, and @JasonCry.
|
||||
- Auto-reply/WhatsApp/TUI/Web: when a final assistant message is `NO_REPLY` and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show `NO_REPLY` placeholders. (#7010) Thanks @Morrowind-Xie.
|
||||
- Gateway/Chat: harden `chat.send` inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.
|
||||
- Gateway/Send: return an actionable error when `send` targets internal-only `webchat`, guiding callers to use `chat.send` or a deliverable channel. (#15703) Thanks @rodrigouroz.
|
||||
- Gateway/Agent: reject malformed `agent:`-prefixed session keys (for example, `agent:main`) in `agent` and `agent.identity.get` instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz.
|
||||
- Gateway/Security: redact sensitive session/path details from `status` responses for non-admin clients; full details remain available to `operator.admin`. (#8590) Thanks @fr33d3m0n.
|
||||
- Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.
|
||||
- Agents/OpenAI: force `store=true` for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.
|
||||
- Agents/Context: apply configured model `contextWindow` overrides after provider discovery so `lookupContextTokens()` honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.
|
||||
- CLI/Build: make legacy daemon CLI compatibility shim generation tolerant of minimal tsdown daemon export sets, while preserving restart/register compatibility aliases and surfacing explicit errors for unavailable legacy daemon commands. Thanks @vignesh07.
|
||||
- Telegram: replace inbound `<media:audio>` placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.
|
||||
- Telegram: retry inbound media `getFile` calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.
|
||||
- Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.
|
||||
- Cron: infer `payload.kind="agentTurn"` for model-only `cron.update` payload patches, so partial agent-turn updates do not fail validation when `kind` is omitted. (#15664) Thanks @rodrigouroz.
|
||||
- Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.
|
||||
- Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu.
|
||||
|
||||
## 2026.2.14
|
||||
|
||||
@@ -52,6 +86,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security/Sessions/Telegram: restrict session tool targeting by default to the current session tree (`tools.sessions.visibility`, default `tree`) with sandbox clamping, and pass configured per-account Telegram webhook secrets in webhook mode when no explicit override is provided. Thanks @aether-ai-agent.
|
||||
- CLI/Plugins: ensure `openclaw message send` exits after successful delivery across plugin-backed channels so one-shot sends do not hang. (#16491) Thanks @yinghaosang.
|
||||
- CLI/Plugins: run registered plugin `gateway_stop` hooks before `openclaw message` exits (success and failure paths), so plugin-backed channels can clean up one-shot CLI resources. (#16580) Thanks @gumadeiras.
|
||||
- WhatsApp: honor per-account `dmPolicy` overrides (account-level settings now take precedence over channel defaults for inbound DMs). (#10082) Thanks @mcaxtr.
|
||||
@@ -91,6 +126,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/Subagents: preserve queued announce items and summary state on delivery errors, retry failed announce drains, and avoid dropping unsent announcements on timeout/failure. (#16729) Thanks @Clawdette-Workspace.
|
||||
- Gateway/Config: make `config.patch` merge object arrays by `id` (for example `agents.list`) instead of replacing the whole array, so partial agent updates do not silently delete unrelated agents. (#6766) Thanks @lightclient.
|
||||
- Webchat/Prompts: stop injecting direct-chat `conversation_label` into inbound untrusted metadata context blocks, preventing internal label noise from leaking into visible chat replies. (#16556) Thanks @nberardi.
|
||||
- Auto-reply/Prompts: include trusted inbound `message_id`, `chat_id`, `reply_to_id`, and optional `message_id_full` metadata fields so action tools (for example reactions) can target the triggering message without relying on user text. (#17662) Thanks @MaikiMolto.
|
||||
- Gateway/Sessions: abort active embedded runs and clear queued session work before `sessions.reset`, returning unavailable if the run does not stop in time. (#16576) Thanks @Grynn.
|
||||
- Sessions/Agents: harden transcript path resolution for mismatched agent context by preserving explicit store roots and adding safe absolute-path fallback to the correct agent sessions directory. (#16288) Thanks @robbyczgw-cla.
|
||||
- Agents: add a safety timeout around embedded `session.compact()` to ensure stalled compaction runs settle and release blocked session lanes. (#16331) Thanks @BinHPdev.
|
||||
@@ -106,6 +142,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Tools/Write/Edit: normalize structured text-block arguments for `content`/`oldText`/`newText` before filesystem edits, preventing JSON-like file corruption and false “exact text not found” misses from block-form params. (#16778) Thanks @danielpipernz.
|
||||
- Ollama/Agents: avoid forcing `<final>` tag enforcement for Ollama models, which could suppress all output as `(no output)`. (#16191) Thanks @Glucksberg.
|
||||
- Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238.
|
||||
- Agents/Process: supervise PTY/child process lifecycles with explicit ownership, cancellation, timeouts, and deterministic cleanup, preventing Codex/Pi PTY sessions from dying or stalling on resume. (#14257) Thanks @onutc.
|
||||
- Skills: watch `SKILL.md` only when refreshing skills snapshot to avoid file-descriptor exhaustion in large data trees. (#11325) Thanks @household-bard.
|
||||
- Memory/QMD: make `memory status` read-only by skipping QMD boot update/embed side effects for status-only manager checks.
|
||||
- Memory/QMD: keep original QMD failures when builtin fallback initialization fails (for example missing embedding API keys), instead of replacing them with fallback init errors.
|
||||
@@ -119,6 +156,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory/QMD: make QMD result JSON parsing resilient to noisy command output by extracting the first JSON array from noisy `stdout`.
|
||||
- Memory/QMD: treat prefixed `no results found` marker output as an empty result set in qmd JSON parsing. (#11302) Thanks @blazerui.
|
||||
- Memory/QMD: avoid multi-collection `query` ranking corruption by running one `qmd query -c <collection>` per managed collection and merging by best score (also used for `search`/`vsearch` fallback-to-query). (#16740) Thanks @volarian-vai.
|
||||
- Memory/QMD: rebind managed collections when existing collection metadata drifts (including sessions name-only listings), preventing non-default agents from reusing another agent's `sessions` collection path. (#17194) Thanks @jonathanadams96.
|
||||
- Memory/QMD: make `openclaw memory index` verify and print the active QMD index file path/size, and fail when QMD leaves a missing or zero-byte index artifact after an update. (#16775) Thanks @Shunamxiao.
|
||||
- Memory/QMD: detect null-byte `ENOTDIR` update failures, rebuild managed collections once, and retry update to self-heal corrupted collection metadata. (#12919) Thanks @jorgejhms.
|
||||
- Memory/QMD/Security: add `rawKeyPrefix` support for QMD scope rules and preserve legacy `keyPrefix: "agent:..."` matching, preventing scoped deny bypass when operators match agent-prefixed session keys.
|
||||
@@ -152,6 +190,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/Media: stream and bound URL-backed input media fetches to prevent memory exhaustion from oversized responses. Thanks @vincentkoc.
|
||||
- Security/Skills: harden archive extraction for download-installed skills to prevent path traversal outside the target directory. Thanks @markmusson.
|
||||
- Security/Slack: compute command authorization for DM slash commands even when `dmPolicy=open`, preventing unauthorized users from running privileged commands via DM. Thanks @christos-eth.
|
||||
- Security/Pairing: scope pairing allowlist writes/reads to channel accounts (for example `telegram:yy`), and propagate account-aware pairing approvals so multi-account channels do not share a single per-channel pairing allowFrom store. (#17631) Thanks @crazytan.
|
||||
- Security/iMessage: keep DM pairing-store identities out of group allowlist authorization (prevents cross-context command authorization). Thanks @vincentkoc.
|
||||
- Security/Google Chat: deprecate `users/<email>` allowlists (treat `users/...` as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc.
|
||||
- Security/Google Chat: reject ambiguous shared-path webhook routing when multiple webhook targets verify successfully (prevents cross-account policy-context misrouting). Thanks @vincentkoc.
|
||||
@@ -207,6 +246,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Docs/Hooks: update hooks documentation URLs to the new `/automation/hooks` location. (#16165) Thanks @nicholascyh.
|
||||
- Security/Audit: warn when `gateway.tools.allow` re-enables default-denied tools over HTTP `POST /tools/invoke`, since this can increase RCE blast radius if the gateway is reachable.
|
||||
- Security/Plugins/Hooks: harden npm-based installs by restricting specs to registry packages only, passing `--ignore-scripts` to `npm pack`, and cleaning up temp install directories.
|
||||
- Security/Sessions: preserve inter-session input provenance for routed prompts so delegated/internal sessions are not treated as direct external user instructions. Thanks @anbecker.
|
||||
- Feishu: stop persistent Typing reaction on NO_REPLY/suppressed runs by wiring reply-dispatcher cleanup to remove typing indicators. (#15464) Thanks @arosstale.
|
||||
- Agents: strip leading empty lines from `sanitizeUserFacingText` output and normalize whitespace-only outputs to empty text. (#16158) Thanks @mcinteerj.
|
||||
- BlueBubbles: gracefully degrade when Private API is disabled by filtering private-only actions, skipping private-only reactions/reply effects, and avoiding private reply markers so non-private flows remain usable. (#16002) Thanks @L-U-C-K-Y.
|
||||
@@ -337,6 +377,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Configure/Gateway: reject literal `"undefined"`/`"null"` token input and validate gateway password prompt values to avoid invalid password-mode configs. (#13767) Thanks @omair445.
|
||||
- Gateway: handle async `EPIPE` on stdout/stderr during shutdown. (#13414) Thanks @keshav55.
|
||||
- Gateway/Control UI: resolve missing dashboard assets when `openclaw` is installed globally via symlink-based Node managers (nvm/fnm/n/Homebrew). (#14919) Thanks @aynorica.
|
||||
- Gateway/Control UI: keep partial assistant output visible when runs are aborted, and persist aborted partials to session transcripts for follow-up context.
|
||||
- Cron: use requested `agentId` for isolated job auth resolution. (#13983) Thanks @0xRaini.
|
||||
- Cron: prevent cron jobs from skipping execution when `nextRunAtMs` advances. (#14068) Thanks @WalterSumbon.
|
||||
- Cron: pass `agentId` to `runHeartbeatOnce` for main-session jobs. (#14140) Thanks @ishikawa-pro.
|
||||
|
||||
@@ -13,24 +13,33 @@ Welcome to the lobster tank! 🦞
|
||||
- **Peter Steinberger** - Benevolent Dictator
|
||||
- GitHub: [@steipete](https://github.com/steipete) · X: [@steipete](https://x.com/steipete)
|
||||
|
||||
- **Shadow** - Discord + Slack subsystem
|
||||
- **Shadow** - Discord subsystem, Discord admin
|
||||
- GitHub: [@thewilloftheshadow](https://github.com/thewilloftheshadow) · X: [@4shad0wed](https://x.com/4shad0wed)
|
||||
|
||||
- **Vignesh** - Memory (QMD), formal modeling, TUI, and Lobster
|
||||
- **Vignesh** - Memory (QMD), formal modeling, TUI, IRC, and Lobster
|
||||
- GitHub: [@vignesh07](https://github.com/vignesh07) · X: [@\_vgnsh](https://x.com/_vgnsh)
|
||||
|
||||
- **Jos** - Telegram, API, Nix mode
|
||||
- GitHub: [@joshp123](https://github.com/joshp123) · X: [@jjpcodes](https://x.com/jjpcodes)
|
||||
|
||||
- **Ayaan Zaidi** - Telegram subsystem, iOS app
|
||||
- GitHub: [@obviyus](https://github.com/obviyus) · X: [@0bviyus](https://x.com/0bviyus)
|
||||
|
||||
- **Tyler Yust** - Agents/subagents, cron, BlueBubbles, macOS app
|
||||
- GitHub: [@tyler6204](https://github.com/tyler6204) · X: [@tyleryust](https://x.com/tyleryust)
|
||||
|
||||
- **Mariano Belinky** - iOS app, Security
|
||||
- GitHub: [@mbelinky](https://github.com/mbelinky) · X: [@belimad](https://x.com/belimad)
|
||||
|
||||
- **Seb Slight** - Docs, Agent Reliability, Runtime Hardening
|
||||
- GitHub: [@sebslight](https://github.com/sebslight) · X: [@sebslig](https://x.com/sebslig)
|
||||
|
||||
- **Christoph Nakazawa** - JS Infra
|
||||
- GitHub: [@cpojer](https://github.com/cpojer) · X: [@cnakazawa](https://x.com/cnakazawa)
|
||||
|
||||
- **Gustavo Madeira Santana** - Multi-agents, CLI, web UI
|
||||
- GitHub: [@gumadeiras](https://github.com/gumadeiras) · X: [@gumadeiras](https://x.com/gumadeiras)
|
||||
|
||||
- **Maximilian Nussbaumer** - DevOps, CI, Code Sanity
|
||||
- GitHub: [@quotentiroler](https://github.com/quotentiroler) · X: [@quotentiroler](https://x.com/quotentiroler)
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. **Bugs & small fixes** → Open a PR!
|
||||
|
||||
164
appcast.xml
164
appcast.xml
@@ -140,6 +140,74 @@
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.14/OpenClaw-2026.2.14.zip" length="22914034" type="application/octet-stream" sparkle:edSignature="lR3nuq46/akMIN8RFDpMkTE0VOVoDVG53Xts589LryMGEtUvJxRQDtHBXfx7ZvToTq6CFKG+L5Kq/4rUspMoAQ=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.2.15</title>
|
||||
<pubDate>Mon, 16 Feb 2026 05:04:34 +0100</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>11213</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.2.15</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.2.15</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Discord: unlock rich interactive agent prompts with Components v2 (buttons, selects, modals, and attachment-backed file blocks) so for native interaction through Discord. Thanks @thewilloftheshadow.</li>
|
||||
<li>Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow.</li>
|
||||
<li>Plugins: expose <code>llm_input</code> and <code>llm_output</code> hook payloads so extensions can observe prompt/input context and model output usage details. (#16724) Thanks @SecondThread.</li>
|
||||
<li>Subagents: nested sub-agents (sub-sub-agents) with configurable depth. Set <code>agents.defaults.subagents.maxSpawnDepth: 2</code> to allow sub-agents to spawn their own children. Includes <code>maxChildrenPerAgent</code> limit (default 5), depth-aware tool policy, and proper announce chain routing. (#14447) Thanks @tyler6204.</li>
|
||||
<li>Slack/Discord/Telegram: add per-channel ack reaction overrides (account/channel-level) to support platform-specific emoji formats. (#17092) Thanks @zerone0x.</li>
|
||||
<li>Cron/Gateway: add finished-run webhook delivery toggle (<code>notify</code>) and dedicated webhook auth token support (<code>cron.webhookToken</code>) for outbound cron webhook posts. (#14535) Thanks @advaitpaliwal.</li>
|
||||
<li>Channels: deduplicate probe/token resolution base types across core + extensions while preserving per-channel error typing. (#16986) Thanks @iyoda and @thewilloftheshadow.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh.</li>
|
||||
<li>Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent.</li>
|
||||
<li>Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent.</li>
|
||||
<li>Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh.</li>
|
||||
<li>Gateway/Security: redact sensitive session/path details from <code>status</code> responses for non-admin clients; full details remain available to <code>operator.admin</code>. (#8590) Thanks @fr33d3m0n.</li>
|
||||
<li>Gateway/Control UI: preserve requested operator scopes for Control UI bypass modes (<code>allowInsecureAuth</code> / <code>dangerouslyDisableDeviceAuth</code>) when device identity is unavailable, preventing false <code>missing scope</code> failures on authenticated LAN/HTTP operator sessions. (#17682) Thanks @leafbird.</li>
|
||||
<li>LINE/Security: fail closed on webhook startup when channel token or channel secret is missing, and treat LINE accounts as configured only when both are present. (#17587) Thanks @davidahmann.</li>
|
||||
<li>Skills/Security: restrict <code>download</code> installer <code>targetDir</code> to the per-skill tools directory to prevent arbitrary file writes. Thanks @Adam55A-code.</li>
|
||||
<li>Skills/Linux: harden go installer fallback on apt-based systems by handling root/no-sudo environments safely, doing best-effort apt index refresh, and returning actionable errors instead of failing with spawn errors. (#17687) Thanks @mcrolly.</li>
|
||||
<li>Web Fetch/Security: cap downloaded response body size before HTML parsing to prevent memory exhaustion from oversized or deeply nested pages. Thanks @xuemian168.</li>
|
||||
<li>Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving <code>passwordFile</code> path exemptions, preventing accidental redaction of non-secret config values like <code>maxTokens</code> and IRC password-file paths. (#16042) Thanks @akramcodez.</li>
|
||||
<li>Dev tooling: harden git <code>pre-commit</code> hook against option injection from malicious filenames (for example <code>--force</code>), preventing accidental staging of ignored files. Thanks @mrthankyou.</li>
|
||||
<li>Gateway/Agent: reject malformed <code>agent:</code>-prefixed session keys (for example, <code>agent:main</code>) in <code>agent</code> and <code>agent.identity.get</code> instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz.</li>
|
||||
<li>Gateway/Chat: harden <code>chat.send</code> inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.</li>
|
||||
<li>Gateway/Send: return an actionable error when <code>send</code> targets internal-only <code>webchat</code>, guiding callers to use <code>chat.send</code> or a deliverable channel. (#15703) Thanks @rodrigouroz.</li>
|
||||
<li>Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing <code>script-src 'self'</code>. Thanks @Adam55A-code.</li>
|
||||
<li>Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent.</li>
|
||||
<li>Agents/Sandbox: clarify system prompt path guidance so sandbox <code>bash/exec</code> uses container paths (for example <code>/workspace</code>) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot.</li>
|
||||
<li>Agents/Context: apply configured model <code>contextWindow</code> overrides after provider discovery so <code>lookupContextTokens()</code> honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.</li>
|
||||
<li>Agents/Context: derive <code>lookupContextTokens()</code> from auth-available model metadata and keep the smallest discovered context window for duplicate model ids, preventing cross-provider cache collisions from overestimating session context limits. (#17586) Thanks @githabideri and @vignesh07.</li>
|
||||
<li>Agents/OpenAI: force <code>store=true</code> for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.</li>
|
||||
<li>Memory/FTS: make <code>buildFtsQuery</code> Unicode-aware so non-ASCII queries (including CJK) produce keyword tokens instead of falling back to vector-only search. (#17672) Thanks @KinGP5471.</li>
|
||||
<li>Auto-reply/Compaction: resolve <code>memory/YYYY-MM-DD.md</code> placeholders with timezone-aware runtime dates and append a <code>Current time:</code> line to memory-flush turns, preventing wrong-year memory filenames without making the system prompt time-variant. (#17603, #17633) Thanks @nicholaspapadam-wq and @vignesh07.</li>
|
||||
<li>Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.</li>
|
||||
<li>Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204.</li>
|
||||
<li>Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone.</li>
|
||||
<li>Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.</li>
|
||||
<li>Subagents/Models: preserve <code>agents.defaults.model.fallbacks</code> when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.</li>
|
||||
<li>Telegram: omit <code>message_thread_id</code> for DM sends/draft previews and keep forum-topic handling (<code>id=1</code> general omitted, non-general kept), preventing DM failures with <code>400 Bad Request: message thread not found</code>. (#10942) Thanks @garnetlyx.</li>
|
||||
<li>Telegram: replace inbound <code><media:audio></code> placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.</li>
|
||||
<li>Telegram: retry inbound media <code>getFile</code> calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.</li>
|
||||
<li>Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.</li>
|
||||
<li>Discord: preserve channel session continuity when runtime payloads omit <code>message.channelId</code> by falling back to event/raw <code>channel_id</code> values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as <code>sessionKey=unknown</code>. (#17622) Thanks @shakkernerd.</li>
|
||||
<li>Discord: dedupe native skill commands by skill name in multi-agent setups to prevent duplicated slash commands with <code>_2</code> suffixes. (#17365) Thanks @seewhyme.</li>
|
||||
<li>Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu.</li>
|
||||
<li>Web UI/Agents: hide <code>BOOTSTRAP.md</code> in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.</li>
|
||||
<li>Auto-reply/WhatsApp/TUI/Web: when a final assistant message is <code>NO_REPLY</code> and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show <code>NO_REPLY</code> placeholders. (#7010) Thanks @Morrowind-Xie.</li>
|
||||
<li>Cron: infer <code>payload.kind="agentTurn"</code> for model-only <code>cron.update</code> payload patches, so partial agent-turn updates do not fail validation when <code>kind</code> is omitted. (#15664) Thanks @rodrigouroz.</li>
|
||||
<li>TUI: make searchable-select filtering and highlight rendering ANSI-aware so queries ignore hidden escape codes and no longer corrupt ANSI styling sequences during match highlighting. (#4519) Thanks @bee4come.</li>
|
||||
<li>TUI/Windows: coalesce rapid single-line submit bursts in Git Bash into one multiline message as a fallback when bracketed paste is unavailable, preventing pasted multiline text from being split into multiple sends. (#4986) Thanks @adamkane.</li>
|
||||
<li>TUI: suppress false <code>(no output)</code> placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07.</li>
|
||||
<li>TUI: preserve copy-sensitive long tokens (URLs/paths/file-like identifiers) during wrapping and overflow sanitization so wrapped output no longer inserts spaces that corrupt copy/paste values. (#17515, #17466, #17505) Thanks @abe238, @trevorpan, and @JasonCry.</li>
|
||||
<li>CLI/Build: make legacy daemon CLI compatibility shim generation tolerant of minimal tsdown daemon export sets, while preserving restart/register compatibility aliases and surfacing explicit errors for unavailable legacy daemon commands. Thanks @vignesh07.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.15/OpenClaw-2026.2.15.zip" length="22896513" type="application/octet-stream" sparkle:edSignature="MLGsd2NeHXFRH1Or0bFQnAjqfuuJDuhl1mvKFIqTQcRvwbeyvOyyLXrqSbmaOgJR3wBQBKLs6jYQ9dQ/3R8RCg=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.2.13</title>
|
||||
<pubDate>Sat, 14 Feb 2026 04:30:23 +0100</pubDate>
|
||||
@@ -241,101 +309,5 @@
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.13/OpenClaw-2026.2.13.zip" length="22902077" type="application/octet-stream" sparkle:edSignature="RpkwlPtB2yN7UOYZWfthV5grhDUcbhcHMeicdRA864Vo/P0Hnq5aHKmSvcbWkjHut96TC57bX+AeUrL7txpLCg=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.2.12</title>
|
||||
<pubDate>Fri, 13 Feb 2026 03:17:54 +0100</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>9500</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.2.12</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.2.12</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>CLI: add <code>openclaw logs --local-time</code> to display log timestamps in local timezone. (#13818) Thanks @xialonglee.</li>
|
||||
<li>Telegram: render blockquotes as native <code><blockquote></code> tags instead of stripping them. (#14608)</li>
|
||||
<li>Config: avoid redacting <code>maxTokens</code>-like fields during config snapshot redaction, preventing round-trip validation failures in <code>/config</code>. (#14006) Thanks @constansino.</li>
|
||||
</ul>
|
||||
<h3>Breaking</h3>
|
||||
<ul>
|
||||
<li>Hooks: <code>POST /hooks/agent</code> now rejects payload <code>sessionKey</code> overrides by default. To keep fixed hook context, set <code>hooks.defaultSessionKey</code> (recommended with <code>hooks.allowedSessionKeyPrefixes: ["hook:"]</code>). If you need legacy behavior, explicitly set <code>hooks.allowRequestSessionKey: true</code>. Thanks @alpernae for reporting.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Gateway/OpenResponses: harden URL-based <code>input_file</code>/<code>input_image</code> handling with explicit SSRF deny policy, hostname allowlists (<code>files.urlAllowlist</code> / <code>images.urlAllowlist</code>), per-request URL input caps (<code>maxUrlParts</code>), blocked-fetch audit logging, and regression coverage/docs updates.</li>
|
||||
<li>Security: fix unauthenticated Nostr profile API remote config tampering. (#13719) Thanks @coygeek.</li>
|
||||
<li>Security: remove bundled soul-evil hook. (#14757) Thanks @Imccccc.</li>
|
||||
<li>Security/Audit: add hook session-routing hardening checks (<code>hooks.defaultSessionKey</code>, <code>hooks.allowRequestSessionKey</code>, and prefix allowlists), and warn when HTTP API endpoints allow explicit session-key routing.</li>
|
||||
<li>Security/Sandbox: confine mirrored skill sync destinations to the sandbox <code>skills/</code> root and stop using frontmatter-controlled skill names as filesystem destination paths. Thanks @1seal.</li>
|
||||
<li>Security/Web tools: treat browser/web content as untrusted by default (wrapped outputs for browser snapshot/tabs/console and structured external-content metadata for web tools), and strip <code>toolResult.details</code> from model-facing transcript/compaction inputs to reduce prompt-injection replay risk.</li>
|
||||
<li>Security/Hooks: harden webhook and device token verification with shared constant-time secret comparison, and add per-client auth-failure throttling for hook endpoints (<code>429</code> + <code>Retry-After</code>). Thanks @akhmittra.</li>
|
||||
<li>Security/Browser: require auth for loopback browser control HTTP routes, auto-generate <code>gateway.auth.token</code> when browser control starts without auth, and add a security-audit check for unauthenticated browser control. Thanks @tcusolle.</li>
|
||||
<li>Sessions/Gateway: harden transcript path resolution and reject unsafe session IDs/file paths so session operations stay within agent sessions directories. Thanks @akhmittra.</li>
|
||||
<li>Gateway: raise WS payload/buffer limits so 5,000,000-byte image attachments work reliably. (#14486) Thanks @0xRaini.</li>
|
||||
<li>Logging/CLI: use local timezone timestamps for console prefixing, and include <code>±HH:MM</code> offsets when using <code>openclaw logs --local-time</code> to avoid ambiguity. (#14771) Thanks @0xRaini.</li>
|
||||
<li>Gateway: drain active turns before restart to prevent message loss. (#13931) Thanks @0xRaini.</li>
|
||||
<li>Gateway: auto-generate auth token during install to prevent launchd restart loops. (#13813) Thanks @cathrynlavery.</li>
|
||||
<li>Gateway: prevent <code>undefined</code>/missing token in auth config. (#13809) Thanks @asklee-klawd.</li>
|
||||
<li>Gateway: handle async <code>EPIPE</code> on stdout/stderr during shutdown. (#13414) Thanks @keshav55.</li>
|
||||
<li>Gateway/Control UI: resolve missing dashboard assets when <code>openclaw</code> is installed globally via symlink-based Node managers (nvm/fnm/n/Homebrew). (#14919) Thanks @aynorica.</li>
|
||||
<li>Cron: use requested <code>agentId</code> for isolated job auth resolution. (#13983) Thanks @0xRaini.</li>
|
||||
<li>Cron: prevent cron jobs from skipping execution when <code>nextRunAtMs</code> advances. (#14068) Thanks @WalterSumbon.</li>
|
||||
<li>Cron: pass <code>agentId</code> to <code>runHeartbeatOnce</code> for main-session jobs. (#14140) Thanks @ishikawa-pro.</li>
|
||||
<li>Cron: re-arm timers when <code>onTimer</code> fires while a job is still executing. (#14233) Thanks @tomron87.</li>
|
||||
<li>Cron: prevent duplicate fires when multiple jobs trigger simultaneously. (#14256) Thanks @xinhuagu.</li>
|
||||
<li>Cron: isolate scheduler errors so one bad job does not break all jobs. (#14385) Thanks @MarvinDontPanic.</li>
|
||||
<li>Cron: prevent one-shot <code>at</code> jobs from re-firing on restart after skipped/errored runs. (#13878) Thanks @lailoo.</li>
|
||||
<li>Heartbeat: prevent scheduler stalls on unexpected run errors and avoid immediate rerun loops after <code>requests-in-flight</code> skips. (#14901) Thanks @joeykrug.</li>
|
||||
<li>Cron: honor stored session model overrides for isolated-agent runs while preserving <code>hooks.gmail.model</code> precedence for Gmail hook sessions. (#14983) Thanks @shtse8.</li>
|
||||
<li>Logging/Browser: fall back to <code>os.tmpdir()/openclaw</code> for default log, browser trace, and browser download temp paths when <code>/tmp/openclaw</code> is unavailable.</li>
|
||||
<li>WhatsApp: convert Markdown bold/strikethrough to WhatsApp formatting. (#14285) Thanks @Raikan10.</li>
|
||||
<li>WhatsApp: allow media-only sends and normalize leading blank payloads. (#14408) Thanks @karimnaguib.</li>
|
||||
<li>WhatsApp: default MIME type for voice messages when Baileys omits it. (#14444) Thanks @mcaxtr.</li>
|
||||
<li>Telegram: handle no-text message in model picker editMessageText. (#14397) Thanks @0xRaini.</li>
|
||||
<li>Telegram: surface REACTION_INVALID as non-fatal warning. (#14340) Thanks @0xRaini.</li>
|
||||
<li>BlueBubbles: fix webhook auth bypass via loopback proxy trust. (#13787) Thanks @coygeek.</li>
|
||||
<li>Slack: change default replyToMode from "off" to "all". (#14364) Thanks @nm-de.</li>
|
||||
<li>Slack: detect control commands when channel messages start with bot mention prefixes (for example, <code>@Bot /new</code>). (#14142) Thanks @beefiker.</li>
|
||||
<li>Signal: enforce E.164 validation for the Signal bot account prompt so mistyped numbers are caught early. (#15063) Thanks @Duartemartins.</li>
|
||||
<li>Discord: process DM reactions instead of silently dropping them. (#10418) Thanks @mcaxtr.</li>
|
||||
<li>Discord: respect replyToMode in threads. (#11062) Thanks @cordx56.</li>
|
||||
<li>Heartbeat: filter noise-only system events so scheduled reminder notifications do not fire when cron runs carry only heartbeat markers. (#13317) Thanks @pvtclawn.</li>
|
||||
<li>Signal: render mention placeholders as <code>@uuid</code>/<code>@phone</code> so mention gating and Clawdbot targeting work. (#2013) Thanks @alexgleason.</li>
|
||||
<li>Discord: omit empty content fields for media-only messages while preserving caption whitespace. (#9507) Thanks @leszekszpunar.</li>
|
||||
<li>Onboarding/Providers: add Z.AI endpoint-specific auth choices (<code>zai-coding-global</code>, <code>zai-coding-cn</code>, <code>zai-global</code>, <code>zai-cn</code>) and expand default Z.AI model wiring. (#13456) Thanks @tomsun28.</li>
|
||||
<li>Onboarding/Providers: update MiniMax API default/recommended models from M2.1 to M2.5, add M2.5/M2.5-Lightning model entries, and include <code>minimax-m2.5</code> in modern model filtering. (#14865) Thanks @adao-max.</li>
|
||||
<li>Ollama: use configured <code>models.providers.ollama.baseUrl</code> for model discovery and normalize <code>/v1</code> endpoints to the native Ollama API root. (#14131) Thanks @shtse8.</li>
|
||||
<li>Voice Call: pass Twilio stream auth token via <code><Parameter></code> instead of query string. (#14029) Thanks @mcwigglesmcgee.</li>
|
||||
<li>Feishu: pass <code>Buffer</code> directly to the Feishu SDK upload APIs instead of <code>Readable.from(...)</code> to avoid form-data upload failures. (#10345) Thanks @youngerstyle.</li>
|
||||
<li>Feishu: trigger mention-gated group handling only when the bot itself is mentioned (not just any mention). (#11088) Thanks @openperf.</li>
|
||||
<li>Feishu: probe status uses the resolved account context for multi-account credential checks. (#11233) Thanks @onevcat.</li>
|
||||
<li>Feishu DocX: preserve top-level converted block order using <code>firstLevelBlockIds</code> when writing/appending documents. (#13994) Thanks @Cynosure159.</li>
|
||||
<li>Feishu plugin packaging: remove <code>workspace:*</code> <code>openclaw</code> dependency from <code>extensions/feishu</code> and sync lockfile for install compatibility. (#14423) Thanks @jackcooper2015.</li>
|
||||
<li>CLI/Wizard: exit with code 1 when <code>configure</code>, <code>agents add</code>, or interactive <code>onboard</code> wizards are canceled, so <code>set -e</code> automation stops correctly. (#14156) Thanks @0xRaini.</li>
|
||||
<li>Media: strip <code>MEDIA:</code> lines with local paths instead of leaking as visible text. (#14399) Thanks @0xRaini.</li>
|
||||
<li>Config/Cron: exclude <code>maxTokens</code> from config redaction and honor <code>deleteAfterRun</code> on skipped cron jobs. (#13342) Thanks @niceysam.</li>
|
||||
<li>Config: ignore <code>meta</code> field changes in config file watcher. (#13460) Thanks @brandonwise.</li>
|
||||
<li>Cron: use requested <code>agentId</code> for isolated job auth resolution. (#13983) Thanks @0xRaini.</li>
|
||||
<li>Cron: pass <code>agentId</code> to <code>runHeartbeatOnce</code> for main-session jobs. (#14140) Thanks @ishikawa-pro.</li>
|
||||
<li>Cron: prevent cron jobs from skipping execution when <code>nextRunAtMs</code> advances. (#14068) Thanks @WalterSumbon.</li>
|
||||
<li>Cron: re-arm timers when <code>onTimer</code> fires while a job is still executing. (#14233) Thanks @tomron87.</li>
|
||||
<li>Cron: prevent duplicate fires when multiple jobs trigger simultaneously. (#14256) Thanks @xinhuagu.</li>
|
||||
<li>Cron: isolate scheduler errors so one bad job does not break all jobs. (#14385) Thanks @MarvinDontPanic.</li>
|
||||
<li>Cron: prevent one-shot <code>at</code> jobs from re-firing on restart after skipped/errored runs. (#13878) Thanks @lailoo.</li>
|
||||
<li>Daemon: suppress <code>EPIPE</code> error when restarting LaunchAgent. (#14343) Thanks @0xRaini.</li>
|
||||
<li>Antigravity: add opus 4.6 forward-compat model and bypass thinking signature sanitization. (#14218) Thanks @jg-noncelogic.</li>
|
||||
<li>Agents: prevent file descriptor leaks in child process cleanup. (#13565) Thanks @KyleChen26.</li>
|
||||
<li>Agents: prevent double compaction caused by cache TTL bypassing guard. (#13514) Thanks @taw0002.</li>
|
||||
<li>Agents: use last API call's cache tokens for context display instead of accumulated sum. (#13805) Thanks @akari-musubi.</li>
|
||||
<li>Agents: keep followup-runner session <code>totalTokens</code> aligned with post-compaction context by using last-call usage and shared token-accounting logic. (#14979) Thanks @shtse8.</li>
|
||||
<li>Hooks/Plugins: wire 9 previously unwired plugin lifecycle hooks into core runtime paths (session, compaction, gateway, and outbound message hooks). (#14882) Thanks @shtse8.</li>
|
||||
<li>Hooks/Tools: dispatch <code>before_tool_call</code> and <code>after_tool_call</code> hooks from both tool execution paths with rebased conflict fixes. (#15012) Thanks @Patrick-Barletta, @Takhoffman.</li>
|
||||
<li>Discord: allow channel-edit to archive/lock threads and set auto-archive duration. (#5542) Thanks @stumct.</li>
|
||||
<li>Discord tests: use a partial @buape/carbon mock in slash command coverage. (#13262) Thanks @arosstale.</li>
|
||||
<li>Tests: update thread ID handling in Slack message collection tests. (#14108) Thanks @swizzmagik.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.12/OpenClaw-2026.2.12.zip" length="22877692" type="application/octet-stream" sparkle:edSignature="TGylTM4/7Lab+qp1nuPeOAmEVV1WkafXUPub8ws0z/0mYfbVygRuiev+u3zdPjQWhLnGYTgRgKVyW+kB2+Q2BQ=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -21,8 +21,8 @@ android {
|
||||
applicationId = "ai.openclaw.android"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 202602150
|
||||
versionName = "2026.2.15"
|
||||
versionCode = 202602160
|
||||
versionName = "2026.2.16"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.2.15</string>
|
||||
<string>2026.2.16</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>20260215</string>
|
||||
<string>20260216</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.2.15</string>
|
||||
<string>2026.2.16</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>20260215</string>
|
||||
<string>20260216</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -81,8 +81,8 @@ targets:
|
||||
properties:
|
||||
CFBundleDisplayName: OpenClaw
|
||||
CFBundleIconName: AppIcon
|
||||
CFBundleShortVersionString: "2026.2.15"
|
||||
CFBundleVersion: "20260215"
|
||||
CFBundleShortVersionString: "2026.2.16"
|
||||
CFBundleVersion: "20260216"
|
||||
UILaunchScreen: {}
|
||||
UIApplicationSceneManifest:
|
||||
UIApplicationSupportsMultipleScenes: false
|
||||
@@ -130,5 +130,5 @@ targets:
|
||||
path: Tests/Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: OpenClawTests
|
||||
CFBundleShortVersionString: "2026.2.15"
|
||||
CFBundleVersion: "20260215"
|
||||
CFBundleShortVersionString: "2026.2.16"
|
||||
CFBundleVersion: "20260216"
|
||||
|
||||
@@ -21,6 +21,7 @@ enum CronWakeMode: String, CaseIterable, Identifiable, Codable {
|
||||
enum CronDeliveryMode: String, CaseIterable, Identifiable, Codable {
|
||||
case none
|
||||
case announce
|
||||
case webhook
|
||||
|
||||
var id: String {
|
||||
self.rawValue
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.2.15</string>
|
||||
<string>2026.2.16</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>202602150</string>
|
||||
<string>202602160</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -2094,7 +2094,7 @@ public struct CronJob: Codable, Sendable {
|
||||
public let sessiontarget: AnyCodable
|
||||
public let wakemode: AnyCodable
|
||||
public let payload: AnyCodable
|
||||
public let delivery: [String: AnyCodable]?
|
||||
public let delivery: AnyCodable?
|
||||
public let state: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
@@ -2110,7 +2110,7 @@ public struct CronJob: Codable, Sendable {
|
||||
sessiontarget: AnyCodable,
|
||||
wakemode: AnyCodable,
|
||||
payload: AnyCodable,
|
||||
delivery: [String: AnyCodable]?,
|
||||
delivery: AnyCodable?,
|
||||
state: [String: AnyCodable]
|
||||
) {
|
||||
self.id = id
|
||||
@@ -2172,7 +2172,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
public let sessiontarget: AnyCodable
|
||||
public let wakemode: AnyCodable
|
||||
public let payload: AnyCodable
|
||||
public let delivery: [String: AnyCodable]?
|
||||
public let delivery: AnyCodable?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
@@ -2184,7 +2184,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
sessiontarget: AnyCodable,
|
||||
wakemode: AnyCodable,
|
||||
payload: AnyCodable,
|
||||
delivery: [String: AnyCodable]?
|
||||
delivery: AnyCodable?
|
||||
) {
|
||||
self.name = name
|
||||
self.agentid = agentid
|
||||
|
||||
@@ -2094,7 +2094,7 @@ public struct CronJob: Codable, Sendable {
|
||||
public let sessiontarget: AnyCodable
|
||||
public let wakemode: AnyCodable
|
||||
public let payload: AnyCodable
|
||||
public let delivery: [String: AnyCodable]?
|
||||
public let delivery: AnyCodable?
|
||||
public let state: [String: AnyCodable]
|
||||
|
||||
public init(
|
||||
@@ -2110,7 +2110,7 @@ public struct CronJob: Codable, Sendable {
|
||||
sessiontarget: AnyCodable,
|
||||
wakemode: AnyCodable,
|
||||
payload: AnyCodable,
|
||||
delivery: [String: AnyCodable]?,
|
||||
delivery: AnyCodable?,
|
||||
state: [String: AnyCodable]
|
||||
) {
|
||||
self.id = id
|
||||
@@ -2172,7 +2172,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
public let sessiontarget: AnyCodable
|
||||
public let wakemode: AnyCodable
|
||||
public let payload: AnyCodable
|
||||
public let delivery: [String: AnyCodable]?
|
||||
public let delivery: AnyCodable?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
@@ -2184,7 +2184,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
sessiontarget: AnyCodable,
|
||||
wakemode: AnyCodable,
|
||||
payload: AnyCodable,
|
||||
delivery: [String: AnyCodable]?
|
||||
delivery: AnyCodable?
|
||||
) {
|
||||
self.name = name
|
||||
self.agentid = agentid
|
||||
|
||||
@@ -27,6 +27,8 @@ Troubleshooting: [/automation/troubleshooting](/automation/troubleshooting)
|
||||
- **Main session**: enqueue a system event, then run on the next heartbeat.
|
||||
- **Isolated**: run a dedicated agent turn in `cron:<jobId>`, with delivery (announce by default or none).
|
||||
- Wakeups are first-class: a job can request “wake now” vs “next heartbeat”.
|
||||
- Webhook posting is per job via `delivery.mode = "webhook"` + `delivery.to = "<url>"`.
|
||||
- Legacy fallback remains for stored jobs with `notify: true` when `cron.webhook` is set, migrate those jobs to webhook delivery mode.
|
||||
|
||||
## Quick start (actionable)
|
||||
|
||||
@@ -99,7 +101,7 @@ A cron job is a stored record with:
|
||||
|
||||
- a **schedule** (when it should run),
|
||||
- a **payload** (what it should do),
|
||||
- optional **delivery mode** (announce or none).
|
||||
- optional **delivery mode** (`announce`, `webhook`, or `none`).
|
||||
- optional **agent binding** (`agentId`): run the job under a specific agent; if
|
||||
missing or unknown, the gateway falls back to the default agent.
|
||||
|
||||
@@ -140,8 +142,9 @@ Key behaviors:
|
||||
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
|
||||
- Each run starts a **fresh session id** (no prior conversation carry-over).
|
||||
- Default behavior: if `delivery` is omitted, isolated jobs announce a summary (`delivery.mode = "announce"`).
|
||||
- `delivery.mode` (isolated-only) chooses what happens:
|
||||
- `delivery.mode` chooses what happens:
|
||||
- `announce`: deliver a summary to the target channel and post a brief summary to the main session.
|
||||
- `webhook`: POST the finished event payload to `delivery.to`.
|
||||
- `none`: internal only (no delivery, no main-session summary).
|
||||
- `wakeMode` controls when the main-session summary posts:
|
||||
- `now`: immediate heartbeat.
|
||||
@@ -163,11 +166,11 @@ Common `agentTurn` fields:
|
||||
- `model` / `thinking`: optional overrides (see below).
|
||||
- `timeoutSeconds`: optional timeout override.
|
||||
|
||||
Delivery config (isolated jobs only):
|
||||
Delivery config:
|
||||
|
||||
- `delivery.mode`: `none` | `announce`.
|
||||
- `delivery.mode`: `none` | `announce` | `webhook`.
|
||||
- `delivery.channel`: `last` or a specific channel.
|
||||
- `delivery.to`: channel-specific target (phone/chat/channel id).
|
||||
- `delivery.to`: channel-specific target (announce) or webhook URL (webhook mode).
|
||||
- `delivery.bestEffort`: avoid failing the job if announce delivery fails.
|
||||
|
||||
Announce delivery suppresses messaging tool sends for the run; use `delivery.channel`/`delivery.to`
|
||||
@@ -192,6 +195,18 @@ Behavior details:
|
||||
- The main-session summary respects `wakeMode`: `now` triggers an immediate heartbeat and
|
||||
`next-heartbeat` waits for the next scheduled heartbeat.
|
||||
|
||||
#### Webhook delivery flow
|
||||
|
||||
When `delivery.mode = "webhook"`, cron posts the finished event payload to `delivery.to`.
|
||||
|
||||
Behavior details:
|
||||
|
||||
- The endpoint must be a valid HTTP(S) URL.
|
||||
- No channel delivery is attempted in webhook mode.
|
||||
- No main-session summary is posted in webhook mode.
|
||||
- If `cron.webhookToken` is set, auth header is `Authorization: Bearer <cron.webhookToken>`.
|
||||
- Deprecated fallback: stored legacy jobs with `notify: true` still post to `cron.webhook` (if configured), with a warning so you can migrate to `delivery.mode = "webhook"`.
|
||||
|
||||
### Model and thinking overrides
|
||||
|
||||
Isolated jobs (`agentTurn`) can override the model and thinking level:
|
||||
@@ -213,11 +228,12 @@ Resolution priority:
|
||||
|
||||
Isolated jobs can deliver output to a channel via the top-level `delivery` config:
|
||||
|
||||
- `delivery.mode`: `announce` (deliver a summary) or `none`.
|
||||
- `delivery.mode`: `announce` (channel delivery), `webhook` (HTTP POST), or `none`.
|
||||
- `delivery.channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last`.
|
||||
- `delivery.to`: channel-specific recipient target.
|
||||
|
||||
Delivery config is only valid for isolated jobs (`sessionTarget: "isolated"`).
|
||||
`announce` delivery is only valid for isolated jobs (`sessionTarget: "isolated"`).
|
||||
`webhook` delivery is valid for both main and isolated jobs.
|
||||
|
||||
If `delivery.channel` or `delivery.to` is omitted, cron can fall back to the main session’s
|
||||
“last route” (the last place the agent replied).
|
||||
@@ -333,10 +349,21 @@ Notes:
|
||||
enabled: true, // default true
|
||||
store: "~/.openclaw/cron/jobs.json",
|
||||
maxConcurrentRuns: 1, // default 1
|
||||
webhook: "https://example.invalid/legacy", // deprecated fallback for stored notify:true jobs
|
||||
webhookToken: "replace-with-dedicated-webhook-token", // optional bearer token for webhook mode
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Webhook behavior:
|
||||
|
||||
- Preferred: set `delivery.mode: "webhook"` with `delivery.to: "https://..."` per job.
|
||||
- Webhook URLs must be valid `http://` or `https://` URLs.
|
||||
- Payload is the cron finished event JSON.
|
||||
- If `cron.webhookToken` is set, auth header is `Authorization: Bearer <cron.webhookToken>`.
|
||||
- If `cron.webhookToken` is not set, no `Authorization` header is sent.
|
||||
- Deprecated fallback: stored legacy jobs with `notify: true` still use `cron.webhook` when present.
|
||||
|
||||
Disable cron entirely:
|
||||
|
||||
- `cron.enabled: false` (config)
|
||||
|
||||
@@ -87,6 +87,77 @@ Token resolution is account-aware. Config token values win over env fallback. `D
|
||||
- Group DMs are ignored by default (`channels.discord.dm.groupEnabled=false`).
|
||||
- Native slash commands run in isolated command sessions (`agent:<agentId>:discord:slash:<userId>`), while still carrying `CommandTargetSessionKey` to the routed conversation session.
|
||||
|
||||
## Interactive components
|
||||
|
||||
OpenClaw supports Discord components v2 containers for agent messages. Use the message tool with a `components` payload. Interaction results are routed back to the agent as normal inbound messages and follow the existing Discord `replyToMode` settings.
|
||||
|
||||
Supported blocks:
|
||||
|
||||
- `text`, `section`, `separator`, `actions`, `media-gallery`, `file`
|
||||
- Action rows allow up to 5 buttons or a single select menu
|
||||
- Select types: `string`, `user`, `role`, `mentionable`, `channel`
|
||||
|
||||
File attachments:
|
||||
|
||||
- `file` blocks must point to an attachment reference (`attachment://<filename>`)
|
||||
- Provide the attachment via `media`/`path`/`filePath` (single file); use `media-gallery` for multiple files
|
||||
- Use `filename` to override the upload name when it should match the attachment reference
|
||||
|
||||
Modal forms:
|
||||
|
||||
- Add `components.modal` with up to 5 fields
|
||||
- Field types: `text`, `checkbox`, `radio`, `select`, `role-select`, `user-select`
|
||||
- OpenClaw adds a trigger button automatically
|
||||
|
||||
Example:
|
||||
|
||||
```json5
|
||||
{
|
||||
channel: "discord",
|
||||
action: "send",
|
||||
to: "channel:123456789012345678",
|
||||
message: "Optional fallback text",
|
||||
components: {
|
||||
text: "Choose a path",
|
||||
blocks: [
|
||||
{
|
||||
type: "actions",
|
||||
buttons: [
|
||||
{ label: "Approve", style: "success" },
|
||||
{ label: "Decline", style: "danger" },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
select: {
|
||||
type: "string",
|
||||
placeholder: "Pick an option",
|
||||
options: [
|
||||
{ label: "Option A", value: "a" },
|
||||
{ label: "Option B", value: "b" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
modal: {
|
||||
title: "Details",
|
||||
triggerLabel: "Open form",
|
||||
fields: [
|
||||
{ type: "text", label: "Requester" },
|
||||
{
|
||||
type: "select",
|
||||
label: "Priority",
|
||||
options: [
|
||||
{ label: "Low", value: "low" },
|
||||
{ label: "High", value: "high" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Access control and routing
|
||||
|
||||
<Tabs>
|
||||
|
||||
@@ -105,7 +105,7 @@ Want “groups can only see folder X” instead of “no host access”? Keep `w
|
||||
docker: {
|
||||
binds: [
|
||||
// hostPath:containerPath:mode
|
||||
"~/FriendsShared:/data:ro",
|
||||
"/home/user/FriendsShared:/data:ro",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -176,12 +176,24 @@ Behavior:
|
||||
|
||||
## Sandbox Session Visibility
|
||||
|
||||
Sandboxed sessions can use session tools, but by default they only see sessions they spawned via `sessions_spawn`.
|
||||
Session tools can be scoped to reduce cross-session access.
|
||||
|
||||
Default behavior:
|
||||
|
||||
- `tools.sessions.visibility` defaults to `tree` (current session + spawned subagent sessions).
|
||||
- For sandboxed sessions, `agents.defaults.sandbox.sessionToolsVisibility` can hard-clamp visibility.
|
||||
|
||||
Config:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
sessions: {
|
||||
// "self" | "tree" | "agent" | "all"
|
||||
// default: "tree"
|
||||
visibility: "tree",
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
@@ -192,3 +204,11 @@ Config:
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `self`: only the current session key.
|
||||
- `tree`: current session + sessions spawned by the current session.
|
||||
- `agent`: any session belonging to the current agent id.
|
||||
- `all`: any session (cross-agent access still requires `tools.agentToAgent`).
|
||||
- When a session is sandboxed and `sessionToolsVisibility="spawned"`, OpenClaw clamps visibility to `tree` even if you set `tools.sessions.visibility="all"`.
|
||||
|
||||
192
docs/experiments/plans/pty-process-supervision.md
Normal file
192
docs/experiments/plans/pty-process-supervision.md
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
summary: "Production plan for reliable interactive process supervision (PTY + non-PTY) with explicit ownership, unified lifecycle, and deterministic cleanup"
|
||||
owner: "openclaw"
|
||||
status: "in-progress"
|
||||
last_updated: "2026-02-15"
|
||||
title: "PTY and Process Supervision Plan"
|
||||
---
|
||||
|
||||
# PTY and Process Supervision Plan
|
||||
|
||||
## 1. Problem and goal
|
||||
|
||||
We need one reliable lifecycle for long-running command execution across:
|
||||
|
||||
- `exec` foreground runs
|
||||
- `exec` background runs
|
||||
- `process` follow up actions (`poll`, `log`, `send-keys`, `paste`, `submit`, `kill`, `remove`)
|
||||
- CLI agent runner subprocesses
|
||||
|
||||
The goal is not just to support PTY. The goal is predictable ownership, cancellation, timeout, and cleanup with no unsafe process matching heuristics.
|
||||
|
||||
## 2. Scope and boundaries
|
||||
|
||||
- Keep implementation internal in `src/process/supervisor`.
|
||||
- Do not create a new package for this.
|
||||
- Keep current behavior compatibility where practical.
|
||||
- Do not broaden scope to terminal replay or tmux style session persistence.
|
||||
|
||||
## 3. Implemented in this branch
|
||||
|
||||
### Supervisor baseline already present
|
||||
|
||||
- Supervisor module is in place under `src/process/supervisor/*`.
|
||||
- Exec runtime and CLI runner are already routed through supervisor spawn and wait.
|
||||
- Registry finalization is idempotent.
|
||||
|
||||
### This pass completed
|
||||
|
||||
1. Explicit PTY command contract
|
||||
|
||||
- `SpawnInput` is now a discriminated union in `src/process/supervisor/types.ts`.
|
||||
- PTY runs require `ptyCommand` instead of reusing generic `argv`.
|
||||
- Supervisor no longer rebuilds PTY command strings from argv joins in `src/process/supervisor/supervisor.ts`.
|
||||
- Exec runtime now passes `ptyCommand` directly in `src/agents/bash-tools.exec-runtime.ts`.
|
||||
|
||||
2. Process layer type decoupling
|
||||
|
||||
- Supervisor types no longer import `SessionStdin` from agents.
|
||||
- Process local stdin contract lives in `src/process/supervisor/types.ts` (`ManagedRunStdin`).
|
||||
- Adapters now depend only on process level types:
|
||||
- `src/process/supervisor/adapters/child.ts`
|
||||
- `src/process/supervisor/adapters/pty.ts`
|
||||
|
||||
3. Process tool lifecycle ownership improvement
|
||||
|
||||
- `src/agents/bash-tools.process.ts` now requests cancellation through supervisor first.
|
||||
- `process kill/remove` now use process-tree fallback termination when supervisor lookup misses.
|
||||
- `remove` keeps deterministic remove behavior by dropping running session entries immediately after termination is requested.
|
||||
|
||||
4. Single source watchdog defaults
|
||||
|
||||
- Added shared defaults in `src/agents/cli-watchdog-defaults.ts`.
|
||||
- `src/agents/cli-backends.ts` consumes the shared defaults.
|
||||
- `src/agents/cli-runner/reliability.ts` consumes the same shared defaults.
|
||||
|
||||
5. Dead helper cleanup
|
||||
|
||||
- Removed unused `killSession` helper path from `src/agents/bash-tools.shared.ts`.
|
||||
|
||||
6. Direct supervisor path tests added
|
||||
|
||||
- Added `src/agents/bash-tools.process.supervisor.test.ts` to cover kill and remove routing through supervisor cancellation.
|
||||
|
||||
7. Reliability gap fixes completed
|
||||
|
||||
- `src/agents/bash-tools.process.ts` now falls back to real OS-level process termination when supervisor lookup misses.
|
||||
- `src/process/supervisor/adapters/child.ts` now uses process-tree termination semantics for default cancel/timeout kill paths.
|
||||
- Added shared process-tree utility in `src/process/kill-tree.ts`.
|
||||
|
||||
8. PTY contract edge-case coverage added
|
||||
|
||||
- Added `src/process/supervisor/supervisor.pty-command.test.ts` for verbatim PTY command forwarding and empty-command rejection.
|
||||
- Added `src/process/supervisor/adapters/child.test.ts` for process-tree kill behavior in child adapter cancellation.
|
||||
|
||||
## 4. Remaining gaps and decisions
|
||||
|
||||
### Reliability status
|
||||
|
||||
The two required reliability gaps for this pass are now closed:
|
||||
|
||||
- `process kill/remove` now has a real OS termination fallback when supervisor lookup misses.
|
||||
- child cancel/timeout now uses process-tree kill semantics for default kill path.
|
||||
- Regression tests were added for both behaviors.
|
||||
|
||||
### Durability and startup reconciliation
|
||||
|
||||
Restart behavior is now explicitly defined as in-memory lifecycle only.
|
||||
|
||||
- `reconcileOrphans()` remains a no-op in `src/process/supervisor/supervisor.ts` by design.
|
||||
- Active runs are not recovered after process restart.
|
||||
- This boundary is intentional for this implementation pass to avoid partial persistence risks.
|
||||
|
||||
### Maintainability follow-ups
|
||||
|
||||
1. `runExecProcess` in `src/agents/bash-tools.exec-runtime.ts` still handles multiple responsibilities and can be split into focused helpers in a follow-up.
|
||||
|
||||
## 5. Implementation plan
|
||||
|
||||
The implementation pass for required reliability and contract items is complete.
|
||||
|
||||
Completed:
|
||||
|
||||
- `process kill/remove` fallback real termination
|
||||
- process-tree cancellation for child adapter default kill path
|
||||
- regression tests for fallback kill and child adapter kill path
|
||||
- PTY command edge-case tests under explicit `ptyCommand`
|
||||
- explicit in-memory restart boundary with `reconcileOrphans()` no-op by design
|
||||
|
||||
Optional follow-up:
|
||||
|
||||
- split `runExecProcess` into focused helpers with no behavior drift
|
||||
|
||||
## 6. File map
|
||||
|
||||
### Process supervisor
|
||||
|
||||
- `src/process/supervisor/types.ts` updated with discriminated spawn input and process local stdin contract.
|
||||
- `src/process/supervisor/supervisor.ts` updated to use explicit `ptyCommand`.
|
||||
- `src/process/supervisor/adapters/child.ts` and `src/process/supervisor/adapters/pty.ts` decoupled from agent types.
|
||||
- `src/process/supervisor/registry.ts` idempotent finalize unchanged and retained.
|
||||
|
||||
### Exec and process integration
|
||||
|
||||
- `src/agents/bash-tools.exec-runtime.ts` updated to pass PTY command explicitly and keep fallback path.
|
||||
- `src/agents/bash-tools.process.ts` updated to cancel via supervisor with real process-tree fallback termination.
|
||||
- `src/agents/bash-tools.shared.ts` removed direct kill helper path.
|
||||
|
||||
### CLI reliability
|
||||
|
||||
- `src/agents/cli-watchdog-defaults.ts` added as shared baseline.
|
||||
- `src/agents/cli-backends.ts` and `src/agents/cli-runner/reliability.ts` now consume same defaults.
|
||||
|
||||
## 7. Validation run in this pass
|
||||
|
||||
Unit tests:
|
||||
|
||||
- `pnpm vitest src/process/supervisor/registry.test.ts`
|
||||
- `pnpm vitest src/process/supervisor/supervisor.test.ts`
|
||||
- `pnpm vitest src/process/supervisor/supervisor.pty-command.test.ts`
|
||||
- `pnpm vitest src/process/supervisor/adapters/child.test.ts`
|
||||
- `pnpm vitest src/agents/cli-backends.test.ts`
|
||||
- `pnpm vitest src/agents/bash-tools.exec.pty-cleanup.test.ts`
|
||||
- `pnpm vitest src/agents/bash-tools.process.poll-timeout.test.ts`
|
||||
- `pnpm vitest src/agents/bash-tools.process.supervisor.test.ts`
|
||||
- `pnpm vitest src/process/exec.test.ts`
|
||||
|
||||
E2E targets:
|
||||
|
||||
- `pnpm test:e2e src/agents/cli-runner.e2e.test.ts`
|
||||
- `pnpm test:e2e src/agents/bash-tools.exec.pty-fallback.e2e.test.ts src/agents/bash-tools.exec.background-abort.e2e.test.ts src/agents/bash-tools.process.send-keys.e2e.test.ts`
|
||||
|
||||
Typecheck note:
|
||||
|
||||
- `pnpm tsgo` currently fails in this repo due to a pre-existing UI typing dependency issue (`@vitest/browser-playwright` resolution), unrelated to this process supervision work.
|
||||
|
||||
## 8. Operational guarantees preserved
|
||||
|
||||
- Exec env hardening behavior is unchanged.
|
||||
- Approval and allowlist flow is unchanged.
|
||||
- Output sanitization and output caps are unchanged.
|
||||
- PTY adapter still guarantees wait settlement on forced kill and listener disposal.
|
||||
|
||||
## 9. Definition of done
|
||||
|
||||
1. Supervisor is lifecycle owner for managed runs.
|
||||
2. PTY spawn uses explicit command contract with no argv reconstruction.
|
||||
3. Process layer has no type dependency on agent layer for supervisor stdin contracts.
|
||||
4. Watchdog defaults are single source.
|
||||
5. Targeted unit and e2e tests remain green.
|
||||
6. Restart durability boundary is explicitly documented or fully implemented.
|
||||
|
||||
## 10. Summary
|
||||
|
||||
The branch now has a coherent and safer supervision shape:
|
||||
|
||||
- explicit PTY contract
|
||||
- cleaner process layering
|
||||
- supervisor driven cancellation path for process operations
|
||||
- real fallback termination when supervisor lookup misses
|
||||
- process-tree cancellation for child-run default kill paths
|
||||
- unified watchdog defaults
|
||||
- explicit in-memory restart boundary (no orphan reconciliation across restart in this pass)
|
||||
@@ -1508,6 +1508,31 @@ Provider auth follows standard order: auth profiles → env vars → `models.pro
|
||||
}
|
||||
```
|
||||
|
||||
### `tools.sessions`
|
||||
|
||||
Controls which sessions can be targeted by the session tools (`sessions_list`, `sessions_history`, `sessions_send`).
|
||||
|
||||
Default: `tree` (current session + sessions spawned by it, such as subagents).
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
sessions: {
|
||||
// "self" | "tree" | "agent" | "all"
|
||||
visibility: "tree",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `self`: only the current session key.
|
||||
- `tree`: current session + sessions spawned by the current session (subagents).
|
||||
- `agent`: any session belonging to the current agent id (can include other users if you run per-sender sessions under the same agent id).
|
||||
- `all`: any session. Cross-agent targeting still requires `tools.agentToAgent`.
|
||||
- Sandbox clamp: when the current session is sandboxed and `agents.defaults.sandbox.sessionToolsVisibility="spawned"`, visibility is forced to `tree` even if `tools.sessions.visibility="all"`.
|
||||
|
||||
### `tools.subagents`
|
||||
|
||||
```json5
|
||||
@@ -2295,12 +2320,16 @@ Current builds no longer include the TCP bridge. Nodes connect over the Gateway
|
||||
cron: {
|
||||
enabled: true,
|
||||
maxConcurrentRuns: 2,
|
||||
webhook: "https://example.invalid/legacy", // deprecated fallback for stored notify:true jobs
|
||||
webhookToken: "replace-with-dedicated-token", // optional bearer token for outbound webhook auth
|
||||
sessionRetention: "24h", // duration string or false
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `sessionRetention`: how long to keep completed cron sessions before pruning. Default: `24h`.
|
||||
- `webhookToken`: bearer token used for cron webhook POST delivery (`delivery.mode = "webhook"`), if omitted no auth header is sent.
|
||||
- `webhook`: deprecated legacy fallback webhook URL (http/https) used only for stored jobs that still have `notify: true`.
|
||||
|
||||
See [Cron Jobs](/automation/cron-jobs).
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ Global and per-agent binds are **merged** (not replaced). Under `scope: "shared"
|
||||
- When set (including `[]`), it replaces `agents.defaults.sandbox.docker.binds` for the browser container.
|
||||
- When omitted, the browser container falls back to `agents.defaults.sandbox.docker.binds` (backwards compatible).
|
||||
|
||||
Example (read-only source + docker socket):
|
||||
Example (read-only source + an extra data directory):
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -84,7 +84,7 @@ Example (read-only source + docker socket):
|
||||
defaults: {
|
||||
sandbox: {
|
||||
docker: {
|
||||
binds: ["/home/user/source:/source:ro", "/var/run/docker.sock:/var/run/docker.sock"],
|
||||
binds: ["/home/user/source:/source:ro", "/var/data/myapp:/data:ro"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -105,7 +105,8 @@ Example (read-only source + docker socket):
|
||||
Security notes:
|
||||
|
||||
- Binds bypass the sandbox filesystem: they expose host paths with whatever mode you set (`:ro` or `:rw`).
|
||||
- Sensitive mounts (e.g., `docker.sock`, secrets, SSH keys) should be `:ro` unless absolutely required.
|
||||
- OpenClaw blocks dangerous bind sources (for example: `docker.sock`, `/etc`, `/proc`, `/sys`, `/dev`, and parent mounts that would expose them).
|
||||
- Sensitive mounts (secrets, SSH keys, service credentials) should be `:ro` unless absolutely required.
|
||||
- Combine with `workspaceAccess: "ro"` if you only need read access to the workspace; bind modes stay independent.
|
||||
- See [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) for how binds interact with tool policy and elevated exec.
|
||||
|
||||
|
||||
@@ -710,7 +710,11 @@ Common use cases:
|
||||
scope: "agent",
|
||||
workspaceAccess: "none",
|
||||
},
|
||||
// Session tools can reveal sensitive data from transcripts. By default OpenClaw limits these tools
|
||||
// to the current session + spawned subagent sessions, but you can clamp further if needed.
|
||||
// See `tools.sessions.visibility` in the configuration reference.
|
||||
tools: {
|
||||
sessions: { visibility: "tree" }, // self | tree | agent | all
|
||||
allow: [
|
||||
"sessions_list",
|
||||
"sessions_history",
|
||||
|
||||
@@ -34,17 +34,17 @@ Notes:
|
||||
# From repo root; set release IDs so Sparkle feed is enabled.
|
||||
# APP_BUILD must be numeric + monotonic for Sparkle compare.
|
||||
BUNDLE_ID=bot.molt.mac \
|
||||
APP_VERSION=2026.2.15 \
|
||||
APP_VERSION=2026.2.16 \
|
||||
APP_BUILD="$(git rev-list --count HEAD)" \
|
||||
BUILD_CONFIG=release \
|
||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||
scripts/package-mac-app.sh
|
||||
|
||||
# Zip for distribution (includes resource forks for Sparkle delta support)
|
||||
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app dist/OpenClaw-2026.2.15.zip
|
||||
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app dist/OpenClaw-2026.2.16.zip
|
||||
|
||||
# Optional: also build a styled DMG for humans (drag to /Applications)
|
||||
scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.2.15.dmg
|
||||
scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.2.16.dmg
|
||||
|
||||
# Recommended: build + notarize/staple zip + DMG
|
||||
# First, create a keychain profile once:
|
||||
@@ -52,14 +52,14 @@ scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.2.15.dmg
|
||||
# --apple-id "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>"
|
||||
NOTARIZE=1 NOTARYTOOL_PROFILE=openclaw-notary \
|
||||
BUNDLE_ID=bot.molt.mac \
|
||||
APP_VERSION=2026.2.15 \
|
||||
APP_VERSION=2026.2.16 \
|
||||
APP_BUILD="$(git rev-list --count HEAD)" \
|
||||
BUILD_CONFIG=release \
|
||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||
scripts/package-mac-dist.sh
|
||||
|
||||
# Optional: ship dSYM alongside the release
|
||||
ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenClaw-2026.2.15.dSYM.zip
|
||||
ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenClaw-2026.2.16.dSYM.zip
|
||||
```
|
||||
|
||||
## Appcast entry
|
||||
@@ -67,7 +67,7 @@ ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenCl
|
||||
Use the release note generator so Sparkle renders formatted HTML notes:
|
||||
|
||||
```bash
|
||||
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/OpenClaw-2026.2.15.zip https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml
|
||||
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/OpenClaw-2026.2.16.zip https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml
|
||||
```
|
||||
|
||||
Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry.
|
||||
@@ -75,7 +75,7 @@ Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when
|
||||
|
||||
## Publish & verify
|
||||
|
||||
- Upload `OpenClaw-2026.2.15.zip` (and `OpenClaw-2026.2.15.dSYM.zip`) to the GitHub release for tag `v2026.2.15`.
|
||||
- Upload `OpenClaw-2026.2.16.zip` (and `OpenClaw-2026.2.16.dSYM.zip`) to the GitHub release for tag `v2026.2.16`.
|
||||
- Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml`.
|
||||
- Sanity checks:
|
||||
- `curl -I https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml` returns 200.
|
||||
|
||||
@@ -442,12 +442,14 @@ Notes:
|
||||
|
||||
- `main` is the canonical direct-chat key; global/unknown are hidden.
|
||||
- `messageLimit > 0` fetches last N messages per session (tool messages filtered).
|
||||
- Session targeting is controlled by `tools.sessions.visibility` (default `tree`: current session + spawned subagent sessions). If you run a shared agent for multiple users, consider setting `tools.sessions.visibility: "self"` to prevent cross-session browsing.
|
||||
- `sessions_send` waits for final completion when `timeoutSeconds > 0`.
|
||||
- Delivery/announce happens after completion and is best-effort; `status: "ok"` confirms the agent run finished, not that the announce was delivered.
|
||||
- `sessions_spawn` starts a sub-agent run and posts an announce reply back to the requester chat.
|
||||
- `sessions_spawn` is non-blocking and returns `status: "accepted"` immediately.
|
||||
- `sessions_send` runs a reply‑back ping‑pong (reply `REPLY_SKIP` to stop; max turns via `session.agentToAgent.maxPingPongTurns`, 0–5).
|
||||
- After the ping‑pong, the target agent runs an **announce step**; reply `ANNOUNCE_SKIP` to suppress the announcement.
|
||||
- Sandbox clamp: when the current session is sandboxed and `agents.defaults.sandbox.sessionToolsVisibility: "spawned"`, OpenClaw clamps `tools.sessions.visibility` to `tree`.
|
||||
|
||||
### `agents_list`
|
||||
|
||||
|
||||
@@ -324,6 +324,7 @@ Legacy `agent.*` configs are migrated by `openclaw doctor`; prefer `agents.defau
|
||||
```json
|
||||
{
|
||||
"tools": {
|
||||
"sessions": { "visibility": "tree" },
|
||||
"allow": ["sessions_list", "sessions_send", "sessions_history", "session_status"],
|
||||
"deny": ["exec", "write", "edit", "apply_patch", "read", "browser"]
|
||||
}
|
||||
|
||||
@@ -224,6 +224,7 @@ Fetch a URL and extract readable content.
|
||||
enabled: true,
|
||||
maxChars: 50000,
|
||||
maxCharsCap: 50000,
|
||||
maxResponseBytes: 2000000,
|
||||
timeoutSeconds: 30,
|
||||
cacheTtlMinutes: 15,
|
||||
maxRedirects: 3,
|
||||
@@ -256,6 +257,7 @@ Notes:
|
||||
- `web_fetch` sends a Chrome-like User-Agent and `Accept-Language` by default; override `userAgent` if needed.
|
||||
- `web_fetch` blocks private/internal hostnames and re-checks redirects (limit with `maxRedirects`).
|
||||
- `maxChars` is clamped to `tools.web.fetch.maxCharsCap`.
|
||||
- `web_fetch` caps the downloaded response body size to `tools.web.fetch.maxResponseBytes` before parsing; oversized responses are truncated and include a warning.
|
||||
- `web_fetch` is best-effort extraction; some sites will need the browser tool.
|
||||
- See [Firecrawl](/tools/firecrawl) for key setup and service details.
|
||||
- Responses are cached (default 15 minutes) to reduce repeated fetches.
|
||||
|
||||
@@ -83,6 +83,10 @@ Cron jobs panel notes:
|
||||
|
||||
- For isolated jobs, delivery defaults to announce summary. You can switch to none if you want internal-only runs.
|
||||
- Channel/target fields appear when announce is selected.
|
||||
- Webhook mode uses `delivery.mode = "webhook"` with `delivery.to` set to a valid HTTP(S) webhook URL.
|
||||
- For main-session jobs, webhook and none delivery modes are available.
|
||||
- Set `cron.webhookToken` to send a dedicated bearer token, if omitted the webhook is sent without an auth header.
|
||||
- Deprecated fallback: stored legacy jobs with `notify: true` can still use `cron.webhook` until migrated.
|
||||
|
||||
## Chat behavior
|
||||
|
||||
@@ -93,6 +97,10 @@ Cron jobs panel notes:
|
||||
- Click **Stop** (calls `chat.abort`)
|
||||
- Type `/stop` (or `stop|esc|abort|wait|exit|interrupt`) to abort out-of-band
|
||||
- `chat.abort` supports `{ sessionKey }` (no `runId`) to abort all active runs for that session
|
||||
- Abort partial retention:
|
||||
- When a run is aborted, partial assistant text can still be shown in the UI
|
||||
- Gateway persists aborted partial assistant text into transcript history when buffered output exists
|
||||
- Persisted entries include abort metadata so transcript consumers can tell abort partials from normal completion output
|
||||
|
||||
## Tailnet access (recommended)
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket.
|
||||
|
||||
- The UI connects to the Gateway WebSocket and uses `chat.history`, `chat.send`, and `chat.inject`.
|
||||
- `chat.inject` appends an assistant note directly to the transcript and broadcasts it to the UI (no agent run).
|
||||
- Aborted runs can keep partial assistant output visible in the UI.
|
||||
- Gateway persists aborted partial assistant text into transcript history when buffered output exists, and marks those entries with abort metadata.
|
||||
- History is always fetched from the gateway (no local file watching).
|
||||
- If the gateway is unreachable, WebChat is read-only.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/bluebubbles",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw BlueBubbles channel plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/copilot-proxy",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw Copilot Proxy provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-otel",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw diagnostics OpenTelemetry exporter",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/discord",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw Discord channel plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -158,6 +158,12 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
threading: {
|
||||
resolveReplyToMode: ({ cfg }) => cfg.channels?.discord?.replyToMode ?? "off",
|
||||
},
|
||||
agentPrompt: {
|
||||
messageToolHints: () => [
|
||||
"- Discord components: set `components` when sending messages to include buttons, selects, or v2 containers.",
|
||||
"- Forms: add `components.modal` (title, fields). OpenClaw adds a trigger button and routes submissions as new messages.",
|
||||
],
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeDiscordMessagingTarget,
|
||||
targetResolver: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/feishu",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/google-antigravity-auth",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw Google Antigravity OAuth provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/google-gemini-cli-auth",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw Gemini CLI OAuth provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/googlechat",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw Google Chat channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/imessage",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw iMessage channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/irc",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw IRC channel plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/line",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw LINE channel plugin",
|
||||
"type": "module",
|
||||
|
||||
101
extensions/line/src/channel.startup.test.ts
Normal file
101
extensions/line/src/channel.startup.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { linePlugin } from "./channel.js";
|
||||
import { setLineRuntime } from "./runtime.js";
|
||||
|
||||
function createRuntime() {
|
||||
const probeLineBot = vi.fn(async () => ({ ok: false }));
|
||||
const monitorLineProvider = vi.fn(async () => ({
|
||||
account: { accountId: "default" },
|
||||
handleWebhook: async () => {},
|
||||
stop: () => {},
|
||||
}));
|
||||
|
||||
const runtime = {
|
||||
channel: {
|
||||
line: {
|
||||
probeLineBot,
|
||||
monitorLineProvider,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
shouldLogVerbose: () => false,
|
||||
},
|
||||
} as unknown as PluginRuntime;
|
||||
|
||||
return { runtime, probeLineBot, monitorLineProvider };
|
||||
}
|
||||
|
||||
function createStartAccountCtx(params: { token: string; secret: string; runtime: unknown }) {
|
||||
return {
|
||||
account: {
|
||||
accountId: "default",
|
||||
channelAccessToken: params.token,
|
||||
channelSecret: params.secret,
|
||||
config: {},
|
||||
},
|
||||
cfg: {} as OpenClawConfig,
|
||||
runtime: params.runtime,
|
||||
abortSignal: undefined,
|
||||
log: { info: vi.fn(), debug: vi.fn() },
|
||||
};
|
||||
}
|
||||
|
||||
describe("linePlugin gateway.startAccount", () => {
|
||||
it("fails startup when channel secret is missing", async () => {
|
||||
const { runtime, monitorLineProvider } = createRuntime();
|
||||
setLineRuntime(runtime);
|
||||
|
||||
await expect(
|
||||
linePlugin.gateway.startAccount(
|
||||
createStartAccountCtx({
|
||||
token: "token",
|
||||
secret: " ",
|
||||
runtime: {},
|
||||
}) as never,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'LINE webhook mode requires a non-empty channel secret for account "default".',
|
||||
);
|
||||
expect(monitorLineProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails startup when channel access token is missing", async () => {
|
||||
const { runtime, monitorLineProvider } = createRuntime();
|
||||
setLineRuntime(runtime);
|
||||
|
||||
await expect(
|
||||
linePlugin.gateway.startAccount(
|
||||
createStartAccountCtx({
|
||||
token: " ",
|
||||
secret: "secret",
|
||||
runtime: {},
|
||||
}) as never,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'LINE webhook mode requires a non-empty channel access token for account "default".',
|
||||
);
|
||||
expect(monitorLineProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("starts provider when token and secret are present", async () => {
|
||||
const { runtime, monitorLineProvider } = createRuntime();
|
||||
setLineRuntime(runtime);
|
||||
|
||||
await linePlugin.gateway.startAccount(
|
||||
createStartAccountCtx({
|
||||
token: "token",
|
||||
secret: "secret",
|
||||
runtime: {},
|
||||
}) as never,
|
||||
);
|
||||
|
||||
expect(monitorLineProvider).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channelAccessToken: "token",
|
||||
channelSecret: "secret",
|
||||
accountId: "default",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -119,12 +119,13 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
|
||||
},
|
||||
};
|
||||
},
|
||||
isConfigured: (account) => Boolean(account.channelAccessToken?.trim()),
|
||||
isConfigured: (account) =>
|
||||
Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
enabled: account.enabled,
|
||||
configured: Boolean(account.channelAccessToken?.trim()),
|
||||
configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
|
||||
tokenSource: account.tokenSource ?? undefined,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||
@@ -603,7 +604,9 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
|
||||
probeAccount: async ({ account, timeoutMs }) =>
|
||||
getLineRuntime().channel.line.probeLineBot(account.channelAccessToken, timeoutMs),
|
||||
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
||||
const configured = Boolean(account.channelAccessToken?.trim());
|
||||
const configured = Boolean(
|
||||
account.channelAccessToken?.trim() && account.channelSecret?.trim(),
|
||||
);
|
||||
return {
|
||||
accountId: account.accountId,
|
||||
name: account.name,
|
||||
@@ -626,6 +629,16 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
|
||||
const account = ctx.account;
|
||||
const token = account.channelAccessToken.trim();
|
||||
const secret = account.channelSecret.trim();
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
`LINE webhook mode requires a non-empty channel access token for account "${account.accountId}".`,
|
||||
);
|
||||
}
|
||||
if (!secret) {
|
||||
throw new Error(
|
||||
`LINE webhook mode requires a non-empty channel secret for account "${account.accountId}".`,
|
||||
);
|
||||
}
|
||||
|
||||
let lineBotLabel = "";
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/llm-task",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw JSON-only LLM task plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/lobster",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.2.16
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.2.15
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/matrix",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw Matrix channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/mattermost",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw Mattermost channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/memory-core",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw core memory search plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/memory-lancedb",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw LanceDB-backed long-term memory plugin with auto-recall/capture",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/minimax-portal-auth",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw MiniMax Portal OAuth provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.2.16
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.2.15
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/msteams",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw Microsoft Teams channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/nextcloud-talk",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw Nextcloud Talk channel plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.2.16
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.2.15
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/nostr",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/open-prose",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenProse VM skill pack plugin (slash command + telemetry).",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/signal",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw Signal channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/slack",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw Slack channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/telegram",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw Telegram channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/tlon",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw Tlon/Urbit channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.2.16
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.2.15
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/twitch",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw Twitch channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.2.16
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.2.15
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/voice-call",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw voice-call plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/whatsapp",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw WhatsApp channel plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.2.16
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.2.15
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/zalo",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw Zalo channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.2.16
|
||||
|
||||
### Changes
|
||||
|
||||
- Version alignment with core OpenClaw release numbers.
|
||||
|
||||
## 2026.2.15
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/zalouser",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "OpenClaw Zalo Personal Account plugin via zca-cli",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,9 +1,38 @@
|
||||
#!/bin/sh
|
||||
FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g')
|
||||
[ -z "$FILES" ] && exit 0
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "$FILES" | xargs pnpm lint --fix
|
||||
echo "$FILES" | xargs pnpm format --no-error-on-unmatched-pattern
|
||||
echo "$FILES" | xargs git add
|
||||
set -euo pipefail
|
||||
|
||||
exit 0
|
||||
ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
RUN_NODE_TOOL="$ROOT_DIR/scripts/pre-commit/run-node-tool.sh"
|
||||
FILTER_FILES="$ROOT_DIR/scripts/pre-commit/filter-staged-files.mjs"
|
||||
|
||||
if [[ ! -x "$RUN_NODE_TOOL" ]]; then
|
||||
echo "Missing helper: $RUN_NODE_TOOL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$FILTER_FILES" ]]; then
|
||||
echo "Missing helper: $FILTER_FILES" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Security: avoid option-injection from malicious file names (e.g. "--all", "--force").
|
||||
# Robustness: NUL-delimited file list handles spaces/newlines safely.
|
||||
mapfile -d '' -t files < <(git diff --cached --name-only --diff-filter=ACMR -z)
|
||||
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mapfile -d '' -t lint_files < <(node "$FILTER_FILES" lint -- "${files[@]}")
|
||||
mapfile -d '' -t format_files < <(node "$FILTER_FILES" format -- "${files[@]}")
|
||||
|
||||
if [ "${#lint_files[@]}" -gt 0 ]; then
|
||||
"$RUN_NODE_TOOL" oxlint --type-aware --fix -- "${lint_files[@]}"
|
||||
fi
|
||||
|
||||
if [ "${#format_files[@]}" -gt 0 ]; then
|
||||
"$RUN_NODE_TOOL" oxfmt --write -- "${format_files[@]}"
|
||||
fi
|
||||
|
||||
git add -- "${files[@]}"
|
||||
|
||||
22
package.json
22
package.json
@@ -1,13 +1,25 @@
|
||||
{
|
||||
"name": "openclaw",
|
||||
"version": "2026.2.15",
|
||||
"version": "2026.2.16",
|
||||
"description": "Multi-channel AI gateway with extensible messaging integrations",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/openclaw/openclaw#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/openclaw/openclaw/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/openclaw/openclaw.git"
|
||||
},
|
||||
"bin": {
|
||||
"openclaw": "openclaw.mjs"
|
||||
},
|
||||
"directories": {
|
||||
"doc": "docs",
|
||||
"test": "test"
|
||||
},
|
||||
"files": [
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
@@ -160,7 +172,7 @@
|
||||
"sharp": "^0.34.5",
|
||||
"signal-utils": "^0.21.1",
|
||||
"sqlite-vec": "0.1.7-alpha.2",
|
||||
"tar": "7.5.7",
|
||||
"tar": "7.5.9",
|
||||
"tslog": "^4.10.2",
|
||||
"undici": "^7.22.0",
|
||||
"ws": "^8.19.0",
|
||||
@@ -177,13 +189,13 @@
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260214.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260215.1",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"lit": "^3.3.2",
|
||||
"ollama": "^0.6.3",
|
||||
"oxfmt": "0.32.0",
|
||||
"oxlint": "^1.47.0",
|
||||
"oxlint-tsgolint": "^0.12.2",
|
||||
"oxlint-tsgolint": "^0.13.0",
|
||||
"rolldown": "1.0.0-rc.4",
|
||||
"tsdown": "^0.20.3",
|
||||
"tsx": "^4.21.0",
|
||||
@@ -205,7 +217,7 @@
|
||||
"form-data": "2.5.4",
|
||||
"qs": "6.14.2",
|
||||
"@sinclair/typebox": "0.34.48",
|
||||
"tar": "7.5.7",
|
||||
"tar": "7.5.9",
|
||||
"tough-cookie": "4.1.3"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
|
||||
162
pnpm-lock.yaml
generated
162
pnpm-lock.yaml
generated
@@ -9,7 +9,7 @@ overrides:
|
||||
form-data: 2.5.4
|
||||
qs: 6.14.2
|
||||
'@sinclair/typebox': 0.34.48
|
||||
tar: 7.5.7
|
||||
tar: 7.5.9
|
||||
tough-cookie: 4.1.3
|
||||
|
||||
importers:
|
||||
@@ -161,8 +161,8 @@ importers:
|
||||
specifier: 0.1.7-alpha.2
|
||||
version: 0.1.7-alpha.2
|
||||
tar:
|
||||
specifier: 7.5.7
|
||||
version: 7.5.7
|
||||
specifier: 7.5.9
|
||||
version: 7.5.9
|
||||
tslog:
|
||||
specifier: ^4.10.2
|
||||
version: 4.10.2
|
||||
@@ -207,8 +207,8 @@ importers:
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
'@typescript/native-preview':
|
||||
specifier: 7.0.0-dev.20260214.1
|
||||
version: 7.0.0-dev.20260214.1
|
||||
specifier: 7.0.0-dev.20260215.1
|
||||
version: 7.0.0-dev.20260215.1
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@vitest/browser@4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18))(vitest@4.0.18)
|
||||
@@ -223,16 +223,16 @@ importers:
|
||||
version: 0.32.0
|
||||
oxlint:
|
||||
specifier: ^1.47.0
|
||||
version: 1.47.0(oxlint-tsgolint@0.12.2)
|
||||
version: 1.47.0(oxlint-tsgolint@0.13.0)
|
||||
oxlint-tsgolint:
|
||||
specifier: ^0.12.2
|
||||
version: 0.12.2
|
||||
specifier: ^0.13.0
|
||||
version: 0.13.0
|
||||
rolldown:
|
||||
specifier: 1.0.0-rc.4
|
||||
version: 1.0.0-rc.4
|
||||
tsdown:
|
||||
specifier: ^0.20.3
|
||||
version: 0.20.3(@typescript/native-preview@7.0.0-dev.20260214.1)(typescript@5.9.3)
|
||||
version: 0.20.3(@typescript/native-preview@7.0.0-dev.20260215.1)(typescript@5.9.3)
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
@@ -783,8 +783,8 @@ packages:
|
||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-string-parser@8.0.0-rc.1':
|
||||
resolution: {integrity: sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw==}
|
||||
'@babel/helper-string-parser@8.0.0-rc.2':
|
||||
resolution: {integrity: sha512-noLx87RwlBEMrTzncWd/FvTxoJ9+ycHNg0n8yyYydIoDsLZuxknKgWRJUqcrVkNrJ74uGyhWQzQaS3q8xfGAhQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5':
|
||||
@@ -2059,33 +2059,33 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.12.2':
|
||||
resolution: {integrity: sha512-XIfavTqkJPGYi/98z7ZCkZvXq2AccMAAB0iwvKDRTQqiweMXVUyeUdx46phCHHH1PgmIVJtVfysThkHq2xCyrw==}
|
||||
'@oxlint-tsgolint/darwin-arm64@0.13.0':
|
||||
resolution: {integrity: sha512-OWQ3U+oDjjupmX0WU9oYyKF2iUOKDMLW/+zan0cd0vYIGId80xTRHHA8oXnREmK8dsMMP3nV3VXME3NH/hS0lw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint-tsgolint/darwin-x64@0.12.2':
|
||||
resolution: {integrity: sha512-tytsvP6zmNShRNDo4GgQartOXmd4GPd+TylCUMdO/iWl9PZVOgRyswWbYVTNgn85Cib/aY2q3Uu+jOw+QlbxvQ==}
|
||||
'@oxlint-tsgolint/darwin-x64@0.13.0':
|
||||
resolution: {integrity: sha512-wZvgj+eVqNkCUjSq2ExlMdbGDpZfaw6J+YctQV1pkGFdn7Y9cySWdfwu5v/AW2JPsJbFMXJ8GAr+WoZbRapz2A==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxlint-tsgolint/linux-arm64@0.12.2':
|
||||
resolution: {integrity: sha512-3W38yJuF7taEquhEuD6mYQyCeWNAlc1pNPjFkspkhLKZVgbrhDA4V6fCxLDDRvrTHde0bXPmFvuPlUq5pSePgA==}
|
||||
'@oxlint-tsgolint/linux-arm64@0.13.0':
|
||||
resolution: {integrity: sha512-nwtf5BgHbAWSVwyIF00l6QpfyFcpDMp6D+3cpe6NTgBYMSSSC0Ip1gswUwzVccOPoQK48t+J6vHyURQ96M1KDg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint-tsgolint/linux-x64@0.12.2':
|
||||
resolution: {integrity: sha512-EjcEspeeV0NmaopEp4wcN5ntQP9VCJJDrTvzOjMP4W6ajz18M+pni9vkKvmcPIpRa/UmWobeFgKoVd/KGueeuQ==}
|
||||
'@oxlint-tsgolint/linux-x64@0.13.0':
|
||||
resolution: {integrity: sha512-Rkzgj38eVoGSBuGDaCrALS4FM19+m1Qlv0hjB4MWvXUej014XkB5ze+svYE3HX+AAm1ey9QYj/CQzfz203FPIg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxlint-tsgolint/win32-arm64@0.12.2':
|
||||
resolution: {integrity: sha512-a9L7iA5K/Ht/i8d9+7RTp6hbPa4cyXP0MdySVXAO6vczpL/4ildfY9Hr2m2wqL12uK6xe/uVABpVTrqay/wV+g==}
|
||||
'@oxlint-tsgolint/win32-arm64@0.13.0':
|
||||
resolution: {integrity: sha512-Y+0hFqLT5M7UIvGvTR3QFK27l17FqXk6UwwpBFOcyBGJ5bLd1RaAPWjqTmcgPvdolA6FCMeW1pxZuNtKDlYd7A==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint-tsgolint/win32-x64@0.12.2':
|
||||
resolution: {integrity: sha512-Cvt40UbTf5ib12DjGN+mMGOnjWa4Bc6Y7KEaXXp9qzckvs3HpNk2wSwMV3gnuR8Ipx4hkzkzrgzD0BAUsySAfA==}
|
||||
'@oxlint-tsgolint/win32-x64@0.13.0':
|
||||
resolution: {integrity: sha512-mXjTttzyyfl8d/XvxggmZFBq0pbQmRvHbjQEv70YECNaLEHG8j8WYUwLa641uudAnV1VoBI34pc7bmgJM7qhOA==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
@@ -2995,43 +2995,43 @@ packages:
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260214.1':
|
||||
resolution: {integrity: sha512-Jb2WcLGpTOC6x58e8QPYC/14xmDbnbFIuKqUvYoI77hVtojVyxZi8L5Y4CgYqXYx8vRWmIFk35c1OGdtPip6Sg==}
|
||||
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260215.1':
|
||||
resolution: {integrity: sha512-icVO/hEMXjWlKhmpjIpqDyCzPvtHqfrPB+2rkd6M3rz84Bmw+o8Xgd7JvRxryZhR+D0y55me/bKh9xgvsgzuhA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260214.1':
|
||||
resolution: {integrity: sha512-O9l2gVuQFZsb8NIQtu0HN5Tn/Hw2fwylPOPS/0Y4oW+FUMhkqtvetUkb3zZ0qj7capilZ4YnmyGYg3TDqkP4Nw==}
|
||||
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260215.1':
|
||||
resolution: {integrity: sha512-Wz73wf1o9+4KwCLg8wnnIZZDAvv2KRZlDyP4X8GfBNzajfIAwYvI0ANWuIDznUUGeDAcqhBJXNe0Bkf4H9y4mg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260214.1':
|
||||
resolution: {integrity: sha512-Hl4e3yxJqzIGgFI8aH/rLGW+a7kSLHJCpAd5JOLG7hHKnamZF4SjlunnoHLV4IcMri+G6UE3W/84i0QvQP5wLA==}
|
||||
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260215.1':
|
||||
resolution: {integrity: sha512-AYyXRxVwLZzfkEYN8FGdV4vqXwbTmv93nAZ6gMLvpDG4ItOybAE1R2obFjlFc+Or/rfQmVvfdkTym3c4bRJ3XQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/native-preview-linux-arm@7.0.0-dev.20260214.1':
|
||||
resolution: {integrity: sha512-TaFrVnx3iXtl/oH1hzwvFyqWj9tzkjW8Ufl2m0Vx2/7GXnzZadm2KA6tFpGbzzWbZJznmXxKHL4O3AZRQYyZqQ==}
|
||||
'@typescript/native-preview-linux-arm@7.0.0-dev.20260215.1':
|
||||
resolution: {integrity: sha512-6WVXFVSp3LBBiBgBMtAHQgTDN72mDhgjrmXH7GoABTxR9asK8oPfmy5cwTp1sPD46pYhqjnSHMrARyg2FaNSeA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/native-preview-linux-x64@7.0.0-dev.20260214.1':
|
||||
resolution: {integrity: sha512-a/JypIXTc/tdodhYdQm24WH6aTfnJJjDbwxce4BS2g6IzYSc2GFcZBvlq1CJYS2FAVLpiSxj0OFAZmgjpCDAKg==}
|
||||
'@typescript/native-preview-linux-x64@7.0.0-dev.20260215.1':
|
||||
resolution: {integrity: sha512-Ui6qbTO+nE7fwh5OGTGfL4ndaT+SpiUiv0F1m3+nMaiAKysY5GbgXUfzWzkSrOODsT8F/4jZ4wCzEzJordt8sQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260214.1':
|
||||
resolution: {integrity: sha512-MJGPEDvdXj8olcWH0P+cWYcaN4r/0J4aSbcaISlen3MZ/2hrrgNl46PV4eGJKKCDniY2pH2fJzrMyJWZOcdb0w==}
|
||||
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260215.1':
|
||||
resolution: {integrity: sha512-dBFyAH9h3bMUaIp/84c3gKwyQ6jQmtzVoIBamSrYNw0xinJ56A/Ln5igdNOYrH8+/Aofmeh7pAWaa8U456XMjw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@typescript/native-preview-win32-x64@7.0.0-dev.20260214.1':
|
||||
resolution: {integrity: sha512-BtF48TRUyiCKznlOcQ7r7EXhonGSanm9X2eu7d8Yq1vaWO5SDgB0e+ISQXSoIfs3a1S3d5S5QV/vTE4+vocPxA==}
|
||||
'@typescript/native-preview-win32-x64@7.0.0-dev.20260215.1':
|
||||
resolution: {integrity: sha512-bEMSwX71OGGvfsfHEa/aX7ZUWbPSI2oKEmeWcDQVY8vH1VK1ZwcFzMhKfgVJPt5pKH2bK3EO3xYnAyKkDO/Ung==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@typescript/native-preview@7.0.0-dev.20260214.1':
|
||||
resolution: {integrity: sha512-BDM0ZLf2v6ilR0tDi8OMEr4X08lFCToPk3/p1SSE4GhagzmlU/5b+9slR0kKtaKMrds01FhvaKx6U9+NmAWgbQ==}
|
||||
'@typescript/native-preview@7.0.0-dev.20260215.1':
|
||||
resolution: {integrity: sha512-grs0BbJyPR7VLNerBVteEToPku1InMKVKVKBUTJi19LfK+LU3+pkU6/fsTfZhH3xmIzIxD/sNRQHLt4x/Yb9yg==}
|
||||
hasBin: true
|
||||
|
||||
'@typespec/ts-http-runtime@0.3.3':
|
||||
@@ -4728,8 +4728,8 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
oxlint-tsgolint@0.12.2:
|
||||
resolution: {integrity: sha512-IFiOhYZfSgiHbBznTZOhFpEHpsZFSP0j7fVRake03HEkgH0YljnTFDNoRkGWsTrnrHr7nRIomSsF4TnCI/O+kQ==}
|
||||
oxlint-tsgolint@0.13.0:
|
||||
resolution: {integrity: sha512-VUOWP5T9R9RwuPLKvNgvhsjdPFVhr2k8no8ea84+KhDtYPmk9L/3StNP3WClyPOKJOT8bFlO3eyhTKxXK9+Oog==}
|
||||
hasBin: true
|
||||
|
||||
oxlint@1.47.0:
|
||||
@@ -5364,8 +5364,8 @@ packages:
|
||||
resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==}
|
||||
engines: {node: '>=12.17'}
|
||||
|
||||
tar@7.5.7:
|
||||
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
|
||||
tar@7.5.9:
|
||||
resolution: {integrity: sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
@@ -6285,7 +6285,7 @@ snapshots:
|
||||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
|
||||
'@babel/helper-string-parser@8.0.0-rc.1': {}
|
||||
'@babel/helper-string-parser@8.0.0-rc.2': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.28.5': {}
|
||||
|
||||
@@ -6308,7 +6308,7 @@ snapshots:
|
||||
|
||||
'@babel/types@8.0.0-rc.1':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 8.0.0-rc.1
|
||||
'@babel/helper-string-parser': 8.0.0-rc.2
|
||||
'@babel/helper-validator-identifier': 8.0.0-rc.1
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
@@ -7546,22 +7546,22 @@ snapshots:
|
||||
'@oxfmt/binding-win32-x64-msvc@0.32.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.12.2':
|
||||
'@oxlint-tsgolint/darwin-arm64@0.13.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/darwin-x64@0.12.2':
|
||||
'@oxlint-tsgolint/darwin-x64@0.13.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/linux-arm64@0.12.2':
|
||||
'@oxlint-tsgolint/linux-arm64@0.13.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/linux-x64@0.12.2':
|
||||
'@oxlint-tsgolint/linux-x64@0.13.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/win32-arm64@0.12.2':
|
||||
'@oxlint-tsgolint/win32-arm64@0.13.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/win32-x64@0.12.2':
|
||||
'@oxlint-tsgolint/win32-x64@0.13.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint/binding-android-arm-eabi@1.47.0':
|
||||
@@ -8473,36 +8473,36 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 25.2.3
|
||||
|
||||
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260214.1':
|
||||
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260215.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260214.1':
|
||||
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260215.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260214.1':
|
||||
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260215.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-linux-arm@7.0.0-dev.20260214.1':
|
||||
'@typescript/native-preview-linux-arm@7.0.0-dev.20260215.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-linux-x64@7.0.0-dev.20260214.1':
|
||||
'@typescript/native-preview-linux-x64@7.0.0-dev.20260215.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260214.1':
|
||||
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260215.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-win32-x64@7.0.0-dev.20260214.1':
|
||||
'@typescript/native-preview-win32-x64@7.0.0-dev.20260215.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview@7.0.0-dev.20260214.1':
|
||||
'@typescript/native-preview@7.0.0-dev.20260215.1':
|
||||
optionalDependencies:
|
||||
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260215.1
|
||||
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260215.1
|
||||
|
||||
'@typespec/ts-http-runtime@0.3.3':
|
||||
dependencies:
|
||||
@@ -8990,7 +8990,7 @@ snapshots:
|
||||
npmlog: 6.0.2
|
||||
rc: 1.2.8
|
||||
semver: 7.7.4
|
||||
tar: 7.5.7
|
||||
tar: 7.5.9
|
||||
url-join: 4.0.1
|
||||
which: 2.0.2
|
||||
yargs: 17.7.2
|
||||
@@ -10385,16 +10385,16 @@ snapshots:
|
||||
'@oxfmt/binding-win32-ia32-msvc': 0.32.0
|
||||
'@oxfmt/binding-win32-x64-msvc': 0.32.0
|
||||
|
||||
oxlint-tsgolint@0.12.2:
|
||||
oxlint-tsgolint@0.13.0:
|
||||
optionalDependencies:
|
||||
'@oxlint-tsgolint/darwin-arm64': 0.12.2
|
||||
'@oxlint-tsgolint/darwin-x64': 0.12.2
|
||||
'@oxlint-tsgolint/linux-arm64': 0.12.2
|
||||
'@oxlint-tsgolint/linux-x64': 0.12.2
|
||||
'@oxlint-tsgolint/win32-arm64': 0.12.2
|
||||
'@oxlint-tsgolint/win32-x64': 0.12.2
|
||||
'@oxlint-tsgolint/darwin-arm64': 0.13.0
|
||||
'@oxlint-tsgolint/darwin-x64': 0.13.0
|
||||
'@oxlint-tsgolint/linux-arm64': 0.13.0
|
||||
'@oxlint-tsgolint/linux-x64': 0.13.0
|
||||
'@oxlint-tsgolint/win32-arm64': 0.13.0
|
||||
'@oxlint-tsgolint/win32-x64': 0.13.0
|
||||
|
||||
oxlint@1.47.0(oxlint-tsgolint@0.12.2):
|
||||
oxlint@1.47.0(oxlint-tsgolint@0.13.0):
|
||||
optionalDependencies:
|
||||
'@oxlint/binding-android-arm-eabi': 1.47.0
|
||||
'@oxlint/binding-android-arm64': 1.47.0
|
||||
@@ -10415,7 +10415,7 @@ snapshots:
|
||||
'@oxlint/binding-win32-arm64-msvc': 1.47.0
|
||||
'@oxlint/binding-win32-ia32-msvc': 1.47.0
|
||||
'@oxlint/binding-win32-x64-msvc': 1.47.0
|
||||
oxlint-tsgolint: 0.12.2
|
||||
oxlint-tsgolint: 0.13.0
|
||||
|
||||
p-finally@1.0.0: {}
|
||||
|
||||
@@ -10791,7 +10791,7 @@ snapshots:
|
||||
dependencies:
|
||||
glob: 10.5.0
|
||||
|
||||
rolldown-plugin-dts@0.22.1(@typescript/native-preview@7.0.0-dev.20260214.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3):
|
||||
rolldown-plugin-dts@0.22.1(@typescript/native-preview@7.0.0-dev.20260215.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/generator': 8.0.0-rc.1
|
||||
'@babel/helper-validator-identifier': 8.0.0-rc.1
|
||||
@@ -10804,7 +10804,7 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
rolldown: 1.0.0-rc.3
|
||||
optionalDependencies:
|
||||
'@typescript/native-preview': 7.0.0-dev.20260214.1
|
||||
'@typescript/native-preview': 7.0.0-dev.20260215.1
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- oxc-resolver
|
||||
@@ -11211,7 +11211,7 @@ snapshots:
|
||||
array-back: 6.2.2
|
||||
wordwrapjs: 5.1.1
|
||||
|
||||
tar@7.5.7:
|
||||
tar@7.5.9:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
@@ -11269,7 +11269,7 @@ snapshots:
|
||||
|
||||
ts-algebra@2.0.0: {}
|
||||
|
||||
tsdown@0.20.3(@typescript/native-preview@7.0.0-dev.20260214.1)(typescript@5.9.3):
|
||||
tsdown@0.20.3(@typescript/native-preview@7.0.0-dev.20260215.1)(typescript@5.9.3):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
cac: 6.7.14
|
||||
@@ -11280,7 +11280,7 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
picomatch: 4.0.3
|
||||
rolldown: 1.0.0-rc.3
|
||||
rolldown-plugin-dts: 0.22.1(@typescript/native-preview@7.0.0-dev.20260214.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3)
|
||||
rolldown-plugin-dts: 0.22.1(@typescript/native-preview@7.0.0-dev.20260215.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3)
|
||||
semver: 7.7.4
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
|
||||
@@ -15,6 +15,8 @@ COPY skills ./skills
|
||||
COPY patches ./patches
|
||||
COPY ui ./ui
|
||||
COPY extensions/memory-core ./extensions/memory-core
|
||||
COPY vendor/a2ui/renderers/lit ./vendor/a2ui/renderers/lit
|
||||
COPY apps/shared/OpenClawKit/Tools/CanvasA2UI ./apps/shared/OpenClawKit/Tools/CanvasA2UI
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm build
|
||||
|
||||
39
scripts/pre-commit/filter-staged-files.mjs
Normal file
39
scripts/pre-commit/filter-staged-files.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* Prints selected files as NUL-delimited tokens to stdout.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/pre-commit/filter-staged-files.mjs lint -- <files...>
|
||||
* node scripts/pre-commit/filter-staged-files.mjs format -- <files...>
|
||||
*
|
||||
* Keep this dependency-free: the pre-commit hook runs in many environments.
|
||||
*/
|
||||
|
||||
const mode = process.argv[2];
|
||||
const rawArgs = process.argv.slice(3);
|
||||
const files = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
|
||||
|
||||
if (mode !== "lint" && mode !== "format") {
|
||||
process.stderr.write("usage: filter-staged-files.mjs <lint|format> -- <files...>\n");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const lintExts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
||||
const formatExts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".md", ".mdx"]);
|
||||
|
||||
const shouldSelect = (filePath) => {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (mode === "lint") {
|
||||
return lintExts.has(ext);
|
||||
}
|
||||
return formatExts.has(ext);
|
||||
};
|
||||
|
||||
for (const file of files) {
|
||||
if (shouldSelect(file)) {
|
||||
process.stdout.write(file);
|
||||
process.stdout.write("\0");
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,16 @@ const unitIsolatedFilesRaw = [
|
||||
"src/browser/server.agent-contract-form-layout-act-commands.test.ts",
|
||||
"src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts",
|
||||
"src/browser/server.auth-token-gates-http.test.ts",
|
||||
"src/browser/server-context.remote-tab-ops.test.ts",
|
||||
"src/browser/server-context.ensure-tab-available.prefers-last-target.test.ts",
|
||||
// Keep this high-variance heavy file off the unit-fast critical path.
|
||||
"src/auto-reply/reply.block-streaming.test.ts",
|
||||
// Archive extraction/fixture-heavy suite; keep off unit-fast critical path.
|
||||
"src/hooks/install.test.ts",
|
||||
// Setup-heavy bot bootstrap suite.
|
||||
"src/telegram/bot.create-telegram-bot.test.ts",
|
||||
// Medium-heavy bot behavior suite; move off unit-fast critical path.
|
||||
"src/telegram/bot.test.ts",
|
||||
// Slack slash registration tests are setup-heavy and can bottleneck unit-fast.
|
||||
"src/slack/monitor/slash.test.ts",
|
||||
// Uses process-level unhandledRejection listeners; keep it off vmForks to avoid cross-file leakage.
|
||||
"src/imessage/monitor.shutdown.unhandled-rejection.test.ts",
|
||||
];
|
||||
@@ -165,7 +173,7 @@ const defaultWorkerBudget =
|
||||
unit: Math.max(2, Math.min(8, Math.floor(localWorkers / 2))),
|
||||
unitIsolated: 1,
|
||||
extensions: Math.max(1, Math.min(4, Math.floor(localWorkers / 4))),
|
||||
gateway: 1,
|
||||
gateway: 2,
|
||||
};
|
||||
|
||||
// Keep worker counts predictable for local runs; trim macOS CI workers to avoid worker crashes/OOM.
|
||||
|
||||
@@ -18,7 +18,7 @@ metadata:
|
||||
"archive": "tar.bz2",
|
||||
"extract": true,
|
||||
"stripComponents": 1,
|
||||
"targetDir": "~/.openclaw/tools/sherpa-onnx-tts/runtime",
|
||||
"targetDir": "runtime",
|
||||
"label": "Download sherpa-onnx runtime (macOS)",
|
||||
},
|
||||
{
|
||||
@@ -29,7 +29,7 @@ metadata:
|
||||
"archive": "tar.bz2",
|
||||
"extract": true,
|
||||
"stripComponents": 1,
|
||||
"targetDir": "~/.openclaw/tools/sherpa-onnx-tts/runtime",
|
||||
"targetDir": "runtime",
|
||||
"label": "Download sherpa-onnx runtime (Linux x64)",
|
||||
},
|
||||
{
|
||||
@@ -40,7 +40,7 @@ metadata:
|
||||
"archive": "tar.bz2",
|
||||
"extract": true,
|
||||
"stripComponents": 1,
|
||||
"targetDir": "~/.openclaw/tools/sherpa-onnx-tts/runtime",
|
||||
"targetDir": "runtime",
|
||||
"label": "Download sherpa-onnx runtime (Windows x64)",
|
||||
},
|
||||
{
|
||||
@@ -49,7 +49,7 @@ metadata:
|
||||
"url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-lessac-high.tar.bz2",
|
||||
"archive": "tar.bz2",
|
||||
"extract": true,
|
||||
"targetDir": "~/.openclaw/tools/sherpa-onnx-tts/models",
|
||||
"targetDir": "models",
|
||||
"label": "Download Piper en_US lessac (high)",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { RequestPermissionRequest } from "@agentclientprotocol/sdk";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolvePermissionRequest } from "./client.js";
|
||||
import { extractAttachmentsFromPrompt, extractTextFromPrompt } from "./event-mapper.js";
|
||||
|
||||
function makePermissionRequest(
|
||||
overrides: Partial<RequestPermissionRequest> = {},
|
||||
@@ -139,3 +140,32 @@ describe("resolvePermissionRequest", () => {
|
||||
expect(res).toEqual({ outcome: { outcome: "cancelled" } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("acp event mapper", () => {
|
||||
it("extracts text and resource blocks into prompt text", () => {
|
||||
const text = extractTextFromPrompt([
|
||||
{ type: "text", text: "Hello" },
|
||||
{ type: "resource", resource: { text: "File contents" } },
|
||||
{ type: "resource_link", uri: "https://example.com", title: "Spec" },
|
||||
{ type: "image", data: "abc", mimeType: "image/png" },
|
||||
]);
|
||||
|
||||
expect(text).toBe("Hello\nFile contents\n[Resource link (Spec)] https://example.com");
|
||||
});
|
||||
|
||||
it("extracts image blocks into gateway attachments", () => {
|
||||
const attachments = extractAttachmentsFromPrompt([
|
||||
{ type: "image", data: "abc", mimeType: "image/png" },
|
||||
{ type: "image", data: "", mimeType: "image/png" },
|
||||
{ type: "text", text: "ignored" },
|
||||
]);
|
||||
|
||||
expect(attachments).toEqual([
|
||||
{
|
||||
type: "image",
|
||||
mimeType: "image/png",
|
||||
content: "abc",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractAttachmentsFromPrompt, extractTextFromPrompt } from "./event-mapper.js";
|
||||
|
||||
describe("acp event mapper", () => {
|
||||
it("extracts text and resource blocks into prompt text", () => {
|
||||
const text = extractTextFromPrompt([
|
||||
{ type: "text", text: "Hello" },
|
||||
{ type: "resource", resource: { text: "File contents" } },
|
||||
{ type: "resource_link", uri: "https://example.com", title: "Spec" },
|
||||
{ type: "image", data: "abc", mimeType: "image/png" },
|
||||
]);
|
||||
|
||||
expect(text).toBe("Hello\nFile contents\n[Resource link (Spec)] https://example.com");
|
||||
});
|
||||
|
||||
it("extracts image blocks into gateway attachments", () => {
|
||||
const attachments = extractAttachmentsFromPrompt([
|
||||
{ type: "image", data: "abc", mimeType: "image/png" },
|
||||
{ type: "image", data: "", mimeType: "image/png" },
|
||||
{ type: "text", text: "ignored" },
|
||||
]);
|
||||
|
||||
expect(attachments).toEqual([
|
||||
{
|
||||
type: "image",
|
||||
mimeType: "image/png",
|
||||
content: "abc",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { GatewayClient } from "../gateway/client.js";
|
||||
import { parseSessionMeta, resolveSessionKey } from "./session-mapper.js";
|
||||
import { createInMemorySessionStore } from "./session.js";
|
||||
|
||||
function createGateway(resolveLabelKey = "agent:main:label"): {
|
||||
gateway: GatewayClient;
|
||||
@@ -54,3 +55,26 @@ describe("acp session mapper", () => {
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("acp session manager", () => {
|
||||
const store = createInMemorySessionStore();
|
||||
|
||||
afterEach(() => {
|
||||
store.clearAllSessionsForTest();
|
||||
});
|
||||
|
||||
it("tracks active runs and clears on cancel", () => {
|
||||
const session = store.createSession({
|
||||
sessionKey: "acp:test",
|
||||
cwd: "/tmp",
|
||||
});
|
||||
const controller = new AbortController();
|
||||
store.setActiveRun(session.sessionId, "run-1", controller);
|
||||
|
||||
expect(store.getSessionByRunId("run-1")?.sessionId).toBe(session.sessionId);
|
||||
|
||||
const cancelled = store.cancelActiveRun(session.sessionId);
|
||||
expect(cancelled).toBe(true);
|
||||
expect(store.getSessionByRunId("run-1")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { describe, expect, it, afterEach } from "vitest";
|
||||
import { createInMemorySessionStore } from "./session.js";
|
||||
|
||||
describe("acp session manager", () => {
|
||||
const store = createInMemorySessionStore();
|
||||
|
||||
afterEach(() => {
|
||||
store.clearAllSessionsForTest();
|
||||
});
|
||||
|
||||
it("tracks active runs and clears on cancel", () => {
|
||||
const session = store.createSession({
|
||||
sessionKey: "acp:test",
|
||||
cwd: "/tmp",
|
||||
});
|
||||
const controller = new AbortController();
|
||||
store.setActiveRun(session.sessionId, "run-1", controller);
|
||||
|
||||
expect(store.getSessionByRunId("run-1")?.sessionId).toBe(session.sessionId);
|
||||
|
||||
const cancelled = store.cancelActiveRun(session.sessionId);
|
||||
expect(cancelled).toBe(true);
|
||||
expect(store.getSessionByRunId("run-1")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
parseAgentSessionKey,
|
||||
} from "../routing/session-key.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { normalizeSkillFilter } from "./skills/filter.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
|
||||
|
||||
export { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
||||
@@ -128,12 +129,7 @@ export function resolveAgentSkillsFilter(
|
||||
cfg: OpenClawConfig,
|
||||
agentId: string,
|
||||
): string[] | undefined {
|
||||
const raw = resolveAgentConfig(cfg, agentId)?.skills;
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = raw.map((entry) => String(entry).trim()).filter(Boolean);
|
||||
return normalized.length > 0 ? normalized : [];
|
||||
return normalizeSkillFilter(resolveAgentConfig(cfg, agentId)?.skills);
|
||||
}
|
||||
|
||||
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { calculateAuthProfileCooldownMs } from "./auth-profiles.js";
|
||||
|
||||
describe("auth profile cooldowns", () => {
|
||||
it("applies exponential backoff with a 1h cap", () => {
|
||||
expect(calculateAuthProfileCooldownMs(1)).toBe(60_000);
|
||||
expect(calculateAuthProfileCooldownMs(2)).toBe(5 * 60_000);
|
||||
expect(calculateAuthProfileCooldownMs(3)).toBe(25 * 60_000);
|
||||
expect(calculateAuthProfileCooldownMs(4)).toBe(60 * 60_000);
|
||||
expect(calculateAuthProfileCooldownMs(5)).toBe(60 * 60_000);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import {
|
||||
type AuthProfileStore,
|
||||
ensureAuthProfileStore,
|
||||
@@ -10,10 +11,7 @@ import {
|
||||
import { CHUTES_TOKEN_ENDPOINT, type ChutesStoredOAuth } from "./chutes-oauth.js";
|
||||
|
||||
describe("auth-profiles (chutes)", () => {
|
||||
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
const previousAgentDir = process.env.OPENCLAW_AGENT_DIR;
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
const previousChutesClientId = process.env.CHUTES_CLIENT_ID;
|
||||
let envSnapshot: ReturnType<typeof captureEnv> | undefined;
|
||||
let tempDir: string | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -22,29 +20,17 @@ describe("auth-profiles (chutes)", () => {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
tempDir = null;
|
||||
}
|
||||
if (previousStateDir === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
||||
}
|
||||
if (previousAgentDir === undefined) {
|
||||
delete process.env.OPENCLAW_AGENT_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
|
||||
}
|
||||
if (previousPiAgentDir === undefined) {
|
||||
delete process.env.PI_CODING_AGENT_DIR;
|
||||
} else {
|
||||
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||
}
|
||||
if (previousChutesClientId === undefined) {
|
||||
delete process.env.CHUTES_CLIENT_ID;
|
||||
} else {
|
||||
process.env.CHUTES_CLIENT_ID = previousChutesClientId;
|
||||
}
|
||||
envSnapshot?.restore();
|
||||
envSnapshot = undefined;
|
||||
});
|
||||
|
||||
it("refreshes expired Chutes OAuth credentials", async () => {
|
||||
envSnapshot = captureEnv([
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"OPENCLAW_AGENT_DIR",
|
||||
"PI_CODING_AGENT_DIR",
|
||||
"CHUTES_CLIENT_ID",
|
||||
]);
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-chutes-"));
|
||||
process.env.OPENCLAW_STATE_DIR = tempDir;
|
||||
process.env.OPENCLAW_AGENT_DIR = path.join(tempDir, "agents", "main", "agent");
|
||||
|
||||
77
src/agents/auth-profiles.getsoonestcooldownexpiry.test.ts
Normal file
77
src/agents/auth-profiles.getsoonestcooldownexpiry.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AuthProfileStore } from "./auth-profiles.js";
|
||||
import { getSoonestCooldownExpiry } from "./auth-profiles.js";
|
||||
|
||||
function makeStore(usageStats?: AuthProfileStore["usageStats"]): AuthProfileStore {
|
||||
return {
|
||||
version: 1,
|
||||
profiles: {},
|
||||
usageStats,
|
||||
};
|
||||
}
|
||||
|
||||
describe("getSoonestCooldownExpiry", () => {
|
||||
it("returns null when no cooldown timestamps exist", () => {
|
||||
const store = makeStore();
|
||||
expect(getSoonestCooldownExpiry(store, ["openai:p1"])).toBeNull();
|
||||
});
|
||||
|
||||
it("returns earliest unusable time across profiles", () => {
|
||||
const store = makeStore({
|
||||
"openai:p1": {
|
||||
cooldownUntil: 1_700_000_002_000,
|
||||
disabledUntil: 1_700_000_004_000,
|
||||
},
|
||||
"openai:p2": {
|
||||
cooldownUntil: 1_700_000_003_000,
|
||||
},
|
||||
"openai:p3": {
|
||||
disabledUntil: 1_700_000_001_000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(getSoonestCooldownExpiry(store, ["openai:p1", "openai:p2", "openai:p3"])).toBe(
|
||||
1_700_000_001_000,
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores unknown profiles and invalid cooldown values", () => {
|
||||
const store = makeStore({
|
||||
"openai:p1": {
|
||||
cooldownUntil: -1,
|
||||
},
|
||||
"openai:p2": {
|
||||
cooldownUntil: Infinity,
|
||||
},
|
||||
"openai:p3": {
|
||||
disabledUntil: NaN,
|
||||
},
|
||||
"openai:p4": {
|
||||
cooldownUntil: 1_700_000_005_000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
getSoonestCooldownExpiry(store, [
|
||||
"missing",
|
||||
"openai:p1",
|
||||
"openai:p2",
|
||||
"openai:p3",
|
||||
"openai:p4",
|
||||
]),
|
||||
).toBe(1_700_000_005_000);
|
||||
});
|
||||
|
||||
it("returns past timestamps when cooldown already expired", () => {
|
||||
const store = makeStore({
|
||||
"openai:p1": {
|
||||
cooldownUntil: 1_700_000_000_000,
|
||||
},
|
||||
"openai:p2": {
|
||||
disabledUntil: 1_700_000_010_000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(getSoonestCooldownExpiry(store, ["openai:p1", "openai:p2"])).toBe(1_700_000_000_000);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,11 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ensureAuthProfileStore, markAuthProfileFailure } from "./auth-profiles.js";
|
||||
import {
|
||||
calculateAuthProfileCooldownMs,
|
||||
ensureAuthProfileStore,
|
||||
markAuthProfileFailure,
|
||||
} from "./auth-profiles.js";
|
||||
|
||||
describe("markAuthProfileFailure", () => {
|
||||
it("disables billing failures for ~5 hours by default", async () => {
|
||||
@@ -129,3 +133,13 @@ describe("markAuthProfileFailure", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateAuthProfileCooldownMs", () => {
|
||||
it("applies exponential backoff with a 1h cap", () => {
|
||||
expect(calculateAuthProfileCooldownMs(1)).toBe(60_000);
|
||||
expect(calculateAuthProfileCooldownMs(2)).toBe(5 * 60_000);
|
||||
expect(calculateAuthProfileCooldownMs(3)).toBe(25 * 60_000);
|
||||
expect(calculateAuthProfileCooldownMs(4)).toBe(60 * 60_000);
|
||||
expect(calculateAuthProfileCooldownMs(5)).toBe(60 * 60_000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ export type {
|
||||
export {
|
||||
calculateAuthProfileCooldownMs,
|
||||
clearAuthProfileCooldown,
|
||||
getSoonestCooldownExpiry,
|
||||
isProfileInCooldown,
|
||||
markAuthProfileCooldown,
|
||||
markAuthProfileFailure,
|
||||
|
||||
@@ -3,13 +3,16 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AuthProfileStore } from "./types.js";
|
||||
import { captureEnv } from "../../test-utils/env.js";
|
||||
import { resolveApiKeyForProfile } from "./oauth.js";
|
||||
import { ensureAuthProfileStore } from "./store.js";
|
||||
|
||||
describe("resolveApiKeyForProfile fallback to main agent", () => {
|
||||
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
const previousAgentDir = process.env.OPENCLAW_AGENT_DIR;
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
const envSnapshot = captureEnv([
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"OPENCLAW_AGENT_DIR",
|
||||
"PI_CODING_AGENT_DIR",
|
||||
]);
|
||||
let tmpDir: string;
|
||||
let mainAgentDir: string;
|
||||
let secondaryAgentDir: string;
|
||||
@@ -30,22 +33,7 @@ describe("resolveApiKeyForProfile fallback to main agent", () => {
|
||||
afterEach(async () => {
|
||||
vi.unstubAllGlobals();
|
||||
|
||||
// Restore original environment
|
||||
if (previousStateDir === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
||||
}
|
||||
if (previousAgentDir === undefined) {
|
||||
delete process.env.OPENCLAW_AGENT_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
|
||||
}
|
||||
if (previousPiAgentDir === undefined) {
|
||||
delete process.env.PI_CODING_AGENT_DIR;
|
||||
} else {
|
||||
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||
}
|
||||
envSnapshot.restore();
|
||||
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -25,6 +25,32 @@ export function isProfileInCooldown(store: AuthProfileStore, profileId: string):
|
||||
return unusableUntil ? Date.now() < unusableUntil : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the soonest `unusableUntil` timestamp (ms epoch) among the given
|
||||
* profiles, or `null` when no profile has a recorded cooldown. Note: the
|
||||
* returned timestamp may be in the past if the cooldown has already expired.
|
||||
*/
|
||||
export function getSoonestCooldownExpiry(
|
||||
store: AuthProfileStore,
|
||||
profileIds: string[],
|
||||
): number | null {
|
||||
let soonest: number | null = null;
|
||||
for (const id of profileIds) {
|
||||
const stats = store.usageStats?.[id];
|
||||
if (!stats) {
|
||||
continue;
|
||||
}
|
||||
const until = resolveProfileUnusableUntil(stats);
|
||||
if (typeof until !== "number" || !Number.isFinite(until) || until <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (soonest === null || until < soonest) {
|
||||
soonest = until;
|
||||
}
|
||||
}
|
||||
return soonest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a profile as successfully used. Resets error count and updates lastUsed.
|
||||
* Uses store lock to avoid overwriting concurrent usage updates.
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import path from "node:path";
|
||||
import type { ExecAsk, ExecHost, ExecSecurity } from "../infra/exec-approvals.js";
|
||||
import type { ProcessSession, SessionStdin } from "./bash-process-registry.js";
|
||||
import type { ProcessSession } from "./bash-process-registry.js";
|
||||
import type { ExecToolDetails } from "./bash-tools.exec.js";
|
||||
import type { BashSandboxConfig } from "./bash-tools.shared.js";
|
||||
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
|
||||
import { mergePathPrepend } from "../infra/path-prepend.js";
|
||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||
export { applyPathPrepend, normalizePathPrepend } from "../infra/path-prepend.js";
|
||||
import type { ManagedRun } from "../process/supervisor/index.js";
|
||||
import { logWarn } from "../logger.js";
|
||||
import { formatSpawnError, spawnWithFallback } from "../process/spawn-utils.js";
|
||||
import { getProcessSupervisor } from "../process/supervisor/index.js";
|
||||
import {
|
||||
addSession,
|
||||
appendOutput,
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
buildDockerExecArgs,
|
||||
chunkString,
|
||||
clampWithDefault,
|
||||
killSession,
|
||||
readEnvInt,
|
||||
} from "./bash-tools.shared.js";
|
||||
import { buildCursorPositionResponse, stripDsrRequests } from "./pty-dsr.js";
|
||||
@@ -147,26 +146,6 @@ export const execSchema = Type.Object({
|
||||
),
|
||||
});
|
||||
|
||||
type PtyExitEvent = { exitCode: number; signal?: number };
|
||||
type PtyListener<T> = (event: T) => void;
|
||||
type PtyHandle = {
|
||||
pid: number;
|
||||
write: (data: string | Buffer) => void;
|
||||
onData: (listener: PtyListener<string>) => void;
|
||||
onExit: (listener: PtyListener<PtyExitEvent>) => void;
|
||||
};
|
||||
type PtySpawn = (
|
||||
file: string,
|
||||
args: string[] | string,
|
||||
options: {
|
||||
name?: string;
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
},
|
||||
) => PtyHandle;
|
||||
|
||||
export type ExecProcessOutcome = {
|
||||
status: "completed" | "failed";
|
||||
exitCode: number | null;
|
||||
@@ -319,138 +298,10 @@ export async function runExecProcess(opts: {
|
||||
}): Promise<ExecProcessHandle> {
|
||||
const startedAt = Date.now();
|
||||
const sessionId = createSessionSlug();
|
||||
let child: ChildProcessWithoutNullStreams | null = null;
|
||||
let pty: PtyHandle | null = null;
|
||||
let stdin: SessionStdin | undefined;
|
||||
const execCommand = opts.execCommand ?? opts.command;
|
||||
const supervisor = getProcessSupervisor();
|
||||
|
||||
const spawnFallbacks = [
|
||||
{
|
||||
label: "no-detach",
|
||||
options: { detached: false },
|
||||
},
|
||||
];
|
||||
|
||||
const handleSpawnFallback = (err: unknown, fallback: { label: string }) => {
|
||||
const errText = formatSpawnError(err);
|
||||
const warning = `Warning: spawn failed (${errText}); retrying with ${fallback.label}.`;
|
||||
logWarn(`exec: spawn failed (${errText}); retrying with ${fallback.label}.`);
|
||||
opts.warnings.push(warning);
|
||||
};
|
||||
|
||||
const spawnShellChild = async (
|
||||
shell: string,
|
||||
shellArgs: string[],
|
||||
): Promise<ChildProcessWithoutNullStreams> => {
|
||||
const { child: spawned } = await spawnWithFallback({
|
||||
argv: [shell, ...shellArgs, execCommand],
|
||||
options: {
|
||||
cwd: opts.workdir,
|
||||
env: opts.env,
|
||||
detached: process.platform !== "win32",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
},
|
||||
fallbacks: spawnFallbacks,
|
||||
onFallback: handleSpawnFallback,
|
||||
});
|
||||
return spawned as ChildProcessWithoutNullStreams;
|
||||
};
|
||||
|
||||
// `exec` does not currently accept tool-provided stdin content. For non-PTY runs,
|
||||
// keeping stdin open can cause commands like `wc -l` (or safeBins-hardened segments)
|
||||
// to block forever waiting for input, leading to accidental backgrounding.
|
||||
// For interactive flows, callers should use `pty: true` (stdin kept open).
|
||||
const maybeCloseNonPtyStdin = () => {
|
||||
if (opts.usePty) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Signal EOF immediately so stdin-only commands can terminate.
|
||||
child?.stdin?.end();
|
||||
} catch {
|
||||
// ignore stdin close errors
|
||||
}
|
||||
};
|
||||
|
||||
if (opts.sandbox) {
|
||||
const { child: spawned } = await spawnWithFallback({
|
||||
argv: [
|
||||
"docker",
|
||||
...buildDockerExecArgs({
|
||||
containerName: opts.sandbox.containerName,
|
||||
command: execCommand,
|
||||
workdir: opts.containerWorkdir ?? opts.sandbox.containerWorkdir,
|
||||
env: opts.env,
|
||||
tty: opts.usePty,
|
||||
}),
|
||||
],
|
||||
options: {
|
||||
cwd: opts.workdir,
|
||||
env: process.env,
|
||||
detached: process.platform !== "win32",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
},
|
||||
fallbacks: spawnFallbacks,
|
||||
onFallback: handleSpawnFallback,
|
||||
});
|
||||
child = spawned as ChildProcessWithoutNullStreams;
|
||||
stdin = child.stdin;
|
||||
maybeCloseNonPtyStdin();
|
||||
} else if (opts.usePty) {
|
||||
const { shell, args: shellArgs } = getShellConfig();
|
||||
try {
|
||||
const ptyModule = (await import("@lydell/node-pty")) as unknown as {
|
||||
spawn?: PtySpawn;
|
||||
default?: { spawn?: PtySpawn };
|
||||
};
|
||||
const spawnPty = ptyModule.spawn ?? ptyModule.default?.spawn;
|
||||
if (!spawnPty) {
|
||||
throw new Error("PTY support is unavailable (node-pty spawn not found).");
|
||||
}
|
||||
pty = spawnPty(shell, [...shellArgs, execCommand], {
|
||||
cwd: opts.workdir,
|
||||
env: opts.env,
|
||||
name: process.env.TERM ?? "xterm-256color",
|
||||
cols: 120,
|
||||
rows: 30,
|
||||
});
|
||||
stdin = {
|
||||
destroyed: false,
|
||||
write: (data, cb) => {
|
||||
try {
|
||||
pty?.write(data);
|
||||
cb?.(null);
|
||||
} catch (err) {
|
||||
cb?.(err as Error);
|
||||
}
|
||||
},
|
||||
end: () => {
|
||||
try {
|
||||
const eof = process.platform === "win32" ? "\x1a" : "\x04";
|
||||
pty?.write(eof);
|
||||
} catch {
|
||||
// ignore EOF errors
|
||||
}
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
const errText = String(err);
|
||||
const warning = `Warning: PTY spawn failed (${errText}); retrying without PTY for \`${opts.command}\`.`;
|
||||
logWarn(`exec: PTY spawn failed (${errText}); retrying without PTY for "${opts.command}".`);
|
||||
opts.warnings.push(warning);
|
||||
child = await spawnShellChild(shell, shellArgs);
|
||||
stdin = child.stdin;
|
||||
}
|
||||
} else {
|
||||
const { shell, args: shellArgs } = getShellConfig();
|
||||
child = await spawnShellChild(shell, shellArgs);
|
||||
stdin = child.stdin;
|
||||
maybeCloseNonPtyStdin();
|
||||
}
|
||||
|
||||
const session = {
|
||||
const session: ProcessSession = {
|
||||
id: sessionId,
|
||||
command: opts.command,
|
||||
scopeKey: opts.scopeKey,
|
||||
@@ -458,9 +309,9 @@ export async function runExecProcess(opts: {
|
||||
notifyOnExit: opts.notifyOnExit,
|
||||
notifyOnExitEmptySuccess: opts.notifyOnExitEmptySuccess === true,
|
||||
exitNotified: false,
|
||||
child: child ?? undefined,
|
||||
stdin,
|
||||
pid: child?.pid ?? pty?.pid,
|
||||
child: undefined,
|
||||
stdin: undefined,
|
||||
pid: undefined,
|
||||
startedAt,
|
||||
cwd: opts.workdir,
|
||||
maxOutputChars: opts.maxOutput,
|
||||
@@ -477,59 +328,9 @@ export async function runExecProcess(opts: {
|
||||
exitSignal: undefined as NodeJS.Signals | number | null | undefined,
|
||||
truncated: false,
|
||||
backgrounded: false,
|
||||
} satisfies ProcessSession;
|
||||
};
|
||||
addSession(session);
|
||||
|
||||
let settled = false;
|
||||
let timeoutTimer: NodeJS.Timeout | null = null;
|
||||
let timeoutFinalizeTimer: NodeJS.Timeout | null = null;
|
||||
let timedOut = false;
|
||||
const timeoutFinalizeMs = 1000;
|
||||
let resolveFn: ((outcome: ExecProcessOutcome) => void) | null = null;
|
||||
|
||||
const settle = (outcome: ExecProcessOutcome) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
resolveFn?.(outcome);
|
||||
};
|
||||
|
||||
const finalizeTimeout = () => {
|
||||
if (session.exited) {
|
||||
return;
|
||||
}
|
||||
markExited(session, null, "SIGKILL", "failed");
|
||||
maybeNotifyOnExit(session, "failed");
|
||||
const aggregated = session.aggregated.trim();
|
||||
const reason = `Command timed out after ${opts.timeoutSec} seconds`;
|
||||
settle({
|
||||
status: "failed",
|
||||
exitCode: null,
|
||||
exitSignal: "SIGKILL",
|
||||
durationMs: Date.now() - startedAt,
|
||||
aggregated,
|
||||
timedOut: true,
|
||||
reason: aggregated ? `${aggregated}\n\n${reason}` : reason,
|
||||
});
|
||||
};
|
||||
|
||||
const onTimeout = () => {
|
||||
timedOut = true;
|
||||
killSession(session);
|
||||
if (!timeoutFinalizeTimer) {
|
||||
timeoutFinalizeTimer = setTimeout(() => {
|
||||
finalizeTimeout();
|
||||
}, timeoutFinalizeMs);
|
||||
}
|
||||
};
|
||||
|
||||
if (opts.timeoutSec > 0) {
|
||||
timeoutTimer = setTimeout(() => {
|
||||
onTimeout();
|
||||
}, opts.timeoutSec * 1000);
|
||||
}
|
||||
|
||||
const emitUpdate = () => {
|
||||
if (!opts.onUpdate) {
|
||||
return;
|
||||
@@ -565,116 +366,208 @@ export async function runExecProcess(opts: {
|
||||
}
|
||||
};
|
||||
|
||||
if (pty) {
|
||||
const cursorResponse = buildCursorPositionResponse();
|
||||
pty.onData((data) => {
|
||||
const raw = data.toString();
|
||||
const { cleaned, requests } = stripDsrRequests(raw);
|
||||
if (requests > 0) {
|
||||
const timeoutMs =
|
||||
typeof opts.timeoutSec === "number" && opts.timeoutSec > 0
|
||||
? Math.floor(opts.timeoutSec * 1000)
|
||||
: undefined;
|
||||
|
||||
const spawnSpec:
|
||||
| {
|
||||
mode: "child";
|
||||
argv: string[];
|
||||
env: NodeJS.ProcessEnv;
|
||||
stdinMode: "pipe-open" | "pipe-closed";
|
||||
}
|
||||
| {
|
||||
mode: "pty";
|
||||
ptyCommand: string;
|
||||
childFallbackArgv: string[];
|
||||
env: NodeJS.ProcessEnv;
|
||||
stdinMode: "pipe-open";
|
||||
} = (() => {
|
||||
if (opts.sandbox) {
|
||||
return {
|
||||
mode: "child" as const,
|
||||
argv: [
|
||||
"docker",
|
||||
...buildDockerExecArgs({
|
||||
containerName: opts.sandbox.containerName,
|
||||
command: execCommand,
|
||||
workdir: opts.containerWorkdir ?? opts.sandbox.containerWorkdir,
|
||||
env: opts.env,
|
||||
tty: opts.usePty,
|
||||
}),
|
||||
],
|
||||
env: process.env,
|
||||
stdinMode: opts.usePty ? ("pipe-open" as const) : ("pipe-closed" as const),
|
||||
};
|
||||
}
|
||||
const { shell, args: shellArgs } = getShellConfig();
|
||||
const childArgv = [shell, ...shellArgs, execCommand];
|
||||
if (opts.usePty) {
|
||||
return {
|
||||
mode: "pty" as const,
|
||||
ptyCommand: execCommand,
|
||||
childFallbackArgv: childArgv,
|
||||
env: opts.env,
|
||||
stdinMode: "pipe-open" as const,
|
||||
};
|
||||
}
|
||||
return {
|
||||
mode: "child" as const,
|
||||
argv: childArgv,
|
||||
env: opts.env,
|
||||
stdinMode: "pipe-closed" as const,
|
||||
};
|
||||
})();
|
||||
|
||||
let managedRun: ManagedRun | null = null;
|
||||
let usingPty = spawnSpec.mode === "pty";
|
||||
const cursorResponse = buildCursorPositionResponse();
|
||||
|
||||
const onSupervisorStdout = (chunk: string) => {
|
||||
if (usingPty) {
|
||||
const { cleaned, requests } = stripDsrRequests(chunk);
|
||||
if (requests > 0 && managedRun?.stdin) {
|
||||
for (let i = 0; i < requests; i += 1) {
|
||||
pty.write(cursorResponse);
|
||||
managedRun.stdin.write(cursorResponse);
|
||||
}
|
||||
}
|
||||
handleStdout(cleaned);
|
||||
});
|
||||
} else if (child) {
|
||||
child.stdout.on("data", handleStdout);
|
||||
child.stderr.on("data", handleStderr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
handleStdout(chunk);
|
||||
};
|
||||
|
||||
const promise = new Promise<ExecProcessOutcome>((resolve) => {
|
||||
resolveFn = resolve;
|
||||
const handleExit = (code: number | null, exitSignal: NodeJS.Signals | number | null) => {
|
||||
if (timeoutTimer) {
|
||||
clearTimeout(timeoutTimer);
|
||||
}
|
||||
if (timeoutFinalizeTimer) {
|
||||
clearTimeout(timeoutFinalizeTimer);
|
||||
try {
|
||||
const spawnBase = {
|
||||
runId: sessionId,
|
||||
sessionId: opts.sessionKey?.trim() || sessionId,
|
||||
backendId: opts.sandbox ? "exec-sandbox" : "exec-host",
|
||||
scopeKey: opts.scopeKey,
|
||||
cwd: opts.workdir,
|
||||
env: spawnSpec.env,
|
||||
timeoutMs,
|
||||
captureOutput: false,
|
||||
onStdout: onSupervisorStdout,
|
||||
onStderr: handleStderr,
|
||||
};
|
||||
managedRun =
|
||||
spawnSpec.mode === "pty"
|
||||
? await supervisor.spawn({
|
||||
...spawnBase,
|
||||
mode: "pty",
|
||||
ptyCommand: spawnSpec.ptyCommand,
|
||||
})
|
||||
: await supervisor.spawn({
|
||||
...spawnBase,
|
||||
mode: "child",
|
||||
argv: spawnSpec.argv,
|
||||
stdinMode: spawnSpec.stdinMode,
|
||||
});
|
||||
} catch (err) {
|
||||
if (spawnSpec.mode === "pty") {
|
||||
const warning = `Warning: PTY spawn failed (${String(err)}); retrying without PTY for \`${opts.command}\`.`;
|
||||
logWarn(
|
||||
`exec: PTY spawn failed (${String(err)}); retrying without PTY for "${opts.command}".`,
|
||||
);
|
||||
opts.warnings.push(warning);
|
||||
usingPty = false;
|
||||
try {
|
||||
managedRun = await supervisor.spawn({
|
||||
runId: sessionId,
|
||||
sessionId: opts.sessionKey?.trim() || sessionId,
|
||||
backendId: "exec-host",
|
||||
scopeKey: opts.scopeKey,
|
||||
mode: "child",
|
||||
argv: spawnSpec.childFallbackArgv,
|
||||
cwd: opts.workdir,
|
||||
env: spawnSpec.env,
|
||||
stdinMode: "pipe-open",
|
||||
timeoutMs,
|
||||
captureOutput: false,
|
||||
onStdout: handleStdout,
|
||||
onStderr: handleStderr,
|
||||
});
|
||||
} catch (retryErr) {
|
||||
markExited(session, null, null, "failed");
|
||||
maybeNotifyOnExit(session, "failed");
|
||||
throw retryErr;
|
||||
}
|
||||
} else {
|
||||
markExited(session, null, null, "failed");
|
||||
maybeNotifyOnExit(session, "failed");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
session.stdin = managedRun.stdin;
|
||||
session.pid = managedRun.pid;
|
||||
|
||||
const promise = managedRun
|
||||
.wait()
|
||||
.then((exit): ExecProcessOutcome => {
|
||||
const durationMs = Date.now() - startedAt;
|
||||
const wasSignal = exitSignal != null;
|
||||
const isSuccess = code === 0 && !wasSignal && !timedOut;
|
||||
const status: "completed" | "failed" = isSuccess ? "completed" : "failed";
|
||||
markExited(session, code, exitSignal, status);
|
||||
const status: "completed" | "failed" =
|
||||
exit.exitCode === 0 && exit.reason === "exit" ? "completed" : "failed";
|
||||
markExited(session, exit.exitCode, exit.exitSignal, status);
|
||||
maybeNotifyOnExit(session, status);
|
||||
if (!session.child && session.stdin) {
|
||||
session.stdin.destroyed = true;
|
||||
}
|
||||
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
const aggregated = session.aggregated.trim();
|
||||
if (!isSuccess) {
|
||||
const reason = timedOut
|
||||
? `Command timed out after ${opts.timeoutSec} seconds`
|
||||
: wasSignal && exitSignal
|
||||
? `Command aborted by signal ${exitSignal}`
|
||||
: code === null
|
||||
? "Command aborted before exit code was captured"
|
||||
: `Command exited with code ${code}`;
|
||||
const message = aggregated ? `${aggregated}\n\n${reason}` : reason;
|
||||
settle({
|
||||
status: "failed",
|
||||
exitCode: code ?? null,
|
||||
exitSignal: exitSignal ?? null,
|
||||
if (status === "completed") {
|
||||
return {
|
||||
status: "completed",
|
||||
exitCode: exit.exitCode ?? 0,
|
||||
exitSignal: exit.exitSignal,
|
||||
durationMs,
|
||||
aggregated,
|
||||
timedOut,
|
||||
reason: message,
|
||||
});
|
||||
return;
|
||||
timedOut: false,
|
||||
};
|
||||
}
|
||||
settle({
|
||||
status: "completed",
|
||||
exitCode: code ?? 0,
|
||||
exitSignal: exitSignal ?? null,
|
||||
const reason =
|
||||
exit.reason === "overall-timeout"
|
||||
? `Command timed out after ${opts.timeoutSec} seconds`
|
||||
: exit.reason === "no-output-timeout"
|
||||
? "Command timed out waiting for output"
|
||||
: exit.exitSignal != null
|
||||
? `Command aborted by signal ${exit.exitSignal}`
|
||||
: exit.exitCode == null
|
||||
? "Command aborted before exit code was captured"
|
||||
: `Command exited with code ${exit.exitCode}`;
|
||||
return {
|
||||
status: "failed",
|
||||
exitCode: exit.exitCode,
|
||||
exitSignal: exit.exitSignal,
|
||||
durationMs,
|
||||
aggregated,
|
||||
timedOut: exit.timedOut,
|
||||
reason: aggregated ? `${aggregated}\n\n${reason}` : reason,
|
||||
};
|
||||
})
|
||||
.catch((err): ExecProcessOutcome => {
|
||||
markExited(session, null, null, "failed");
|
||||
maybeNotifyOnExit(session, "failed");
|
||||
const aggregated = session.aggregated.trim();
|
||||
const message = aggregated ? `${aggregated}\n\n${String(err)}` : String(err);
|
||||
return {
|
||||
status: "failed",
|
||||
exitCode: null,
|
||||
exitSignal: null,
|
||||
durationMs: Date.now() - startedAt,
|
||||
aggregated,
|
||||
timedOut: false,
|
||||
});
|
||||
};
|
||||
|
||||
if (pty) {
|
||||
pty.onExit((event) => {
|
||||
const rawSignal = event.signal ?? null;
|
||||
const normalizedSignal = rawSignal === 0 ? null : rawSignal;
|
||||
handleExit(event.exitCode ?? null, normalizedSignal);
|
||||
});
|
||||
} else if (child) {
|
||||
child.once("close", (code, exitSignal) => {
|
||||
handleExit(code, exitSignal);
|
||||
});
|
||||
|
||||
child.once("error", (err) => {
|
||||
if (timeoutTimer) {
|
||||
clearTimeout(timeoutTimer);
|
||||
}
|
||||
if (timeoutFinalizeTimer) {
|
||||
clearTimeout(timeoutFinalizeTimer);
|
||||
}
|
||||
markExited(session, null, null, "failed");
|
||||
maybeNotifyOnExit(session, "failed");
|
||||
const aggregated = session.aggregated.trim();
|
||||
const message = aggregated ? `${aggregated}\n\n${String(err)}` : String(err);
|
||||
settle({
|
||||
status: "failed",
|
||||
exitCode: null,
|
||||
exitSignal: null,
|
||||
durationMs: Date.now() - startedAt,
|
||||
aggregated,
|
||||
timedOut,
|
||||
reason: message,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
reason: message,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
session,
|
||||
startedAt,
|
||||
pid: session.pid ?? undefined,
|
||||
promise,
|
||||
kill: () => killSession(session),
|
||||
kill: () => {
|
||||
managedRun?.cancel("manual-cancel");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
71
src/agents/bash-tools.exec.pty-cleanup.test.ts
Normal file
71
src/agents/bash-tools.exec.pty-cleanup.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { resetProcessRegistryForTests } from "./bash-process-registry";
|
||||
import { createExecTool } from "./bash-tools.exec";
|
||||
|
||||
const { ptySpawnMock } = vi.hoisted(() => ({
|
||||
ptySpawnMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@lydell/node-pty", () => ({
|
||||
spawn: (...args: unknown[]) => ptySpawnMock(...args),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
resetProcessRegistryForTests();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("exec disposes PTY listeners after normal exit", async () => {
|
||||
const disposeData = vi.fn();
|
||||
const disposeExit = vi.fn();
|
||||
|
||||
ptySpawnMock.mockImplementation(() => ({
|
||||
pid: 0,
|
||||
write: vi.fn(),
|
||||
onData: (listener: (value: string) => void) => {
|
||||
listener("ok");
|
||||
return { dispose: disposeData };
|
||||
},
|
||||
onExit: (listener: (event: { exitCode: number; signal?: number }) => void) => {
|
||||
listener({ exitCode: 0 });
|
||||
return { dispose: disposeExit };
|
||||
},
|
||||
kill: vi.fn(),
|
||||
}));
|
||||
|
||||
const tool = createExecTool({ allowBackground: false });
|
||||
const result = await tool.execute("toolcall", {
|
||||
command: "echo ok",
|
||||
pty: true,
|
||||
});
|
||||
|
||||
expect(result.details.status).toBe("completed");
|
||||
expect(disposeData).toHaveBeenCalledTimes(1);
|
||||
expect(disposeExit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("exec tears down PTY resources on timeout", async () => {
|
||||
const disposeData = vi.fn();
|
||||
const disposeExit = vi.fn();
|
||||
const kill = vi.fn();
|
||||
|
||||
ptySpawnMock.mockImplementation(() => ({
|
||||
pid: 0,
|
||||
write: vi.fn(),
|
||||
onData: () => ({ dispose: disposeData }),
|
||||
onExit: () => ({ dispose: disposeExit }),
|
||||
kill,
|
||||
}));
|
||||
|
||||
const tool = createExecTool({ allowBackground: false });
|
||||
await expect(
|
||||
tool.execute("toolcall", {
|
||||
command: "sleep 5",
|
||||
pty: true,
|
||||
timeout: 0.01,
|
||||
}),
|
||||
).rejects.toThrow("Command timed out");
|
||||
expect(kill).toHaveBeenCalledTimes(1);
|
||||
expect(disposeData).toHaveBeenCalledTimes(1);
|
||||
expect(disposeExit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
39
src/agents/bash-tools.exec.pty-fallback-failure.test.ts
Normal file
39
src/agents/bash-tools.exec.pty-fallback-failure.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { listRunningSessions, resetProcessRegistryForTests } from "./bash-process-registry";
|
||||
import { createExecTool } from "./bash-tools.exec";
|
||||
|
||||
const { supervisorSpawnMock } = vi.hoisted(() => ({
|
||||
supervisorSpawnMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../process/supervisor/index.js", () => ({
|
||||
getProcessSupervisor: () => ({
|
||||
spawn: (...args: unknown[]) => supervisorSpawnMock(...args),
|
||||
cancel: vi.fn(),
|
||||
cancelScope: vi.fn(),
|
||||
reconcileOrphans: vi.fn(),
|
||||
getRecord: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
resetProcessRegistryForTests();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("exec cleans session state when PTY fallback spawn also fails", async () => {
|
||||
supervisorSpawnMock
|
||||
.mockRejectedValueOnce(new Error("pty spawn failed"))
|
||||
.mockRejectedValueOnce(new Error("child fallback failed"));
|
||||
|
||||
const tool = createExecTool({ allowBackground: false });
|
||||
|
||||
await expect(
|
||||
tool.execute("toolcall", {
|
||||
command: "echo ok",
|
||||
pty: true,
|
||||
}),
|
||||
).rejects.toThrow("child fallback failed");
|
||||
|
||||
expect(listRunningSessions()).toHaveLength(0);
|
||||
});
|
||||
152
src/agents/bash-tools.process.supervisor.test.ts
Normal file
152
src/agents/bash-tools.process.supervisor.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProcessSession } from "./bash-process-registry.js";
|
||||
import {
|
||||
addSession,
|
||||
getFinishedSession,
|
||||
getSession,
|
||||
resetProcessRegistryForTests,
|
||||
} from "./bash-process-registry.js";
|
||||
import { createProcessTool } from "./bash-tools.process.js";
|
||||
|
||||
const { supervisorMock } = vi.hoisted(() => ({
|
||||
supervisorMock: {
|
||||
spawn: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
cancelScope: vi.fn(),
|
||||
reconcileOrphans: vi.fn(),
|
||||
getRecord: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const { killProcessTreeMock } = vi.hoisted(() => ({
|
||||
killProcessTreeMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../process/supervisor/index.js", () => ({
|
||||
getProcessSupervisor: () => supervisorMock,
|
||||
}));
|
||||
|
||||
vi.mock("../process/kill-tree.js", () => ({
|
||||
killProcessTree: (...args: unknown[]) => killProcessTreeMock(...args),
|
||||
}));
|
||||
|
||||
function createBackgroundSession(id: string, pid?: number): ProcessSession {
|
||||
return {
|
||||
id,
|
||||
command: "sleep 999",
|
||||
startedAt: Date.now(),
|
||||
cwd: "/tmp",
|
||||
maxOutputChars: 10_000,
|
||||
pendingMaxOutputChars: 30_000,
|
||||
totalOutputChars: 0,
|
||||
pendingStdout: [],
|
||||
pendingStderr: [],
|
||||
pendingStdoutChars: 0,
|
||||
pendingStderrChars: 0,
|
||||
aggregated: "",
|
||||
tail: "",
|
||||
pid,
|
||||
exited: false,
|
||||
exitCode: undefined,
|
||||
exitSignal: undefined,
|
||||
truncated: false,
|
||||
backgrounded: true,
|
||||
};
|
||||
}
|
||||
|
||||
describe("process tool supervisor cancellation", () => {
|
||||
beforeEach(() => {
|
||||
supervisorMock.spawn.mockReset();
|
||||
supervisorMock.cancel.mockReset();
|
||||
supervisorMock.cancelScope.mockReset();
|
||||
supervisorMock.reconcileOrphans.mockReset();
|
||||
supervisorMock.getRecord.mockReset();
|
||||
killProcessTreeMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetProcessRegistryForTests();
|
||||
});
|
||||
|
||||
it("routes kill through supervisor when run is managed", async () => {
|
||||
supervisorMock.getRecord.mockReturnValue({
|
||||
runId: "sess",
|
||||
state: "running",
|
||||
});
|
||||
addSession(createBackgroundSession("sess"));
|
||||
const processTool = createProcessTool();
|
||||
|
||||
const result = await processTool.execute("toolcall", {
|
||||
action: "kill",
|
||||
sessionId: "sess",
|
||||
});
|
||||
|
||||
expect(supervisorMock.cancel).toHaveBeenCalledWith("sess", "manual-cancel");
|
||||
expect(getSession("sess")).toBeDefined();
|
||||
expect(getSession("sess")?.exited).toBe(false);
|
||||
expect(result.content[0]).toMatchObject({
|
||||
type: "text",
|
||||
text: "Termination requested for session sess.",
|
||||
});
|
||||
});
|
||||
|
||||
it("remove drops running session immediately when cancellation is requested", async () => {
|
||||
supervisorMock.getRecord.mockReturnValue({
|
||||
runId: "sess",
|
||||
state: "running",
|
||||
});
|
||||
addSession(createBackgroundSession("sess"));
|
||||
const processTool = createProcessTool();
|
||||
|
||||
const result = await processTool.execute("toolcall", {
|
||||
action: "remove",
|
||||
sessionId: "sess",
|
||||
});
|
||||
|
||||
expect(supervisorMock.cancel).toHaveBeenCalledWith("sess", "manual-cancel");
|
||||
expect(getSession("sess")).toBeUndefined();
|
||||
expect(getFinishedSession("sess")).toBeUndefined();
|
||||
expect(result.content[0]).toMatchObject({
|
||||
type: "text",
|
||||
text: "Removed session sess (termination requested).",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to process-tree kill when supervisor record is missing", async () => {
|
||||
supervisorMock.getRecord.mockReturnValue(undefined);
|
||||
addSession(createBackgroundSession("sess-fallback", 4242));
|
||||
const processTool = createProcessTool();
|
||||
|
||||
const result = await processTool.execute("toolcall", {
|
||||
action: "kill",
|
||||
sessionId: "sess-fallback",
|
||||
});
|
||||
|
||||
expect(killProcessTreeMock).toHaveBeenCalledWith(4242);
|
||||
expect(getSession("sess-fallback")).toBeUndefined();
|
||||
expect(getFinishedSession("sess-fallback")).toBeDefined();
|
||||
expect(result.content[0]).toMatchObject({
|
||||
type: "text",
|
||||
text: "Killed session sess-fallback.",
|
||||
});
|
||||
});
|
||||
|
||||
it("fails remove when no supervisor record and no pid is available", async () => {
|
||||
supervisorMock.getRecord.mockReturnValue(undefined);
|
||||
addSession(createBackgroundSession("sess-no-pid"));
|
||||
const processTool = createProcessTool();
|
||||
|
||||
const result = await processTool.execute("toolcall", {
|
||||
action: "remove",
|
||||
sessionId: "sess-no-pid",
|
||||
});
|
||||
|
||||
expect(killProcessTreeMock).not.toHaveBeenCalled();
|
||||
expect(getSession("sess-no-pid")).toBeDefined();
|
||||
expect(result.details).toMatchObject({ status: "failed" });
|
||||
expect(result.content[0]).toMatchObject({
|
||||
type: "text",
|
||||
text: "Unable to remove session sess-no-pid: no active supervisor run or process id.",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { formatDurationCompact } from "../infra/format-time/format-duration.ts";
|
||||
import { killProcessTree } from "../process/kill-tree.js";
|
||||
import { getProcessSupervisor } from "../process/supervisor/index.js";
|
||||
import {
|
||||
type ProcessSession,
|
||||
deleteSession,
|
||||
drainSession,
|
||||
getFinishedSession,
|
||||
@@ -11,13 +14,7 @@ import {
|
||||
markExited,
|
||||
setJobTtlMs,
|
||||
} from "./bash-process-registry.js";
|
||||
import {
|
||||
deriveSessionName,
|
||||
killSession,
|
||||
pad,
|
||||
sliceLogLines,
|
||||
truncateMiddle,
|
||||
} from "./bash-tools.shared.js";
|
||||
import { deriveSessionName, pad, sliceLogLines, truncateMiddle } from "./bash-tools.shared.js";
|
||||
import { encodeKeySequence, encodePaste } from "./pty-keys.js";
|
||||
|
||||
export type ProcessToolDefaults = {
|
||||
@@ -65,8 +62,9 @@ const processSchema = Type.Object({
|
||||
offset: Type.Optional(Type.Number({ description: "Log offset" })),
|
||||
limit: Type.Optional(Type.Number({ description: "Log length" })),
|
||||
timeout: Type.Optional(
|
||||
Type.Union([Type.Number(), Type.String()], {
|
||||
Type.Number({
|
||||
description: "For poll: wait up to this many milliseconds before returning",
|
||||
minimum: 0,
|
||||
}),
|
||||
),
|
||||
});
|
||||
@@ -106,9 +104,28 @@ export function createProcessTool(
|
||||
setJobTtlMs(defaults.cleanupMs);
|
||||
}
|
||||
const scopeKey = defaults?.scopeKey;
|
||||
const supervisor = getProcessSupervisor();
|
||||
const isInScope = (session?: { scopeKey?: string } | null) =>
|
||||
!scopeKey || session?.scopeKey === scopeKey;
|
||||
|
||||
const cancelManagedSession = (sessionId: string) => {
|
||||
const record = supervisor.getRecord(sessionId);
|
||||
if (!record || record.state === "exited") {
|
||||
return false;
|
||||
}
|
||||
supervisor.cancel(sessionId, "manual-cancel");
|
||||
return true;
|
||||
};
|
||||
|
||||
const terminateSessionFallback = (session: ProcessSession) => {
|
||||
const pid = session.pid ?? session.child?.pid;
|
||||
if (typeof pid !== "number" || !Number.isFinite(pid) || pid <= 0) {
|
||||
return false;
|
||||
}
|
||||
killProcessTree(pid);
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
name: "process",
|
||||
label: "process",
|
||||
@@ -138,7 +155,7 @@ export function createProcessTool(
|
||||
eof?: boolean;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
timeout?: number | string;
|
||||
timeout?: unknown;
|
||||
};
|
||||
|
||||
if (params.action === "list") {
|
||||
@@ -522,10 +539,25 @@ export function createProcessTool(
|
||||
if (!scopedSession.backgrounded) {
|
||||
return failText(`Session ${params.sessionId} is not backgrounded.`);
|
||||
}
|
||||
killSession(scopedSession);
|
||||
markExited(scopedSession, null, "SIGKILL", "failed");
|
||||
const canceled = cancelManagedSession(scopedSession.id);
|
||||
if (!canceled) {
|
||||
const terminated = terminateSessionFallback(scopedSession);
|
||||
if (!terminated) {
|
||||
return failText(
|
||||
`Unable to terminate session ${params.sessionId}: no active supervisor run or process id.`,
|
||||
);
|
||||
}
|
||||
markExited(scopedSession, null, "SIGKILL", "failed");
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `Killed session ${params.sessionId}.` }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: canceled
|
||||
? `Termination requested for session ${params.sessionId}.`
|
||||
: `Killed session ${params.sessionId}.`,
|
||||
},
|
||||
],
|
||||
details: {
|
||||
status: "failed",
|
||||
name: scopedSession ? deriveSessionName(scopedSession.command) : undefined,
|
||||
@@ -554,10 +586,30 @@ export function createProcessTool(
|
||||
|
||||
case "remove": {
|
||||
if (scopedSession) {
|
||||
killSession(scopedSession);
|
||||
markExited(scopedSession, null, "SIGKILL", "failed");
|
||||
const canceled = cancelManagedSession(scopedSession.id);
|
||||
if (canceled) {
|
||||
// Keep remove semantics deterministic: drop from process registry now.
|
||||
scopedSession.backgrounded = false;
|
||||
deleteSession(params.sessionId);
|
||||
} else {
|
||||
const terminated = terminateSessionFallback(scopedSession);
|
||||
if (!terminated) {
|
||||
return failText(
|
||||
`Unable to remove session ${params.sessionId}: no active supervisor run or process id.`,
|
||||
);
|
||||
}
|
||||
markExited(scopedSession, null, "SIGKILL", "failed");
|
||||
deleteSession(params.sessionId);
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `Removed session ${params.sessionId}.` }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: canceled
|
||||
? `Removed session ${params.sessionId} (termination requested).`
|
||||
: `Removed session ${params.sessionId}.`,
|
||||
},
|
||||
],
|
||||
details: {
|
||||
status: "failed",
|
||||
name: scopedSession ? deriveSessionName(scopedSession.command) : undefined,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { sliceUtf16Safe } from "../utils.js";
|
||||
import { assertSandboxPath } from "./sandbox-paths.js";
|
||||
import { killProcessTree } from "./shell-utils.js";
|
||||
|
||||
const CHUNK_LIMIT = 8 * 1024;
|
||||
|
||||
@@ -115,13 +113,6 @@ export async function resolveSandboxWorkdir(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export function killSession(session: { pid?: number; child?: ChildProcessWithoutNullStreams }) {
|
||||
const pid = session.pid ?? session.child?.pid;
|
||||
if (pid) {
|
||||
killProcessTree(pid);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveWorkdir(workdir: string, warnings: string[]) {
|
||||
const current = safeCwd();
|
||||
const fallback = current ?? homedir();
|
||||
|
||||
@@ -2,7 +2,19 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { sleep } from "../utils.js";
|
||||
import { runClaudeCliAgent } from "./claude-cli-runner.js";
|
||||
|
||||
const runCommandWithTimeoutMock = vi.fn();
|
||||
const mocks = vi.hoisted(() => ({
|
||||
spawn: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../process/supervisor/index.js", () => ({
|
||||
getProcessSupervisor: () => ({
|
||||
spawn: (...args: unknown[]) => mocks.spawn(...args),
|
||||
cancel: vi.fn(),
|
||||
cancelScope: vi.fn(),
|
||||
reconcileOrphans: async () => {},
|
||||
getRecord: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve: (value: T) => void;
|
||||
@@ -18,6 +30,40 @@ function createDeferred<T>() {
|
||||
};
|
||||
}
|
||||
|
||||
function createManagedRun(
|
||||
exit: Promise<{
|
||||
reason: "exit" | "overall-timeout" | "no-output-timeout" | "signal" | "manual-cancel";
|
||||
exitCode: number | null;
|
||||
exitSignal: NodeJS.Signals | null;
|
||||
durationMs: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
timedOut: boolean;
|
||||
noOutputTimedOut: boolean;
|
||||
}>,
|
||||
) {
|
||||
return {
|
||||
runId: "run-test",
|
||||
pid: 12345,
|
||||
startedAtMs: Date.now(),
|
||||
wait: async () => await exit,
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function successExit(payload: { message: string; session_id: string }) {
|
||||
return {
|
||||
reason: "exit" as const,
|
||||
exitCode: 0,
|
||||
exitSignal: null,
|
||||
durationMs: 1,
|
||||
stdout: JSON.stringify(payload),
|
||||
stderr: "",
|
||||
timedOut: false,
|
||||
noOutputTimedOut: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForCalls(mockFn: { mock: { calls: unknown[][] } }, count: number) {
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
if (mockFn.mock.calls.length >= count) {
|
||||
@@ -28,23 +74,15 @@ async function waitForCalls(mockFn: { mock: { calls: unknown[][] } }, count: num
|
||||
throw new Error(`Expected ${count} calls, got ${mockFn.mock.calls.length}`);
|
||||
}
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
||||
}));
|
||||
|
||||
describe("runClaudeCliAgent", () => {
|
||||
beforeEach(() => {
|
||||
runCommandWithTimeoutMock.mockReset();
|
||||
mocks.spawn.mockReset();
|
||||
});
|
||||
|
||||
it("starts a new session with --session-id when none is provided", async () => {
|
||||
runCommandWithTimeoutMock.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({ message: "ok", session_id: "sid-1" }),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
mocks.spawn.mockResolvedValueOnce(
|
||||
createManagedRun(Promise.resolve(successExit({ message: "ok", session_id: "sid-1" }))),
|
||||
);
|
||||
|
||||
await runClaudeCliAgent({
|
||||
sessionId: "openclaw-session",
|
||||
@@ -56,21 +94,18 @@ describe("runClaudeCliAgent", () => {
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
|
||||
const argv = runCommandWithTimeoutMock.mock.calls[0]?.[0] as string[];
|
||||
expect(argv).toContain("claude");
|
||||
expect(argv).toContain("--session-id");
|
||||
expect(argv).toContain("hi");
|
||||
expect(mocks.spawn).toHaveBeenCalledTimes(1);
|
||||
const spawnInput = mocks.spawn.mock.calls[0]?.[0] as { argv: string[]; mode: string };
|
||||
expect(spawnInput.mode).toBe("child");
|
||||
expect(spawnInput.argv).toContain("claude");
|
||||
expect(spawnInput.argv).toContain("--session-id");
|
||||
expect(spawnInput.argv).toContain("hi");
|
||||
});
|
||||
|
||||
it("uses --resume when a claude session id is provided", async () => {
|
||||
runCommandWithTimeoutMock.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({ message: "ok", session_id: "sid-2" }),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
mocks.spawn.mockResolvedValueOnce(
|
||||
createManagedRun(Promise.resolve(successExit({ message: "ok", session_id: "sid-2" }))),
|
||||
);
|
||||
|
||||
await runClaudeCliAgent({
|
||||
sessionId: "openclaw-session",
|
||||
@@ -83,32 +118,21 @@ describe("runClaudeCliAgent", () => {
|
||||
claudeSessionId: "c9d7b831-1c31-4d22-80b9-1e50ca207d4b",
|
||||
});
|
||||
|
||||
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
|
||||
const argv = runCommandWithTimeoutMock.mock.calls[0]?.[0] as string[];
|
||||
expect(argv).toContain("--resume");
|
||||
expect(argv).toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
|
||||
expect(argv).toContain("hi");
|
||||
expect(mocks.spawn).toHaveBeenCalledTimes(1);
|
||||
const spawnInput = mocks.spawn.mock.calls[0]?.[0] as { argv: string[] };
|
||||
expect(spawnInput.argv).toContain("--resume");
|
||||
expect(spawnInput.argv).toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
|
||||
expect(spawnInput.argv).not.toContain("--session-id");
|
||||
expect(spawnInput.argv).toContain("hi");
|
||||
});
|
||||
|
||||
it("serializes concurrent claude-cli runs", async () => {
|
||||
const firstDeferred = createDeferred<{
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
killed: boolean;
|
||||
}>();
|
||||
const secondDeferred = createDeferred<{
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
killed: boolean;
|
||||
}>();
|
||||
const firstDeferred = createDeferred<ReturnType<typeof successExit>>();
|
||||
const secondDeferred = createDeferred<ReturnType<typeof successExit>>();
|
||||
|
||||
runCommandWithTimeoutMock
|
||||
.mockImplementationOnce(() => firstDeferred.promise)
|
||||
.mockImplementationOnce(() => secondDeferred.promise);
|
||||
mocks.spawn
|
||||
.mockResolvedValueOnce(createManagedRun(firstDeferred.promise))
|
||||
.mockResolvedValueOnce(createManagedRun(secondDeferred.promise));
|
||||
|
||||
const firstRun = runClaudeCliAgent({
|
||||
sessionId: "s1",
|
||||
@@ -130,25 +154,13 @@ describe("runClaudeCliAgent", () => {
|
||||
runId: "run-2",
|
||||
});
|
||||
|
||||
await waitForCalls(runCommandWithTimeoutMock, 1);
|
||||
await waitForCalls(mocks.spawn, 1);
|
||||
|
||||
firstDeferred.resolve({
|
||||
stdout: JSON.stringify({ message: "ok", session_id: "sid-1" }),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
firstDeferred.resolve(successExit({ message: "ok", session_id: "sid-1" }));
|
||||
|
||||
await waitForCalls(runCommandWithTimeoutMock, 2);
|
||||
await waitForCalls(mocks.spawn, 2);
|
||||
|
||||
secondDeferred.resolve({
|
||||
stdout: JSON.stringify({ message: "ok", session_id: "sid-2" }),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
secondDeferred.resolve(successExit({ message: "ok", session_id: "sid-2" }));
|
||||
|
||||
await Promise.all([firstRun, secondRun]);
|
||||
});
|
||||
|
||||
36
src/agents/cli-backends.test.ts
Normal file
36
src/agents/cli-backends.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveCliBackendConfig } from "./cli-backends.js";
|
||||
|
||||
describe("resolveCliBackendConfig reliability merge", () => {
|
||||
it("deep-merges reliability watchdog overrides for codex", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
cliBackends: {
|
||||
"codex-cli": {
|
||||
command: "codex",
|
||||
reliability: {
|
||||
watchdog: {
|
||||
resume: {
|
||||
noOutputTimeoutMs: 42_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const resolved = resolveCliBackendConfig("codex-cli", cfg);
|
||||
|
||||
expect(resolved).not.toBeNull();
|
||||
expect(resolved?.config.reliability?.watchdog?.resume?.noOutputTimeoutMs).toBe(42_000);
|
||||
// Ensure defaults are retained when only one field is overridden.
|
||||
expect(resolved?.config.reliability?.watchdog?.resume?.noOutputTimeoutRatio).toBe(0.3);
|
||||
expect(resolved?.config.reliability?.watchdog?.resume?.minMs).toBe(60_000);
|
||||
expect(resolved?.config.reliability?.watchdog?.resume?.maxMs).toBe(180_000);
|
||||
expect(resolved?.config.reliability?.watchdog?.fresh?.noOutputTimeoutRatio).toBe(0.8);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user