mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
400 Commits
v2026.5.3
...
codex/gate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
659cc50f20 | ||
|
|
cc1e702046 | ||
|
|
35d449f998 | ||
|
|
83b9488419 | ||
|
|
8e1be8c317 | ||
|
|
43b5df7295 | ||
|
|
a7b665cfed | ||
|
|
d0cae0d950 | ||
|
|
1c52447f0b | ||
|
|
a4f2bf273a | ||
|
|
5005f5b22e | ||
|
|
4556707cb7 | ||
|
|
0909df1a4f | ||
|
|
86385f72e9 | ||
|
|
828b6be39d | ||
|
|
14b5f73e2a | ||
|
|
29a3e71106 | ||
|
|
ed1089f822 | ||
|
|
7c0f5463a5 | ||
|
|
fdaa5a0c3d | ||
|
|
02ac7dc5a6 | ||
|
|
a9817a5f97 | ||
|
|
e2eb8e3cfe | ||
|
|
a71f906837 | ||
|
|
59b5058cdb | ||
|
|
4820b701a5 | ||
|
|
0fc8afeac9 | ||
|
|
112924b113 | ||
|
|
b63336186a | ||
|
|
be8b4dc845 | ||
|
|
7b86481c94 | ||
|
|
06056926a0 | ||
|
|
021373a454 | ||
|
|
982d123b80 | ||
|
|
4fab34a63b | ||
|
|
3af3fcfebe | ||
|
|
3fb8c405ed | ||
|
|
ef0dbcf49d | ||
|
|
f2efe33afc | ||
|
|
8b2bf7b2e9 | ||
|
|
f368201790 | ||
|
|
8cf1800ee9 | ||
|
|
5de7f99801 | ||
|
|
94f8f1914e | ||
|
|
2e399e6f1a | ||
|
|
3921e1b0b7 | ||
|
|
a3f6f24b79 | ||
|
|
2d849bbafa | ||
|
|
ee314e4236 | ||
|
|
de4903ec7a | ||
|
|
9aad2b82c3 | ||
|
|
8c7ec5d1f9 | ||
|
|
edddb07f20 | ||
|
|
dff437a1cb | ||
|
|
417660b662 | ||
|
|
daefb5e341 | ||
|
|
9dc38f37ea | ||
|
|
841eb81baf | ||
|
|
fc7e2a10c8 | ||
|
|
2511be5244 | ||
|
|
74ab62c6a2 | ||
|
|
103cdd9d96 | ||
|
|
0e702f1063 | ||
|
|
c240e718e9 | ||
|
|
7b8315d18e | ||
|
|
ea75cd8971 | ||
|
|
37c0520a0b | ||
|
|
30e259b9c5 | ||
|
|
9008031e96 | ||
|
|
6c2573e37a | ||
|
|
2fe2dbdb7d | ||
|
|
3d3b0dad77 | ||
|
|
15b9966781 | ||
|
|
0dd30c804c | ||
|
|
fa1d826a41 | ||
|
|
7c6bf331b8 | ||
|
|
4f2f5e0461 | ||
|
|
48a3a23d40 | ||
|
|
40f92b8d78 | ||
|
|
981767516d | ||
|
|
03d04c243b | ||
|
|
c1da0ddd54 | ||
|
|
1df2ac442a | ||
|
|
cb38535875 | ||
|
|
e3364ae3bd | ||
|
|
d5edeae6ee | ||
|
|
89db1e5440 | ||
|
|
8afc9ef73c | ||
|
|
042d7b8823 | ||
|
|
fc1f1f4fdf | ||
|
|
0b3a86cab0 | ||
|
|
5f373ae4d3 | ||
|
|
a90be474f4 | ||
|
|
c59c20e9fd | ||
|
|
1ce136ce16 | ||
|
|
909894c8c4 | ||
|
|
df7d18f6d3 | ||
|
|
2db259503b | ||
|
|
4abba333fe | ||
|
|
0909ff16d9 | ||
|
|
87e3f3779f | ||
|
|
863e8d0c38 | ||
|
|
ea8d5b1877 | ||
|
|
e069675c1d | ||
|
|
47b7df3c5d | ||
|
|
7c696e0e73 | ||
|
|
510a2dc80c | ||
|
|
ad534fdb1b | ||
|
|
bd183072e4 | ||
|
|
1d16ce3f24 | ||
|
|
a68c6e20e9 | ||
|
|
8469a51326 | ||
|
|
14f756c05b | ||
|
|
626e078863 | ||
|
|
a7c5a04259 | ||
|
|
d5b0083300 | ||
|
|
5efbb3078a | ||
|
|
a9f1882047 | ||
|
|
24ec2aebe8 | ||
|
|
57f9a558e4 | ||
|
|
97d35f4c57 | ||
|
|
23eb44b045 | ||
|
|
e0430e2e15 | ||
|
|
51d3ec7395 | ||
|
|
4c40686f9e | ||
|
|
89a15fddaf | ||
|
|
b8f6e16ba5 | ||
|
|
feb9a5af6a | ||
|
|
3434cfa381 | ||
|
|
54300e5270 | ||
|
|
33e19fb5ae | ||
|
|
6b7f9eafed | ||
|
|
061af13bf3 | ||
|
|
04aa4a3fe6 | ||
|
|
1f724bc50b | ||
|
|
5d9752ba18 | ||
|
|
05d6c62152 | ||
|
|
b7ce9439e7 | ||
|
|
dade5f9133 | ||
|
|
098b72910d | ||
|
|
5397667272 | ||
|
|
b37fba7c07 | ||
|
|
5b528f4dfe | ||
|
|
304fa098f2 | ||
|
|
88b21427f8 | ||
|
|
7482754aca | ||
|
|
474bea162b | ||
|
|
be41b8cbc7 | ||
|
|
a9282f3571 | ||
|
|
23950b5664 | ||
|
|
9b95e477be | ||
|
|
baecb6b4d6 | ||
|
|
6e8cdd7d59 | ||
|
|
da1e1435ad | ||
|
|
43bdb886e9 | ||
|
|
fcb396bf65 | ||
|
|
071db2ca69 | ||
|
|
a1304c92c6 | ||
|
|
281b5bd511 | ||
|
|
be21d64d08 | ||
|
|
f0537e93fb | ||
|
|
9efbae7acd | ||
|
|
03ad3c0684 | ||
|
|
ef79347763 | ||
|
|
e5f5989aa9 | ||
|
|
b31c001a2b | ||
|
|
e622223bcd | ||
|
|
e8d0cf75ea | ||
|
|
87e3b1a241 | ||
|
|
f2e7f33d69 | ||
|
|
cf03fe6b6a | ||
|
|
e524878998 | ||
|
|
7129db1960 | ||
|
|
e11a8a84ac | ||
|
|
585ce38015 | ||
|
|
48e1256810 | ||
|
|
0f7cd6d905 | ||
|
|
2484f37378 | ||
|
|
f8d7182f81 | ||
|
|
e23f3a859c | ||
|
|
573ecd8660 | ||
|
|
17c05bbb21 | ||
|
|
b171f6e081 | ||
|
|
92a00ebef5 | ||
|
|
54f243d696 | ||
|
|
49ab43477e | ||
|
|
42abef0afb | ||
|
|
85e9af7767 | ||
|
|
dbde49f44e | ||
|
|
7c38f0997f | ||
|
|
1cbe32ef23 | ||
|
|
1621d9f27d | ||
|
|
6e9c0bfbe4 | ||
|
|
0dd1b11e83 | ||
|
|
f409b093fd | ||
|
|
c36f8f1e39 | ||
|
|
a5dcf3d300 | ||
|
|
b2efd19648 | ||
|
|
5fe8cde28f | ||
|
|
3c971255fa | ||
|
|
826786b114 | ||
|
|
fbf9132b32 | ||
|
|
cdc00614cc | ||
|
|
f4f98f45c7 | ||
|
|
8a8a12559d | ||
|
|
3f045d9129 | ||
|
|
80acedaf0a | ||
|
|
31bba9ea22 | ||
|
|
128cc2c84b | ||
|
|
605e89468e | ||
|
|
fa689295c6 | ||
|
|
deffd11a43 | ||
|
|
f29aaa2e04 | ||
|
|
86fc9e3279 | ||
|
|
3dcff3b267 | ||
|
|
d8da04e58e | ||
|
|
8412b189df | ||
|
|
92d33e4de8 | ||
|
|
bbdf1fe11c | ||
|
|
ab24e93573 | ||
|
|
c76d8f5a7c | ||
|
|
70850d15ee | ||
|
|
02f455fda3 | ||
|
|
51e847fb96 | ||
|
|
0907c60dd7 | ||
|
|
3c4f67141d | ||
|
|
21ac476904 | ||
|
|
7050af56d4 | ||
|
|
83037720d9 | ||
|
|
eeff1f7cb6 | ||
|
|
ea04e019ac | ||
|
|
38d6b43792 | ||
|
|
ac09ec00e8 | ||
|
|
361737d1f1 | ||
|
|
a224810a7f | ||
|
|
1df6226d90 | ||
|
|
a9d77b3eb0 | ||
|
|
bc0b54e844 | ||
|
|
4c68bfdb6c | ||
|
|
b6f9b5f21e | ||
|
|
cbd91676ac | ||
|
|
47134d1ce6 | ||
|
|
5ab18100e2 | ||
|
|
ccb94a6282 | ||
|
|
e80de466e5 | ||
|
|
8f75a4ebdf | ||
|
|
36bab71abc | ||
|
|
1d935cce51 | ||
|
|
5a6cedc14a | ||
|
|
e2f4aa4617 | ||
|
|
18db16471b | ||
|
|
20ade148be | ||
|
|
66267b5435 | ||
|
|
705bde4594 | ||
|
|
3d0563dee2 | ||
|
|
a6d67ccf29 | ||
|
|
1bf824f586 | ||
|
|
dcb3e64e2f | ||
|
|
0fcf2c64c0 | ||
|
|
7be29b2801 | ||
|
|
f2d9b2c493 | ||
|
|
1360cec546 | ||
|
|
117364e2b9 | ||
|
|
cf1991d27d | ||
|
|
7d26fb32a7 | ||
|
|
809f5ae150 | ||
|
|
5eac4686aa | ||
|
|
1a573d33bc | ||
|
|
d60eef3b74 | ||
|
|
dd83f72a7f | ||
|
|
8d6db59cf7 | ||
|
|
7d98e7f1fe | ||
|
|
b2f2185348 | ||
|
|
0c1df35315 | ||
|
|
309ff6bada | ||
|
|
7fc9a82dca | ||
|
|
19f948af2e | ||
|
|
796d4ab43d | ||
|
|
65f2c2a0db | ||
|
|
b5d408cd69 | ||
|
|
654b70dde8 | ||
|
|
32b4d1ec8a | ||
|
|
dadf0005ec | ||
|
|
b2f0f67e0d | ||
|
|
472763238d | ||
|
|
71c7232764 | ||
|
|
2949171fcc | ||
|
|
e9ca63cf06 | ||
|
|
90d25d59c6 | ||
|
|
9dc3271efb | ||
|
|
09e7eb6687 | ||
|
|
3f732aee83 | ||
|
|
02b9dbde39 | ||
|
|
12af95a55e | ||
|
|
f42a2c738c | ||
|
|
5ca0aa1d15 | ||
|
|
973e240bb3 | ||
|
|
e3cba91ef0 | ||
|
|
a8b38bb742 | ||
|
|
616a4e9782 | ||
|
|
fe107d5256 | ||
|
|
484195d14e | ||
|
|
cf40284544 | ||
|
|
a4df85e55f | ||
|
|
6f8b9bb573 | ||
|
|
143db94701 | ||
|
|
a90bc434dd | ||
|
|
c52b5657a2 | ||
|
|
9cc802241c | ||
|
|
11c600cf19 | ||
|
|
51fea3826a | ||
|
|
eb3922f1a5 | ||
|
|
8846fe0998 | ||
|
|
bc924889be | ||
|
|
0b6db06d7d | ||
|
|
ac00d7882a | ||
|
|
a3c36a0931 | ||
|
|
f632f5e60b | ||
|
|
471489159b | ||
|
|
c956946b26 | ||
|
|
571d75aab3 | ||
|
|
eeed33e61e | ||
|
|
30b201eff0 | ||
|
|
b0b5983ce3 | ||
|
|
be438cf887 | ||
|
|
7e296aef4b | ||
|
|
708c7cd2e2 | ||
|
|
50da306c0a | ||
|
|
111df161df | ||
|
|
1fe2b8b548 | ||
|
|
7b29fc36c3 | ||
|
|
18bd7b60e4 | ||
|
|
36f8a8603d | ||
|
|
5be66ca648 | ||
|
|
45cfe1dfa1 | ||
|
|
1c2eda206e | ||
|
|
90c0edcb61 | ||
|
|
56b83230df | ||
|
|
01a22d4ec9 | ||
|
|
c979ed3a3a | ||
|
|
0659c58df8 | ||
|
|
fcfb6500da | ||
|
|
df39e611f8 | ||
|
|
828d071ada | ||
|
|
57c37ef933 | ||
|
|
eb1a0aa574 | ||
|
|
3a8ea14fe3 | ||
|
|
a04d9060d3 | ||
|
|
857580108d | ||
|
|
8e79392dcc | ||
|
|
9b397b414a | ||
|
|
642e1dfcdf | ||
|
|
dfadf03e1f | ||
|
|
c151573f4c | ||
|
|
b0f947f61c | ||
|
|
c1db7df2ea | ||
|
|
0362f64eac | ||
|
|
786fdeb366 | ||
|
|
d5ecee2cf3 | ||
|
|
5ef1885ce3 | ||
|
|
36bcf88ffc | ||
|
|
9c3b7b7b15 | ||
|
|
4856cbb017 | ||
|
|
ecab09870a | ||
|
|
0468ebe200 | ||
|
|
08762aa290 | ||
|
|
0633cb4504 | ||
|
|
d85fa16e8f | ||
|
|
ecec68d06d | ||
|
|
2b01bcf6c8 | ||
|
|
53426cf611 | ||
|
|
1be1131631 | ||
|
|
a8467c9fce | ||
|
|
419bcd26f0 | ||
|
|
2493ab1978 | ||
|
|
eb66def656 | ||
|
|
5d09b4b92c | ||
|
|
0fa70f5a47 | ||
|
|
57b2d29761 | ||
|
|
9c37cfcbdb | ||
|
|
9799e412f8 | ||
|
|
b13e9f1864 | ||
|
|
c42a349b42 | ||
|
|
5f416f09f6 | ||
|
|
f5927cbb43 | ||
|
|
40b8d52240 | ||
|
|
443f7035a2 | ||
|
|
c308d04bca | ||
|
|
8ea04f994a | ||
|
|
12d90a26f7 | ||
|
|
71f55214ec | ||
|
|
05d11a4318 | ||
|
|
f1340be051 | ||
|
|
52dbc4d680 | ||
|
|
e782f47eca | ||
|
|
4dc2aedb76 | ||
|
|
ecd562b2b5 | ||
|
|
34b3471f85 | ||
|
|
f88e1f4c1c | ||
|
|
d057a308f3 |
@@ -14,7 +14,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
|
||||
- Stable `2026.3.12` pre-upgrade diagnostics may require a plain `gateway status --deep` fallback.
|
||||
- Treat `precheck=latest-ref-fail` on that stable pre-upgrade lane as baseline, not automatically a regression.
|
||||
- Pass `--json` for machine-readable summaries.
|
||||
- Per-phase logs land under `/tmp/openclaw-parallels-*`.
|
||||
- Per-phase logs land under `.artifacts/parallels/openclaw-parallels-*` by default. Override with `OPENCLAW_PARALLELS_ARTIFACT_ROOT` when a run needs another artifact volume.
|
||||
- Do not run local and gateway agent turns in parallel on the same fresh workspace or session.
|
||||
- Hard-cap every top-level Parallels lane with host `timeout --foreground` (or `gtimeout --foreground` if that is the available binary) so a stalled install, snapshot switch, or `prlctl exec` transport cannot consume the rest of the testing window. Defaults:
|
||||
- macOS: `75m`
|
||||
@@ -68,8 +68,16 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
|
||||
- The Windows same-guest update helper should write stage markers to its log before long steps like tgz download and `npm install -g` so the outer progress monitor does not sit on `waiting for first log line` during healthy but quiet installs.
|
||||
- Linux same-guest update verification should also export `HOME=/root`, pass `OPENAI_API_KEY` via `prlctl exec ... /usr/bin/env`, and use `openclaw agent --local`; the fresh Linux baseline does not rely on persisted gateway credentials.
|
||||
- The npm-update wrapper now prints per-lane progress from the nested log files. If a lane still looks stuck, inspect the nested logs in `runDir` first (`macos-fresh.log`, `windows-fresh.log`, `linux-fresh.log`, `macos-update.log`, `windows-update.log`, `linux-update.log`) instead of assuming the outer wrapper hung.
|
||||
- If the wrapper fails a lane, read the auto-dumped tail first, then the full nested lane log under `/tmp/openclaw-parallels-npm-update.*`.
|
||||
- Each run writes both `summary.json` and `summary.md`; read the markdown first for quick human triage, then the JSON/timings for automation.
|
||||
- For full beta validation after a tag is published, prefer one command:
|
||||
- `timeout --foreground 150m pnpm test:parallels:npm-update -- --beta-validation beta3 --json`
|
||||
This resolves `beta3` to the latest `*-beta.3` version, runs latest->that-version same-guest update coverage, and then runs fresh install smoke for that exact published target on the same selected OS matrix. Use `--platform macos|windows|linux` to narrow reruns.
|
||||
- For beta 4 npm validation with agent turns, the known-good shape is:
|
||||
- `gtimeout --foreground 150m pnpm test:parallels:npm-update -- --beta-validation beta4 --model openai/gpt-5.4 --json`
|
||||
Prefer the explicit `beta4` alias over `openclaw@beta` when validating a specific prerelease number; npm tags can move.
|
||||
- If the wrapper fails a lane, read the auto-dumped tail first, then the full nested lane log under `.artifacts/parallels/openclaw-parallels-npm-update.*`.
|
||||
- Current known macOS update-lane transport signature when the fallback is missing or bypassed: `Unable to authenticate the user. Make sure that the specified credentials are correct and try again.` Treat that as Parallels current-user authentication before blaming npm or OpenClaw.
|
||||
- A macOS packaged fresh install with global package directories or bundled files mode `0777` usually means the harness used the root `prlctl exec` fallback under a permissive umask. The POSIX guest transports should prepend `umask 022`; verify the phase preflight line before blaming npm.
|
||||
|
||||
## CLI invocation footgun
|
||||
|
||||
|
||||
@@ -139,6 +139,20 @@ pnpm test:docker:npm-telegram-live
|
||||
- `OPENCLAW_QA_CONVEX_SITE_URL`
|
||||
- `OPENCLAW_QA_CONVEX_SECRET_MAINTAINER`
|
||||
- `OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE=mock-openai`
|
||||
- If direct Telegram env is missing locally and `op signin` blocks, prefer dispatching the manual GitHub lane because the `qa-live-shared` environment already has Convex CI credentials:
|
||||
|
||||
```bash
|
||||
gh workflow run "NPM Telegram Beta E2E" --repo openclaw/openclaw --ref main \
|
||||
-f package_spec=openclaw@YYYY.M.D-beta.N \
|
||||
-f package_label=openclaw@YYYY.M.D-beta.N \
|
||||
-f provider_mode=mock-openai
|
||||
```
|
||||
|
||||
- Poll the exact run id from the dispatch URL. `gh run view --json artifacts` is not supported; list artifacts with:
|
||||
|
||||
```bash
|
||||
gh api repos/openclaw/openclaw/actions/runs/<run-id>/artifacts
|
||||
```
|
||||
|
||||
## Character evals
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ on:
|
||||
- qa-live
|
||||
- npm-telegram
|
||||
live_suite_filter:
|
||||
description: Optional exact live suite id for focused live/E2E reruns; blank runs all selected live suites
|
||||
description: Optional exact live/E2E suite id, or comma-separated QA live lanes such as qa-live-matrix,qa-live-telegram; blank runs all selected live suites
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -274,7 +274,7 @@ jobs:
|
||||
|
||||
const activePrLimitLabel = "r: too-many-prs";
|
||||
const activePrLimitOverrideLabel = "r: too-many-prs-override";
|
||||
const activePrLimit = 10;
|
||||
const activePrLimit = 20;
|
||||
const labelColor = "B60205";
|
||||
const labelDescription = `Author has more than ${activePrLimit} active PRs in this repo`;
|
||||
const authorLogin = pullRequest.user?.login;
|
||||
|
||||
@@ -255,6 +255,24 @@ jobs:
|
||||
- name: Build Mantis harness
|
||||
run: pnpm build
|
||||
|
||||
- name: Setup Go for Crabbox CLI
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26.x"
|
||||
cache: false
|
||||
|
||||
- name: Install Crabbox CLI
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
install_dir="${RUNNER_TEMP}/crabbox"
|
||||
mkdir -p "$install_dir" "$HOME/.local/bin"
|
||||
git clone --depth 1 https://github.com/openclaw/crabbox.git "$install_dir/src"
|
||||
go build -C "$install_dir/src" -o "$HOME/.local/bin/crabbox" ./cmd/crabbox
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
"$HOME/.local/bin/crabbox" --version
|
||||
"$HOME/.local/bin/crabbox" warmup --help 2>&1 | grep -q -- "-desktop"
|
||||
|
||||
- name: Prepare baseline and candidate worktrees
|
||||
shell: bash
|
||||
env:
|
||||
@@ -285,6 +303,12 @@ jobs:
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_DISCORD_CAPTURE_CONTENT: "1"
|
||||
CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR }}
|
||||
CRABBOX_COORDINATOR_TOKEN: ${{ secrets.CRABBOX_COORDINATOR_TOKEN }}
|
||||
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }}
|
||||
OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }}
|
||||
CRABBOX_ACCESS_CLIENT_ID: ${{ secrets.CRABBOX_ACCESS_CLIENT_ID }}
|
||||
CRABBOX_ACCESS_CLIENT_SECRET: ${{ secrets.CRABBOX_ACCESS_CLIENT_SECRET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -296,9 +320,14 @@ jobs:
|
||||
fi
|
||||
}
|
||||
|
||||
CRABBOX_COORDINATOR="${CRABBOX_COORDINATOR:-${OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR:-}}"
|
||||
CRABBOX_COORDINATOR_TOKEN="${CRABBOX_COORDINATOR_TOKEN:-${OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN:-}}"
|
||||
export CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN
|
||||
|
||||
require_var OPENAI_API_KEY
|
||||
require_var OPENCLAW_QA_CONVEX_SITE_URL
|
||||
require_var OPENCLAW_QA_CONVEX_SECRET_CI
|
||||
require_var CRABBOX_COORDINATOR_TOKEN
|
||||
|
||||
root=".artifacts/qa-e2e/mantis/discord-status-reactions"
|
||||
worktree_root=".artifacts/qa-e2e/mantis/discord-status-reactions-worktrees"
|
||||
@@ -328,6 +357,55 @@ jobs:
|
||||
run_lane baseline
|
||||
run_lane candidate
|
||||
|
||||
desktop_lease_id=""
|
||||
warmup_output="$(
|
||||
crabbox warmup \
|
||||
--provider hetzner \
|
||||
--desktop \
|
||||
--browser \
|
||||
--class standard \
|
||||
--idle-timeout 30m \
|
||||
--ttl 90m
|
||||
)"
|
||||
printf '%s\n' "$warmup_output" | tee "$root/crabbox-desktop-warmup.log"
|
||||
desktop_lease_id="$(printf '%s\n' "$warmup_output" | grep -Eo 'cbx_[a-f0-9]+' | head -n 1 || true)"
|
||||
if [[ ! "$desktop_lease_id" =~ ^cbx_[a-f0-9]+$ ]]; then
|
||||
echo "Crabbox desktop warmup did not return a lease id." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cleanup_desktop_lease() {
|
||||
if [[ -n "$desktop_lease_id" ]]; then
|
||||
crabbox stop --provider hetzner "$desktop_lease_id" || true
|
||||
fi
|
||||
}
|
||||
trap cleanup_desktop_lease EXIT
|
||||
|
||||
capture_desktop_lane() {
|
||||
local lane="$1"
|
||||
local html_file="$root/$lane/discord-status-reactions-tool-only-timeline.html"
|
||||
local desktop_dir="$root/$lane/desktop-browser"
|
||||
if [[ ! -f "$html_file" ]]; then
|
||||
echo "Missing desktop source HTML for ${lane}: ${html_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
local args=(
|
||||
openclaw qa mantis desktop-browser-smoke
|
||||
--html-file "$html_file"
|
||||
--output-dir "$desktop_dir"
|
||||
--provider hetzner
|
||||
--class standard
|
||||
--idle-timeout 30m
|
||||
--ttl 90m
|
||||
--lease-id "$desktop_lease_id"
|
||||
)
|
||||
pnpm "${args[@]}"
|
||||
cp "$desktop_dir/desktop-browser-smoke.png" "$root/$lane/discord-status-reactions-tool-only-desktop.png"
|
||||
}
|
||||
|
||||
capture_desktop_lane baseline
|
||||
capture_desktop_lane candidate
|
||||
|
||||
baseline_status="$(jq -r '.scenarios[0].status' "$root/baseline/discord-qa-summary.json")"
|
||||
candidate_status="$(jq -r '.scenarios[0].status' "$root/candidate/discord-qa-summary.json")"
|
||||
|
||||
@@ -351,6 +429,8 @@ jobs:
|
||||
echo "- Candidate status: \`${candidate_status}\`"
|
||||
echo "- Baseline screenshot: \`baseline/discord-status-reactions-tool-only-timeline.png\`"
|
||||
echo "- Candidate screenshot: \`candidate/discord-status-reactions-tool-only-timeline.png\`"
|
||||
echo "- Baseline desktop screenshot: \`baseline/discord-status-reactions-tool-only-desktop.png\`"
|
||||
echo "- Candidate desktop screenshot: \`candidate/discord-status-reactions-tool-only-desktop.png\`"
|
||||
} > "$root/mantis-report.md"
|
||||
|
||||
cat "$root/mantis-report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -366,7 +446,7 @@ jobs:
|
||||
|
||||
- name: Upload Mantis status reaction artifacts
|
||||
id: upload_artifact
|
||||
if: always()
|
||||
if: ${{ always() && steps.run_mantis.outputs.output_dir != '' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mantis-discord-status-reactions-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
@@ -409,7 +489,9 @@ jobs:
|
||||
for required in \
|
||||
"$root/comparison.json" \
|
||||
"$root/baseline/discord-status-reactions-tool-only-timeline.png" \
|
||||
"$root/candidate/discord-status-reactions-tool-only-timeline.png"
|
||||
"$root/candidate/discord-status-reactions-tool-only-timeline.png" \
|
||||
"$root/baseline/discord-status-reactions-tool-only-desktop.png" \
|
||||
"$root/candidate/discord-status-reactions-tool-only-desktop.png"
|
||||
do
|
||||
if [[ ! -f "$required" ]]; then
|
||||
echo "Missing required QA evidence file: $required" >&2
|
||||
@@ -435,6 +517,8 @@ jobs:
|
||||
mkdir -p "$artifacts_worktree/$artifact_root"
|
||||
cp "$root/baseline/discord-status-reactions-tool-only-timeline.png" "$artifacts_worktree/$artifact_root/baseline.png"
|
||||
cp "$root/candidate/discord-status-reactions-tool-only-timeline.png" "$artifacts_worktree/$artifact_root/candidate.png"
|
||||
cp "$root/baseline/discord-status-reactions-tool-only-desktop.png" "$artifacts_worktree/$artifact_root/baseline-desktop.png"
|
||||
cp "$root/candidate/discord-status-reactions-tool-only-desktop.png" "$artifacts_worktree/$artifact_root/candidate-desktop.png"
|
||||
cp "$root/comparison.json" "$artifacts_worktree/$artifact_root/comparison.json"
|
||||
cp "$root/mantis-report.md" "$artifacts_worktree/$artifact_root/mantis-report.md"
|
||||
|
||||
@@ -470,6 +554,10 @@ jobs:
|
||||
| --- | --- |
|
||||
| <img src="${raw_base}/baseline.png" width="420" alt="Baseline Discord status reaction timeline"> | <img src="${raw_base}/candidate.png" width="420" alt="Candidate Discord status reaction timeline"> |
|
||||
|
||||
| Baseline desktop/VNC browser | Candidate desktop/VNC browser |
|
||||
| --- | --- |
|
||||
| <img src="${raw_base}/baseline-desktop.png" width="420" alt="Baseline Mantis desktop browser screenshot"> | <img src="${raw_base}/candidate-desktop.png" width="420" alt="Candidate Mantis desktop browser screenshot"> |
|
||||
|
||||
Raw QA files: https://github.com/${GITHUB_REPOSITORY}/tree/qa-artifacts/${artifact_root}
|
||||
EOF
|
||||
|
||||
|
||||
17
.github/workflows/npm-telegram-beta-e2e.yml
vendored
17
.github/workflows/npm-telegram-beta-e2e.yml
vendored
@@ -220,6 +220,23 @@ jobs:
|
||||
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
|
||||
export OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="${output_dir}"
|
||||
|
||||
append_telegram_summary() {
|
||||
local status=$?
|
||||
local report="${output_dir}/telegram-qa-report.md"
|
||||
if [[ -n "${GITHUB_STEP_SUMMARY:-}" && -f "${report}" ]]; then
|
||||
{
|
||||
echo "## Package Telegram E2E"
|
||||
echo
|
||||
echo "- Package: ${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL:-${OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC}}"
|
||||
echo "- Provider mode: ${OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE}"
|
||||
echo
|
||||
cat "${report}"
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
return "${status}"
|
||||
}
|
||||
trap append_telegram_summary EXIT
|
||||
|
||||
if [[ -n "${PACKAGE_ARTIFACT_NAME// }" ]]; then
|
||||
mapfile -t package_tgzs < <(find .artifacts/telegram-package-under-test -type f -name "*.tgz" | sort)
|
||||
if [[ "${#package_tgzs[@]}" -ne 1 ]]; then
|
||||
|
||||
@@ -409,6 +409,7 @@ jobs:
|
||||
add_profile_suite native-live-src-gateway-profiles-xai "full"
|
||||
add_profile_suite native-live-src-gateway-profiles-zai "full"
|
||||
add_profile_suite native-live-src-gateway-backends "stable full"
|
||||
add_profile_suite native-live-src-infra "stable full"
|
||||
add_profile_suite native-live-test "stable full"
|
||||
add_profile_suite native-live-extensions-l-n "full"
|
||||
add_profile_suite native-live-extensions-moonshot "full"
|
||||
@@ -817,6 +818,9 @@ jobs:
|
||||
export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}"
|
||||
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}-timings.json"
|
||||
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
|
||||
if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then
|
||||
OPENCLAW_DOCKER_BUILD_ON_MISSING=1 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
fi
|
||||
|
||||
node .release-harness/scripts/test-docker-all.mjs
|
||||
|
||||
@@ -1060,7 +1064,7 @@ jobs:
|
||||
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/targeted-${{ steps.plan.outputs.artifact_suffix }}-timings.json"
|
||||
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
|
||||
if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then
|
||||
OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
OPENCLAW_DOCKER_BUILD_ON_MISSING=1 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
fi
|
||||
export OPENCLAW_DOCKER_ALL_BUILD=0
|
||||
|
||||
@@ -1188,6 +1192,9 @@ jobs:
|
||||
export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/release-openwebui"
|
||||
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/release-openwebui-timings.json"
|
||||
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
|
||||
if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then
|
||||
OPENCLAW_DOCKER_BUILD_ON_MISSING=1 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh
|
||||
fi
|
||||
|
||||
node .release-harness/scripts/test-docker-all.mjs
|
||||
|
||||
@@ -1983,6 +1990,12 @@ jobs:
|
||||
timeout_minutes: 90
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
- suite_id: native-live-src-infra
|
||||
label: Native live infra
|
||||
command: OPENCLAW_LIVE_APNS_REACHABILITY=1 node .release-harness/scripts/test-live-shard.mjs native-live-src-infra
|
||||
timeout_minutes: 45
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
- suite_id: native-live-test
|
||||
label: Native live test harnesses
|
||||
command: node .release-harness/scripts/test-live-shard.mjs native-live-test
|
||||
|
||||
74
.github/workflows/openclaw-release-checks.yml
vendored
74
.github/workflows/openclaw-release-checks.yml
vendored
@@ -54,7 +54,7 @@ on:
|
||||
- qa-parity
|
||||
- qa-live
|
||||
live_suite_filter:
|
||||
description: Optional exact live suite id for focused live/E2E reruns; blank runs all selected live suites
|
||||
description: Optional exact live/E2E suite id, or comma-separated QA live lanes such as qa-live-matrix,qa-live-telegram; blank runs all selected live suites
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
@@ -88,6 +88,9 @@ jobs:
|
||||
release_profile: ${{ steps.inputs.outputs.release_profile }}
|
||||
rerun_group: ${{ steps.inputs.outputs.rerun_group }}
|
||||
live_suite_filter: ${{ steps.inputs.outputs.live_suite_filter }}
|
||||
qa_live_matrix_enabled: ${{ steps.inputs.outputs.qa_live_matrix_enabled }}
|
||||
qa_live_telegram_enabled: ${{ steps.inputs.outputs.qa_live_telegram_enabled }}
|
||||
qa_live_slack_enabled: ${{ steps.inputs.outputs.qa_live_slack_enabled }}
|
||||
package_acceptance_package_spec: ${{ steps.inputs.outputs.package_acceptance_package_spec }}
|
||||
steps:
|
||||
- name: Require main or release workflow ref for release checks
|
||||
@@ -205,9 +208,66 @@ jobs:
|
||||
RELEASE_PROFILE_INPUT: ${{ inputs.release_profile }}
|
||||
RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }}
|
||||
RELEASE_LIVE_SUITE_FILTER_INPUT: ${{ inputs.live_suite_filter }}
|
||||
RELEASE_QA_SLACK_LIVE_CI_ENABLED: ${{ vars.OPENCLAW_QA_SLACK_LIVE_CI_ENABLED || 'false' }}
|
||||
RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT: ${{ inputs.package_acceptance_package_spec }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
qa_live_matrix_enabled=true
|
||||
qa_live_telegram_enabled=true
|
||||
qa_live_slack_enabled=false
|
||||
qa_live_slack_ci_enabled="$(printf '%s' "$RELEASE_QA_SLACK_LIVE_CI_ENABLED" | tr '[:upper:]' '[:lower:]')"
|
||||
if [[ "$qa_live_slack_ci_enabled" != "true" && "$qa_live_slack_ci_enabled" != "1" && "$qa_live_slack_ci_enabled" != "yes" ]]; then
|
||||
qa_live_slack_ci_enabled=false
|
||||
else
|
||||
qa_live_slack_ci_enabled=true
|
||||
fi
|
||||
|
||||
filter="$(printf '%s' "$RELEASE_LIVE_SUITE_FILTER_INPUT" | tr '[:upper:]' '[:lower:]')"
|
||||
if [[ -n "${filter// }" ]]; then
|
||||
qa_filter_seen=false
|
||||
matrix_selected=false
|
||||
telegram_selected=false
|
||||
slack_selected=false
|
||||
|
||||
IFS=', ' read -r -a filter_tokens <<< "$filter"
|
||||
for token in "${filter_tokens[@]}"; do
|
||||
token="${token//$'\t'/}"
|
||||
token="${token//$'\r'/}"
|
||||
token="${token//$'\n'/}"
|
||||
[[ -z "$token" ]] && continue
|
||||
case "$token" in
|
||||
qa-live|qa-live-all|qa-all)
|
||||
qa_filter_seen=true
|
||||
matrix_selected=true
|
||||
telegram_selected=true
|
||||
;;
|
||||
qa-live-non-slack|qa-non-slack|non-slack|no-slack|without-slack)
|
||||
qa_filter_seen=true
|
||||
matrix_selected=true
|
||||
telegram_selected=true
|
||||
;;
|
||||
qa-live-matrix|qa-matrix|matrix)
|
||||
qa_filter_seen=true
|
||||
matrix_selected=true
|
||||
;;
|
||||
qa-live-telegram|qa-telegram|telegram)
|
||||
qa_filter_seen=true
|
||||
telegram_selected=true
|
||||
;;
|
||||
qa-live-slack|qa-slack|slack)
|
||||
qa_filter_seen=true
|
||||
slack_selected="$qa_live_slack_ci_enabled"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$qa_filter_seen" == "true" ]]; then
|
||||
qa_live_matrix_enabled="$matrix_selected"
|
||||
qa_live_telegram_enabled="$telegram_selected"
|
||||
qa_live_slack_enabled="$slack_selected"
|
||||
fi
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'ref=%s\n' "$RELEASE_REF_INPUT"
|
||||
printf 'provider=%s\n' "$RELEASE_PROVIDER_INPUT"
|
||||
@@ -215,6 +275,9 @@ jobs:
|
||||
printf 'release_profile=%s\n' "$RELEASE_PROFILE_INPUT"
|
||||
printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT"
|
||||
printf 'live_suite_filter=%s\n' "$RELEASE_LIVE_SUITE_FILTER_INPUT"
|
||||
printf 'qa_live_matrix_enabled=%s\n' "$qa_live_matrix_enabled"
|
||||
printf 'qa_live_telegram_enabled=%s\n' "$qa_live_telegram_enabled"
|
||||
printf 'qa_live_slack_enabled=%s\n' "$qa_live_slack_enabled"
|
||||
printf 'package_acceptance_package_spec=%s\n' "$RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -243,6 +306,7 @@ jobs:
|
||||
if [[ -n "${RELEASE_LIVE_SUITE_FILTER// }" ]]; then
|
||||
echo "- Live suite filter: \`${RELEASE_LIVE_SUITE_FILTER}\`"
|
||||
fi
|
||||
echo "- QA live lanes: Matrix \`${{ steps.inputs.outputs.qa_live_matrix_enabled }}\`, Telegram \`${{ steps.inputs.outputs.qa_live_telegram_enabled }}\`, Slack \`${{ steps.inputs.outputs.qa_live_slack_enabled }}\`"
|
||||
if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Package Acceptance package spec: \`${PACKAGE_ACCEPTANCE_PACKAGE_SPEC}\`"
|
||||
else
|
||||
@@ -462,7 +526,7 @@ jobs:
|
||||
published_upgrade_survivor_baselines: all-since-2026.4.23
|
||||
published_upgrade_survivor_scenarios: reported-issues
|
||||
telegram_mode: mock-openai
|
||||
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-mention-gating
|
||||
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-current-session-status-tool,telegram-mention-gating
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
@@ -655,7 +719,7 @@ jobs:
|
||||
qa_live_matrix_release_checks:
|
||||
name: Run QA Lab live Matrix lane
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group)
|
||||
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_matrix_enabled == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
@@ -732,7 +796,7 @@ jobs:
|
||||
qa_live_telegram_release_checks:
|
||||
name: Run QA Lab live Telegram lane
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group)
|
||||
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_telegram_enabled == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
@@ -825,7 +889,7 @@ jobs:
|
||||
qa_live_slack_release_checks:
|
||||
name: Run QA Lab live Slack lane
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group)
|
||||
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_slack_enabled == 'true' && vars.OPENCLAW_QA_SLACK_LIVE_CI_ENABLED == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
|
||||
99
.github/workflows/plugin-clawhub-release.yml
vendored
99
.github/workflows/plugin-clawhub-release.yml
vendored
@@ -32,7 +32,7 @@ env:
|
||||
CLAWHUB_REGISTRY: "https://clawhub.ai"
|
||||
CLAWHUB_REPOSITORY: "openclaw/clawhub"
|
||||
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.
|
||||
CLAWHUB_REF: "199e6a0cdf32471702e0503e9899e8d24f06a527"
|
||||
CLAWHUB_REF: "facf20ceb6cc459e2872d941e71335a784bbc55c"
|
||||
|
||||
jobs:
|
||||
preview_plugins_clawhub:
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node environment
|
||||
@@ -62,14 +62,29 @@ jobs:
|
||||
|
||||
- name: Resolve checked-out ref
|
||||
id: ref
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate ref is on main or a release branch
|
||||
env:
|
||||
TARGET_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin \
|
||||
+refs/heads/main:refs/remotes/origin/main \
|
||||
'+refs/heads/release/*:refs/remotes/origin/release/*'
|
||||
if [[ -n "${TARGET_REF}" ]]; then
|
||||
if git rev-parse --verify --quiet "${TARGET_REF}^{commit}" >/dev/null; then
|
||||
target_sha="$(git rev-parse "${TARGET_REF}^{commit}")"
|
||||
elif git rev-parse --verify --quiet "origin/${TARGET_REF}^{commit}" >/dev/null; then
|
||||
target_sha="$(git rev-parse "origin/${TARGET_REF}^{commit}")"
|
||||
else
|
||||
echo "Unable to resolve requested publish ref: ${TARGET_REF}" >&2
|
||||
exit 1
|
||||
fi
|
||||
git checkout --detach "${target_sha}"
|
||||
fi
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate ref is on main or a release branch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git merge-base --is-ancestor HEAD origin/main; then
|
||||
exit 0
|
||||
fi
|
||||
@@ -153,6 +168,12 @@ jobs:
|
||||
echo "::error::One or more selected plugin versions already exist on ClawHub. Bump the version before running a real publish."
|
||||
exit 1
|
||||
|
||||
- name: Verify OpenClaw ClawHub package ownership
|
||||
if: steps.plan.outputs.has_candidates == 'true'
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
run: node --import tsx scripts/plugin-clawhub-owner-preflight.ts .local/plugin-clawhub-release-plan.json
|
||||
|
||||
preview_plugin_pack:
|
||||
needs: preview_plugins_clawhub
|
||||
if: needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
|
||||
@@ -161,7 +182,7 @@ jobs:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
max-parallel: 6
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
|
||||
steps:
|
||||
@@ -169,8 +190,18 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout target revision
|
||||
env:
|
||||
TARGET_SHA: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin \
|
||||
+refs/heads/main:refs/remotes/origin/main \
|
||||
'+refs/heads/release/*:refs/remotes/origin/release/*'
|
||||
git checkout --detach "${TARGET_SHA}"
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
@@ -185,9 +216,15 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ env.CLAWHUB_REPOSITORY }}
|
||||
ref: ${{ env.CLAWHUB_REF }}
|
||||
ref: main
|
||||
path: clawhub-source
|
||||
fetch-depth: 1
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout pinned ClawHub CLI revision
|
||||
working-directory: clawhub-source
|
||||
env:
|
||||
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
|
||||
run: git checkout --detach "${CLAWHUB_REF}"
|
||||
|
||||
- name: Install ClawHub CLI dependencies
|
||||
working-directory: clawhub-source
|
||||
@@ -203,6 +240,9 @@ jobs:
|
||||
chmod +x "$RUNNER_TEMP/clawhub"
|
||||
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Verify package-local runtime build
|
||||
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
|
||||
|
||||
- name: Preview publish command
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
@@ -223,6 +263,7 @@ jobs:
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 6
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
|
||||
steps:
|
||||
@@ -230,8 +271,18 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout target revision
|
||||
env:
|
||||
TARGET_SHA: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin \
|
||||
+refs/heads/main:refs/remotes/origin/main \
|
||||
'+refs/heads/release/*:refs/remotes/origin/release/*'
|
||||
git checkout --detach "${TARGET_SHA}"
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
@@ -246,9 +297,15 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ env.CLAWHUB_REPOSITORY }}
|
||||
ref: ${{ env.CLAWHUB_REF }}
|
||||
ref: main
|
||||
path: clawhub-source
|
||||
fetch-depth: 1
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout pinned ClawHub CLI revision
|
||||
working-directory: clawhub-source
|
||||
env:
|
||||
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
|
||||
run: git checkout --detach "${CLAWHUB_REF}"
|
||||
|
||||
- name: Install ClawHub CLI dependencies
|
||||
working-directory: clawhub-source
|
||||
@@ -304,7 +361,19 @@ jobs:
|
||||
encoded_name="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_NAME ?? ""))')"
|
||||
encoded_version="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_VERSION ?? ""))')"
|
||||
url="${CLAWHUB_REGISTRY%/}/api/v1/packages/${encoded_name}/versions/${encoded_version}"
|
||||
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' "${url}")"
|
||||
status=""
|
||||
for attempt in $(seq 1 8); do
|
||||
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' "${url}")"
|
||||
if [[ "${status}" == "404" || "${status}" =~ ^2 ]]; then
|
||||
break
|
||||
fi
|
||||
if [[ "${status}" == "429" || "${status}" =~ ^5 ]]; then
|
||||
echo "ClawHub availability check returned ${status} for ${PACKAGE_NAME}@${PACKAGE_VERSION}; retrying (${attempt}/8)."
|
||||
sleep 60
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
if [[ "${status}" =~ ^2 ]]; then
|
||||
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on ClawHub."
|
||||
exit 1
|
||||
|
||||
@@ -562,6 +562,7 @@ jobs:
|
||||
run_live_slack:
|
||||
name: Run Slack live QA lane with Convex leases
|
||||
needs: [authorize_actor, validate_selected_ref]
|
||||
if: vars.OPENCLAW_QA_SLACK_LIVE_CI_ENABLED == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
environment: qa-live-shared
|
||||
|
||||
200
.github/workflows/windows-blacksmith-testbox.yml
vendored
Normal file
200
.github/workflows/windows-blacksmith-testbox.yml
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
name: Windows Blacksmith Testbox
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
testbox_id:
|
||||
type: string
|
||||
description: "Testbox session ID"
|
||||
required: true
|
||||
runner_label:
|
||||
type: string
|
||||
description: "Windows runner label"
|
||||
required: false
|
||||
default: "blacksmith-16vcpu-windows-2025"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
name: windows
|
||||
runs-on: ${{ inputs.runner_label }}
|
||||
timeout-minutes: 75
|
||||
defaults:
|
||||
run:
|
||||
shell: pwsh
|
||||
steps:
|
||||
- name: Begin Testbox
|
||||
shell: bash
|
||||
env:
|
||||
TESTBOX_ID: ${{ inputs.testbox_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
metadata_port="${METADATA_PORT:-}"
|
||||
if [ -z "$metadata_port" ]; then
|
||||
metadata_port="$(cat /proc/cmdline | tr ' ' '\n' | grep '^metadata_port=' | cut -d= -f2)"
|
||||
fi
|
||||
if [ -z "$metadata_port" ]; then
|
||||
echo "metadata_port not found in kernel cmdline" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
metadata_addr="192.168.127.1:${metadata_port}"
|
||||
state=/tmp/.testbox
|
||||
mkdir -p "$state"
|
||||
chmod 700 "$state"
|
||||
|
||||
installation_model_id="$(curl -s --connect-timeout 2 --max-time 5 "http://${metadata_addr}/installationModelID")"
|
||||
api_url="$(curl -s --connect-timeout 2 --max-time 5 "http://${metadata_addr}/backendURL")"
|
||||
auth_token="$(curl -s --connect-timeout 2 --max-time 5 "http://${metadata_addr}/stickyDiskToken")"
|
||||
|
||||
if [ -z "$api_url" ] || [ -z "$installation_model_id" ] || [ -z "$auth_token" ]; then
|
||||
echo "could not read required Blacksmith metadata" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${BLACKSMITH_HOSTNAME:-}" ]; then
|
||||
runner_host="$BLACKSMITH_HOSTNAME"
|
||||
else
|
||||
runner_host="${BLACKSMITH_HOST_PUBLIC_IP:-}"
|
||||
fi
|
||||
runner_ssh_port="${BLACKSMITH_SSH_PORT:-22}"
|
||||
|
||||
response="$(curl -s -f -L --post302 --post303 -X POST "${api_url}/api/testbox/phone-home" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${auth_token}" \
|
||||
-d "{
|
||||
\"testbox_id\": \"${TESTBOX_ID}\",
|
||||
\"installation_model_id\": ${installation_model_id},
|
||||
\"status\": \"hydrating\",
|
||||
\"ip_address\": \"${runner_host}\",
|
||||
\"ssh_port\": \"${runner_ssh_port}\",
|
||||
\"working_directory\": \"${GITHUB_WORKSPACE}\",
|
||||
\"adopted_run_id\": \"${GITHUB_RUN_ID}\",
|
||||
\"metadata\": {}
|
||||
}" 2>/dev/null || true)"
|
||||
|
||||
echo "$TESTBOX_ID" > "$state/testbox_id"
|
||||
echo "$installation_model_id" > "$state/installation_model_id"
|
||||
echo "$auth_token" > "$state/auth_token"
|
||||
echo "$api_url" > "$state/api_url"
|
||||
echo "$runner_host" > "$state/runner_host"
|
||||
echo "$runner_ssh_port" > "$state/runner_ssh_port"
|
||||
echo "$GITHUB_WORKSPACE" > "$state/working_directory"
|
||||
echo "$GITHUB_RUN_ID" > "$state/adopted_run_id"
|
||||
|
||||
if [ -n "$response" ] && echo "$response" | jq -e . >/dev/null 2>&1; then
|
||||
echo "$response" | jq -r '.ssh_public_key // empty' > "$state/ssh_public_key"
|
||||
idle_timeout="$(echo "$response" | jq -r '.idle_timeout // empty')"
|
||||
echo "${idle_timeout:-10}" > "$state/idle_timeout"
|
||||
echo "phone-home response=json"
|
||||
else
|
||||
printf '%s\n' "$response" > "$state/ssh_public_key"
|
||||
echo "10" > "$state/idle_timeout"
|
||||
echo "phone-home response=raw"
|
||||
fi
|
||||
|
||||
ssh_public_key="$(cat "$state/ssh_public_key" 2>/dev/null || true)"
|
||||
if [ -n "$ssh_public_key" ]; then
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s\n' "$ssh_public_key" >> ~/.ssh/authorized_keys
|
||||
chmod 700 ~/.ssh
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Prepare Windows shell
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
Write-Host "runner=$env:RUNNER_NAME"
|
||||
Write-Host "machine=$env:COMPUTERNAME"
|
||||
Write-Host ("os=" + [System.Environment]::OSVersion.VersionString)
|
||||
Write-Host ("powershell=" + $PSVersionTable.PSVersion.ToString())
|
||||
git --version
|
||||
|
||||
- name: Run Testbox
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
state=/tmp/.testbox
|
||||
test -d "$state"
|
||||
|
||||
testbox_id="$(cat "$state/testbox_id")"
|
||||
installation_model_id="$(cat "$state/installation_model_id")"
|
||||
auth_token="$(cat "$state/auth_token")"
|
||||
idle_timeout="$(cat "$state/idle_timeout" 2>/dev/null || true)"
|
||||
idle_timeout="${idle_timeout:-10}"
|
||||
api_url="$(cat "$state/api_url")"
|
||||
runner_host="$(cat "$state/runner_host")"
|
||||
runner_ssh_port="$(cat "$state/runner_ssh_port")"
|
||||
working_directory="$(cat "$state/working_directory")"
|
||||
adopted_run_id="$(cat "$state/adopted_run_id")"
|
||||
|
||||
ready_body="$RUNNER_TEMP/testbox-ready.json"
|
||||
cat > "$ready_body" <<JSON
|
||||
{
|
||||
"testbox_id": "${testbox_id}",
|
||||
"installation_model_id": ${installation_model_id},
|
||||
"status": "ready",
|
||||
"ip_address": "${runner_host}",
|
||||
"ssh_port": "${runner_ssh_port}",
|
||||
"working_directory": "${working_directory}",
|
||||
"adopted_run_id": "${adopted_run_id}",
|
||||
"metadata": {}
|
||||
}
|
||||
JSON
|
||||
|
||||
http_code="$(curl -sS -L --post302 --post303 -o "$RUNNER_TEMP/testbox-ready.response" -w '%{http_code}' \
|
||||
-X POST "${api_url}/api/testbox/phone-home" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${auth_token}" \
|
||||
--data-binary @"$ready_body" || true)"
|
||||
echo "phone_home_ready_http=${http_code}"
|
||||
|
||||
echo "============================================"
|
||||
echo "Testbox ready!"
|
||||
echo " Testbox ID: ${testbox_id}"
|
||||
echo " Runner host: ${runner_host}"
|
||||
echo " SSH port: ${runner_ssh_port}"
|
||||
echo " Working directory: ${working_directory}"
|
||||
echo " Run ID: ${adopted_run_id}"
|
||||
echo " SSH: ssh -p ${runner_ssh_port} runner@${runner_host}"
|
||||
echo "============================================"
|
||||
|
||||
last_activity="$(date +%s)"
|
||||
idle_timeout_seconds=$(( idle_timeout * 60 ))
|
||||
|
||||
while true; do
|
||||
sleep 30
|
||||
now="$(date +%s)"
|
||||
|
||||
if netstat -na 2>/dev/null | grep ":${runner_ssh_port}" | grep -q ESTABLISHED; then
|
||||
last_activity="$now"
|
||||
elif [ -f ~/.testbox-last-activity ]; then
|
||||
file_mtime="$(stat -c %Y ~/.testbox-last-activity 2>/dev/null || stat -f %m ~/.testbox-last-activity)"
|
||||
if [ "$file_mtime" -gt "$last_activity" ]; then
|
||||
last_activity="$file_mtime"
|
||||
fi
|
||||
fi
|
||||
|
||||
idle_seconds=$(( now - last_activity ))
|
||||
if [ "$idle_seconds" -ge "$idle_timeout_seconds" ]; then
|
||||
echo "Idle timeout reached (${idle_timeout} minutes). Shutting down."
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Testbox action marker
|
||||
if: ${{ false }}
|
||||
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
|
||||
189
.github/workflows/windows-testbox-probe.yml
vendored
Normal file
189
.github/workflows/windows-testbox-probe.yml
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
name: Windows Testbox Probe
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_ref:
|
||||
description: "Git ref or SHA to check out"
|
||||
required: false
|
||||
default: "main"
|
||||
type: string
|
||||
runner_label:
|
||||
description: "Windows runner label"
|
||||
required: false
|
||||
default: "blacksmith-16vcpu-windows-2025"
|
||||
type: choice
|
||||
options:
|
||||
- blacksmith-16vcpu-windows-2025
|
||||
- blacksmith-32vcpu-windows-2025
|
||||
- windows-2025
|
||||
keepalive_minutes:
|
||||
description: "Minutes to keep the Windows runner alive for SSH inspection"
|
||||
required: false
|
||||
default: "20"
|
||||
type: string
|
||||
require_wsl2:
|
||||
description: "Fail the run when WSL2 is unavailable"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
import_ubuntu_wsl2:
|
||||
description: "Import a throwaway Ubuntu WSL2 distro when none is installed"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
enable_wsl2_features:
|
||||
description: "Try enabling Windows WSL2/VM optional features before probing"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
jobs:
|
||||
probe:
|
||||
name: Windows probe
|
||||
runs-on: ${{ inputs.runner_label }}
|
||||
timeout-minutes: 75
|
||||
defaults:
|
||||
run:
|
||||
shell: pwsh
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.target_ref || github.ref }}
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Probe native Windows
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
Write-Host "runner=$env:RUNNER_NAME"
|
||||
Write-Host "machine=$env:COMPUTERNAME"
|
||||
Write-Host "workspace=$env:GITHUB_WORKSPACE"
|
||||
Write-Host "target_ref=${{ inputs.target_ref || github.ref }}"
|
||||
Write-Host ("os=" + [System.Environment]::OSVersion.VersionString)
|
||||
Write-Host ("arch=" + [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)
|
||||
Write-Host ("powershell=" + $PSVersionTable.PSVersion.ToString())
|
||||
cmd.exe /c ver
|
||||
git --version
|
||||
|
||||
- name: Probe WSL2
|
||||
id: wsl2
|
||||
env:
|
||||
ENABLE_WSL2_FEATURES: ${{ inputs.enable_wsl2_features }}
|
||||
IMPORT_UBUNTU_WSL2: ${{ inputs.import_ubuntu_wsl2 }}
|
||||
UBUNTU_WSL_ROOTFS_URL: https://cloud-images.ubuntu.com/wsl/releases/24.04/current/ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz
|
||||
run: |
|
||||
$ErrorActionPreference = "Continue"
|
||||
$ok = $false
|
||||
|
||||
function Invoke-WslText {
|
||||
param([string[]] $Arguments)
|
||||
$output = & wsl.exe @Arguments 2>&1
|
||||
$code = $LASTEXITCODE
|
||||
$text = (($output | ForEach-Object { "$_" }) -join "`n") -replace "`0", ""
|
||||
[pscustomobject]@{ Code = $code; Text = $text }
|
||||
}
|
||||
|
||||
function Get-WslDistros {
|
||||
$result = Invoke-WslText -Arguments @("--list", "--quiet")
|
||||
$result.Text -split "\r?\n" |
|
||||
ForEach-Object { $_.Trim() } |
|
||||
Where-Object {
|
||||
$_ -and
|
||||
$_ -notmatch "Windows Subsystem for Linux has no installed distributions" -and
|
||||
$_ -notmatch "^Use 'wsl\.exe" -and
|
||||
$_ -notmatch "^and 'wsl\.exe"
|
||||
}
|
||||
}
|
||||
|
||||
$wsl = Get-Command wsl.exe -ErrorAction SilentlyContinue
|
||||
if (-not $wsl) {
|
||||
Write-Warning "wsl.exe is not available on this runner."
|
||||
} else {
|
||||
Write-Host "wsl.exe=$($wsl.Source)"
|
||||
if ($env:ENABLE_WSL2_FEATURES -eq "true") {
|
||||
Write-Host "enable_wsl2_features=true"
|
||||
foreach ($feature in @("Microsoft-Windows-Subsystem-Linux", "VirtualMachinePlatform", "HypervisorPlatform", "Microsoft-Hyper-V-All")) {
|
||||
dism.exe /online /enable-feature /featurename:$feature /all /norestart
|
||||
Write-Host "enable_feature_${feature}_exit=$LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
$status = Invoke-WslText -Arguments @("--status")
|
||||
Write-Host $status.Text
|
||||
Write-Host "wsl_status_exit=$($status.Code)"
|
||||
|
||||
$list = Invoke-WslText -Arguments @("--list", "--verbose")
|
||||
Write-Host $list.Text
|
||||
Write-Host "wsl_list_exit=$($list.Code)"
|
||||
|
||||
$distros = @(Get-WslDistros)
|
||||
if ($distros.Count -eq 0 -and $env:IMPORT_UBUNTU_WSL2 -eq "true") {
|
||||
Write-Host "import_ubuntu_wsl2=true"
|
||||
$wslRoot = "C:\wsl\UbuntuProbe"
|
||||
$rootfs = "C:\wsl\ubuntu-noble-wsl.rootfs.tar.gz"
|
||||
New-Item -ItemType Directory -Force -Path @((Split-Path -Parent $rootfs), $wslRoot) | Out-Null
|
||||
Invoke-WebRequest -Uri $env:UBUNTU_WSL_ROOTFS_URL -OutFile $rootfs -UseBasicParsing
|
||||
wsl.exe --import UbuntuProbe $wslRoot $rootfs --version 2
|
||||
Write-Host "wsl_import_exit=$LASTEXITCODE"
|
||||
$list = Invoke-WslText -Arguments @("--list", "--verbose")
|
||||
Write-Host $list.Text
|
||||
Write-Host "wsl_list_after_import_exit=$($list.Code)"
|
||||
$distros = @(Get-WslDistros)
|
||||
}
|
||||
|
||||
if ($distros.Count -gt 0) {
|
||||
$distro = $distros[0]
|
||||
Write-Host "wsl_probe_distro=$distro"
|
||||
wsl.exe -d $distro --exec bash -lc 'set -euo pipefail; uname -a; if [ -f /etc/os-release ]; then sed -n "1,8p" /etc/os-release; fi'
|
||||
} else {
|
||||
wsl.exe --exec bash -lc 'set -euo pipefail; uname -a; if [ -f /etc/os-release ]; then sed -n "1,8p" /etc/os-release; fi'
|
||||
}
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$ok = $true
|
||||
}
|
||||
Write-Host "wsl_exec_exit=$LASTEXITCODE"
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
"wsl2_ok=true" >> $env:GITHUB_OUTPUT
|
||||
"OPENCLAW_WSL2_PROBE_OK=true" >> $env:GITHUB_ENV
|
||||
Write-Host "wsl2_ok=true"
|
||||
} else {
|
||||
"wsl2_ok=false" >> $env:GITHUB_OUTPUT
|
||||
"OPENCLAW_WSL2_PROBE_OK=false" >> $env:GITHUB_ENV
|
||||
Write-Warning "wsl2_ok=false"
|
||||
}
|
||||
|
||||
exit 0
|
||||
|
||||
- name: Keep runner alive for SSH inspection
|
||||
env:
|
||||
KEEPALIVE_MINUTES: ${{ inputs.keepalive_minutes }}
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$minutes = 20
|
||||
if ($env:KEEPALIVE_MINUTES -match '^\d+$') {
|
||||
$minutes = [int]$env:KEEPALIVE_MINUTES
|
||||
}
|
||||
$minutes = [Math]::Max(0, [Math]::Min($minutes, 60))
|
||||
Write-Host "keepalive_minutes=$minutes"
|
||||
for ($i = 1; $i -le $minutes; $i++) {
|
||||
Write-Host "keepalive minute $i/$minutes"
|
||||
Start-Sleep -Seconds 60
|
||||
}
|
||||
|
||||
- name: Enforce WSL2 requirement
|
||||
if: ${{ inputs.require_wsl2 }}
|
||||
run: |
|
||||
if ($env:OPENCLAW_WSL2_PROBE_OK -ne "true") {
|
||||
Write-Error "WSL2 probe failed or WSL2 is unavailable on this Windows runner."
|
||||
exit 1
|
||||
}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -219,3 +219,4 @@ extensions/**/.openclaw-runtime-deps-stamp.json
|
||||
|
||||
# Output dir for scripts/run-opengrep.sh (local opengrep scans)
|
||||
/.opengrep-out/
|
||||
/.crabbox-artifacts
|
||||
|
||||
@@ -72,7 +72,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
- After landing PR: search duplicate open issues/PRs. Before closing: comment why + canonical link.
|
||||
- If an issue/PR is already fixed on current `main` or solved by a new release: comment with proof + canonical commit/PR/release, then close.
|
||||
- GH comments with markdown backticks, `$`, or shell snippets: avoid inline double-quoted `--body`; use single quotes or `--body-file`.
|
||||
- PR create: body required. Include concise Summary + Verification sections; mention issue/PR refs, behavior changed, and exact local/Testbox/CI proof. Never open an empty-body or placeholder-body PR.
|
||||
- PR create: description/body always required. Include concise Summary + Verification sections; mention issue/PR refs, behavior changed, and exact local/Testbox/CI proof. Never open an empty-description, empty-body, or placeholder-body PR.
|
||||
- PR execution artifacts/screenshots: attach them to the PR, comment, or an external artifact store. Do not add `.github/pr-assets` or other PR-only assets to the repo.
|
||||
- PR review answer must explicitly cover: what bug/behavior we are trying to fix; PR/issue URL(s) and affected endpoint/surface; whether this is the best possible fix, with high-certainty evidence from code, tests, CI, and shipped/current behavior.
|
||||
- When working on an issue or PR, always end the user-facing final answer with the full GitHub URL.
|
||||
|
||||
314
CHANGELOG.md
314
CHANGELOG.md
@@ -6,7 +6,257 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Highlights
|
||||
|
||||
- Google Meet/Voice Call: make Twilio dial-in joins speak through the realtime Gemini voice bridge with paced audio streaming, backpressure-aware buffering, barge-in queue clearing, and no TwiML fallback during realtime speech, giving Meet participants a much snappier OpenClaw voice agent. (#77064) Thanks @scoootscooob.
|
||||
|
||||
### Changes
|
||||
|
||||
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
|
||||
- Secrets/external channel contracts: also look in `<rootDir>/dist/` when resolving the `secret-contract-api` sidecar, so npm-published externalized channel plugins (e.g. `@openclaw/discord` since 2026.5.2) whose compiled artifacts live under `dist/` actually contribute their channel SecretRef contracts to the runtime snapshot. Without this, env-backed `channels.discord.token` SecretRefs silently failed to resolve at gateway start on 2026.5.3, leaving the channel `not configured` even though #76449 had landed the generic external-contract loader. Thanks @mogglemoss.
|
||||
- Models/auth: add `openclaw models auth list [--provider <id>] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc.
|
||||
- Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar.
|
||||
- Control UI/cron: make the New Job sidebar collapsible so the jobs list can reclaim space while keeping the form one click away. Thanks @BunsDev.
|
||||
- Gateway/startup: keep model-catalog test helpers, run-session lookup code, QR pairing helpers, and TypeBox memory-tool schema construction out of hot startup import paths, reducing default gateway benchmark plugin-load and memory pressure.
|
||||
- Control UI/performance: record browser long animation frame or long task entries in the debug event log when supported, making slow dashboard renders easier to attribute from the UI.
|
||||
- Slack/streaming: add `streaming.progress.render: "rich"` for Block Kit progress drafts backed by structured progress line data.
|
||||
- Slack/streaming: keep the newest rich progress lines when Block Kit limits trim long progress drafts. Thanks @vincentkoc.
|
||||
- Channels/streaming: cap progress-draft tool lines by default so edited progress boxes avoid jumpy reflow from long wrapped lines.
|
||||
- Agents/verbose: use compact explain-mode tool summaries for `/verbose` and progress drafts by default, with `agents.defaults.toolProgressDetail: "raw"` and per-agent overrides for debugging raw command/detail output.
|
||||
- Control UI/chat: add an agent-first filter to the chat session picker, keep chat controls/composer responsive across phone/tablet/desktop widths, keep desktop chat controls on one row, avoid duplicate avatar refreshes during initial chat load, and hide that row while scrolling down the transcript. Thanks @BunsDev.
|
||||
- Control UI/chat: collapse consecutive duplicate text messages into one bubble with a count so no-op heartbeat acknowledgements stay compact without hiding nearby context.
|
||||
- Agents/subagents: preserve every grouped child result when direct completion fallback has to bypass the requester-agent announce turn. Thanks @vincentkoc.
|
||||
- TTS/telephony: honor provider voice/model overrides in telephony synthesis providers so Google Meet agent speech logs match the backend that actually produced the audio. Thanks @vincentkoc.
|
||||
- Voice Call/realtime: bound the paced Twilio audio queue and close overloaded realtime streams before provider audio can pile up behind the websocket backpressure guard. Thanks @vincentkoc.
|
||||
- Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi.
|
||||
- Gateway/performance: defer non-readiness sidecars until after the ready signal, avoid hot-path channel plugin barrel imports, and fast-path trusted bundled plugin metadata during Gateway startup.
|
||||
- Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed.
|
||||
- Gateway/diagnostics: add startup phase spans, active work labels, stale terminal bridge markers, and default sync-I/O tracing in `pnpm gateway:watch` so slow Gateway turns are easier to attribute from logs and stability diagnostics.
|
||||
- Plugins/loader: preserve real compiled plugin module evaluation errors on the native fast path instead of treating every thrown `.js` module as a source-transform fallback miss. Thanks @vincentkoc.
|
||||
- QA/Mantis: add `pnpm openclaw qa mantis slack-desktop-smoke` to run Slack live QA inside a Crabbox VNC desktop, open Slack Web, and capture desktop screenshots beside the Slack QA artifacts.
|
||||
- QA/Mantis: pass the runtime env through desktop-browser Crabbox and artifact-copy child commands, so embedded Mantis callers can provide Crabbox credentials without mutating the parent process. Thanks @vincentkoc.
|
||||
- QA/Mantis: return the copied Slack desktop screenshot path even when remote Slack QA fails, so the CLI still prints the failure screenshot artifact. Thanks @vincentkoc.
|
||||
- QA/Mantis: accept Blacksmith Testbox `tbx_...` lease ids from desktop smoke warmup, so provider overrides do not fail before inspect/run. Thanks @vincentkoc.
|
||||
- Plugins/update: treat official externalized bundled npm migrations and ClawHub-to-npm fallbacks as trusted source-linked installs, so prerelease-only official plugin packages can migrate from bundled builds without being rejected as unsafe prerelease resolutions. Thanks @vincentkoc.
|
||||
- Plugins/update: move ClawHub-preferred externalized plugin installs back to ClawHub after an earlier npm fallback once the ClawHub package becomes available. Thanks @vincentkoc.
|
||||
- Plugins/update: clean stale bundled load paths for already-externalized pinned npm and ClawHub plugin installs, so release-channel sync does not leave removed bundled paths ahead of the installed external package. Thanks @vincentkoc.
|
||||
- Telegram: accept plugin-owned numeric forum-topic targets in the agent message tool and keep reply-dispatch provider chunks behind a real stable runtime alias during in-place package updates. Fixes #77137. Thanks @richardmqq.
|
||||
- Google Meet: preserve `realtime.introMessage: ""` so realtime Chrome joins can stay silent instead of restoring the default spoken intro. Thanks @vincentkoc.
|
||||
- Plugins/SDK: add bounded `before_agent_finalize` retry instructions so workflow plugins can request one more model pass. Thanks @100yenadmin.
|
||||
- Discord/status: add degraded Discord transport and gateway event-loop starvation signals to `openclaw channels status`, `openclaw status --deep`, and fetch-timeout logs so intermittent socket resets do not look like a healthy running channel. (#76327) Thanks @joshavant.
|
||||
- Providers/OpenRouter: add opt-in response caching params that send OpenRouter's `X-OpenRouter-Cache`, `X-OpenRouter-Cache-TTL`, and cache-clear headers only on verified OpenRouter routes. Thanks @vincentkoc.
|
||||
- Providers/OpenRouter: expand app-attribution categories so OpenClaw advertises coding, programming, writing, chat, and personal-agent usage on verified OpenRouter routes. Thanks @vincentkoc.
|
||||
- Plugins/update: make package upgrades swap pnpm/npm-prefix installs cleanly, keep legacy plugin install runtime chunks working, and on the beta channel fall back default-line npm plugins to default/latest when plugin beta releases are missing or fail install validation. Thanks @vincentkoc and @joshavant.
|
||||
- Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow outbound target idea from #13424. Thanks @vincentkoc and @agentz-manfred.
|
||||
- Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi.
|
||||
- Agents/sandbox: store sandbox container and browser registry entries as per-runtime shard files, reducing unrelated session lock contention while `openclaw doctor --fix` migrates legacy monolithic registry files. (#74831) Thanks @luckylhb90.
|
||||
- Plugins/ClawHub: annotate 429 errors from ClawHub with the reset window from `RateLimit-Reset`/`Retry-After` and append a `Sign in for higher rate limits.` hint when the request was unauthenticated, so users can see when downloads will recover and how to lift the cap. Thanks @romneyda.
|
||||
- Plugins/runtime state: add `registerIfAbsent` for atomic keyed-store dedupe claims that return whether a plugin successfully claimed a key without overwriting an existing live value. Thanks @amknight.
|
||||
- Plugin SDK: add plugin-owned `SessionEntry` slot projection and scoped trusted-policy session extension reads. (#75609; replaces part of #73384/#74483) Thanks @100yenadmin.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/OpenAI: default direct OpenAI Responses models to the SSE transport instead of WebSocket auto-selection, preventing pi runtime chat turns from hanging on servers where the WebSocket path stalls while the OpenAI HTTP stream works. Thanks @vincentkoc.
|
||||
- CLI/update: disable and skip plugins that fail package-update plugin sync, so a broken npm/ClawHub/git/marketplace plugin cannot turn a successful OpenClaw package update into a failed update result. Thanks @vincentkoc.
|
||||
- CLI/update: use an absolute POSIX npm script shell during package-manager updates, so restricted PATH environments can still run dependency lifecycle scripts while updating from `--tag main`. Fixes #77530. Thanks @PeterTremonti.
|
||||
- Diagnostics: grant the internal diagnostics event bus to official installed diagnostics exporter plugins, so npm-installed `@openclaw/diagnostics-prometheus` can emit metrics without broadening the capability to arbitrary global plugins. Fixes #76628. Thanks @RayWoo.
|
||||
- Browser: enforce strict SSRF current-URL checks before existing-session screenshots, matching existing-session snapshot handling. Thanks @vincentkoc.
|
||||
- Active Memory: give timeout partial transcript recovery enough abort-settle headroom so temporary recall summaries are returned before cleanup. Thanks @vincentkoc.
|
||||
- Gateway/chat: clear the active reply-run guard before draining queued same-session follow-up turns, so sequential `chat.send` calls no longer trip `ReplyRunAlreadyActiveError` every other request. Fixes #77485. Thanks @bws14email.
|
||||
- Agents/media: avoid sending generated image, video, and music attachments twice when streamed reply text arrives before the final `MEDIA:` directive.
|
||||
- CLI/sessions: cap `openclaw sessions` output to the newest 100 rows by default and add `--limit <n|all>` plus JSON pagination metadata, so repeated machine polling of large session stores cannot fan out into unbounded per-row enrichment/output work. Fixes #77500. Thanks @Kaotic3.
|
||||
- Doctor/config: restore legacy group chat config migrations for `routing.allowFrom`, `routing.groupChat.*`, and `channels.telegram.requireMention` so upgrades keep WhatsApp, Telegram, and iMessage group mention gates and history settings instead of leaving configs invalid or silently blocked. Thanks @scoootscooob.
|
||||
- CLI/update: make package-update follow-up processes write completion results and exit explicitly, so Windows packaged upgrades do not hang after the new package finishes post-core plugin work. Thanks @vincentkoc.
|
||||
- Release validation: skip Slack live QA unless Slack credentials are explicitly configured, so release gates can keep proving non-Slack surfaces while Slack is still local and credential-gated. Thanks @vincentkoc.
|
||||
- Plugins/update: treat OpenClaw CalVer correction versions like `2026.5.3-1` as satisfying base plugin API ranges, so correction builds can install plugins that require the base runtime API. Fixes #77293. (#77450) Thanks @p3nchan.
|
||||
- fix(gateway): clamp unbound websocket auth scopes [AI]. (#77413) Thanks @pgondhi987.
|
||||
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
|
||||
- Active Memory: send a bounded latest-message search query to the recall worker so channel/runtime metadata does not become the memory search string. Fixes #65309. Thanks @joeykrug, @westley3601, @pimenov, and @tasi333.
|
||||
- fix(device-pair): require pairing scope for pair command [AI]. (#76377) Thanks @pgondhi987.
|
||||
- Providers/OpenRouter: keep DeepSeek V4 `reasoning_effort` on OpenRouter-supported values, mapping stale `max` thinking overrides to `xhigh` so `openrouter/deepseek/deepseek-v4-pro` no longer fails with OpenRouter's invalid-effort 400. Fixes #77350. (#77423) Thanks @krllagent, @mushuiyu886, and @sallyom.
|
||||
- fix(qqbot): keep private commands off framework surface [AI]. (#77212) Thanks @pgondhi987.
|
||||
- Claude CLI: honor non-off `/think` levels by passing Claude Code's session-scoped `--effort` flag through the CLI backend seam, so chat bridges no longer show an inert thinking control. Fixes #77303. Thanks @Petr1t.
|
||||
- Agents/subagents: refresh deferred final-delivery payloads when same-session completion output changes, so retried parent notifications use the final child summary instead of stale progress text. Thanks @vincentkoc.
|
||||
- active-memory: skip the memory sub-agent gracefully instead of logging a confusing allowlist error when no memory plugin (`memory-core` or `memory-lancedb`) is loaded, so active-memory with no memory backend no longer produces misleading "No callable tools remain" warnings in the gateway log. Fixes #77506. Thanks @hclsys.
|
||||
- Memory/wiki: preserve representation from both corpora in `corpus=all` searches while backfilling unused result capacity, so memory hits are not starved by numerically higher wiki integer scores. Fixes #77337. Thanks @hclsys.
|
||||
- Telegram: clean up tool-only draft previews after assistant message boundaries so transient `Surfacing...` tool-status bubbles do not linger when no matching final preview arrives. Thanks @BunsDev.
|
||||
- Slack: report `unknown error` instead of `undefined` in socket-mode startup retry logs and label the retry reason explicitly.
|
||||
- Telegram: let explicit forum-topic `requireMention` settings override persisted `/activate` and `/deactivate` state, so per-topic mention gates work consistently. Fixes #49864. Thanks @Panniantong.
|
||||
- Cron: surface failed isolated-run diagnostics in `cron show`, status, and run history when requested tools are unavailable, so blocked cron runs report the actual tool-policy failure instead of a misleading green result. Fixes #75763. Thanks @RyanSandoval.
|
||||
- TUI/escape abort: track the in-flight runId after `chat.send` resolves so pressing Esc during the gap before the first gateway event aborts the run instead of repeatedly printing `no active run`. Fixes #1296. Thanks @Lukavyi and @romneyda.
|
||||
- TUI/render: stop the long-token sanitizer from injecting literal spaces inside inline code spans, fenced code blocks, table borders, and bare hyphenated/dotted identifiers, so copied package names, entity IDs, and shell line-continuations stay byte-for-byte intact while narrow-terminal protection still chunks unidentifiable long prose tokens. Fixes #48432, #39505. Thanks @DocOellerson, @xeusoc, @CCcassiusdjs, @akramcodez, @brokemac79, @romneyda.
|
||||
- Plugin skills: publish plugin-declared skills through the generated plugin skills directory (`~/.openclaw/plugin-skills/`) while keeping direct prompt loading intact, so agent file-based discovery paths find plugin skill `SKILL.md` files and inactive plugin links are cleaned up. Fixes #77296. (#77328) Thanks @zhangguiping-xydt.
|
||||
- Gateway/status: label Linux managed gateway services as `systemd user`, making status output explicit about the user-service scope instead of implying a system-level unit. Thanks @vincentkoc.
|
||||
- Plugins/install: remove the previous managed plugin directory when a reinstall switches sources, so stale ClawHub and npm copies no longer keep duplicate plugin ids in discovery after the new install wins. Thanks @vincentkoc.
|
||||
- Plugins/install: let official plugin reinstall recovery repair source-only installed runtime shadows, so `openclaw plugins install npm:@openclaw/discord --force` can replace the bad package instead of stopping at stale config validation. Thanks @vincentkoc.
|
||||
- CLI/update: stage pnpm-detected npm-layout global package updates through a clean npm prefix swap, keep plugin install runtime imports behind a stable alias, and ship legacy install-runtime aliases back to `2026.3.22`, preventing stale overlay chunks from breaking plugin post-update sync. Thanks @vincentkoc.
|
||||
- Plugins/commands: allow the official ClawHub Codex plugin package to keep reserved `/codex` command ownership, matching the existing npm-managed Codex package behavior. Thanks @vincentkoc.
|
||||
- Auth/OpenAI Codex: rewrite invalidated per-agent Codex auth-order and session profile overrides toward a healthy relogin profile, so revoked OAuth accounts do not stay pinned after signing in again. Thanks @BunsDev.
|
||||
- Plugins/commands: scope QQBot framework slash commands to the QQBot channel so `/bot-*` command handlers and native specs do not leak onto unrelated chat surfaces. Thanks @vincentkoc.
|
||||
- fix: harden backend message action gateway routing [AI]. (#76374) Thanks @pgondhi987.
|
||||
- Gate QQBot streaming command auth [AI]. (#76375) Thanks @pgondhi987.
|
||||
- Plugins/discovery: ignore managed npm plugin packages that only expose TypeScript source entries without compiled runtime output, so stale/broken installs cannot hide a working bundled or reinstallable channel plugin during setup. Thanks @vincentkoc.
|
||||
- CLI/update: treat OpenClaw stable correction versions like `2026.5.3-1` as newer than their base stable release, so package updates no longer ask for downgrade confirmation. Thanks @vincentkoc.
|
||||
- Plugins/install: suppress dangerous-pattern scanner warnings for trusted official OpenClaw npm installs, so installing `@openclaw/discord` no longer prints credential-harvesting warnings for the official package. Thanks @vincentkoc.
|
||||
- Plugins/commands: suppress dangerous-pattern scanner warnings for trusted catalog npm installs from owner-gated `/plugins install` commands, so chat-driven installs match the CLI install trust path. Thanks @vincentkoc.
|
||||
- Plugins/release: make the published npm runtime verifier reject blank `openclaw.runtimeExtensions` entries instead of treating them as absent and passing via inferred outputs. Thanks @vincentkoc.
|
||||
- Plugins/security: ignore inline and block comments when matching source-rule context in plugin install scans, so comment-only `fetch`/`post` references near environment defaults do not block clean plugins. Thanks @vincentkoc.
|
||||
- Doctor/plugins: remove stale managed install records for bundled plugins even when the bundled plugin is not explicitly configured, so doctor cleanup cannot leave orphaned install metadata behind. Thanks @vincentkoc.
|
||||
- Web fetch: scope provider fallback cache entries by the selected fetch provider so config reloads cannot reuse another provider's cached fallback payload. Thanks @vincentkoc.
|
||||
- Web search: honor late-bound `tools.web.search.enabled: false` during tool execution so config reloads cannot leave an already-created `web_search` tool runnable. Thanks @vincentkoc.
|
||||
- Plugins/packages: reject inferred built runtime entries that exist but fail package-boundary checks instead of falling back to TypeScript source for installed packages. Thanks @vincentkoc.
|
||||
- Plugins/loader: do not retry native-loaded JavaScript plugin modules through the source transformer after native evaluation has already reached a missing dependency, avoiding duplicate top-level side effects. Thanks @vincentkoc.
|
||||
- Plugins/packages: reject blank `openclaw.runtimeExtensions` entries instead of silently ignoring them and falling back to inferred TypeScript runtime entries. Thanks @vincentkoc.
|
||||
- Doctor/plugins: remove stale managed npm plugin shadow entries from the managed package lock as well as `package.json` and `node_modules`, so future npm operations do not keep referencing repaired bundled-plugin shadows. Thanks @vincentkoc.
|
||||
- Plugins/runtime state: keep the key being registered when namespace eviction runs in the same millisecond as existing entries, so `register` and `registerIfAbsent` do not report success while evicting their own fresh value. Thanks @vincentkoc.
|
||||
- Control UI/Talk: make failed Talk startup errors dismissable and clear the stale Talk error state when dismissed, so missing realtime voice provider configuration does not leave a permanent chat banner. Fixes #77071. Thanks @ijoshdavis.
|
||||
- Control UI/Talk: stop and clear failed realtime Talk sessions when dismissing runtime error banners, so the next Talk click starts a fresh session instead of only stopping the stale one. Thanks @vincentkoc.
|
||||
- Control UI/Talk: retry from a failed realtime Talk session on the next Talk click instead of requiring a separate stale-session stop click first. Thanks @vincentkoc.
|
||||
- Canvas host: preserve the Gateway TLS scheme in browser canvas host URLs and startup mount logs, so direct HTTPS gateways do not advertise insecure canvas links. Thanks @vincentkoc.
|
||||
- WhatsApp/login: route login success and failure messages through the injected runtime, so setup/onboarding surfaces capture all login output instead of only the QR. Thanks @vincentkoc.
|
||||
- Google Chat: create an isolated Google auth transport per auth client, so google-auth-library interceptor mutations do not accumulate across webhook verification and access-token clients. Thanks @vincentkoc.
|
||||
- Doctor/plugins: remove orphaned or recovered managed npm copies of bundled `@openclaw/*` plugins during `doctor --fix`, so stale package manifests cannot shadow the current bundled plugin config schema.
|
||||
- Control UI/performance: cap long-task and long-animation-frame diagnostics in the shared event log, so slow-render telemetry does not evict gateway/plugin events from the Debug and Overview views. Thanks @vincentkoc.
|
||||
- Gateway/startup: log the canvas host mount only after the HTTP server has bound, so startup logs no longer report the canvas host as mounted before it can serve requests.
|
||||
- Control UI/i18n: render the Sessions active filter tooltip with the configured minute count in every locale and make the i18n check reject placeholder drift. Thanks @BunsDev.
|
||||
- Web fetch: late-bind `web_fetch` config and provider fallback metadata from the active runtime snapshot, matching `web_search` so long-lived tools do not use stale fetch provider settings. Thanks @vincentkoc.
|
||||
- Discord: clear stale startup probe bot/application status when the async bot probe throws, not just when it returns a degraded probe result. Thanks @vincentkoc.
|
||||
- Web search: scope explicit bundled `web_search` provider runtime loading through manifest ownership, so selecting DuckDuckGo/Gemini/etc. does not import unrelated bundled providers or log their optional dependency failures. Thanks @vincentkoc.
|
||||
- Plugins/discovery: demote the source-only TypeScript runtime check on already-installed `origin: "global"` plugin packages from a config-blocking error to a warning and let the runtime fall through to the TypeScript source via jiti, so a single broken installed package no longer blocks `plugins install` for unrelated plugins; install-time rejection of newly-installed source-only packages is unchanged. Thanks @romneyda.
|
||||
- Providers/OpenAI Codex: stop the OAuth progress spinner before showing the manual redirect paste prompt, so callback timeouts do not spam `Browser callback did not finish` across terminals.
|
||||
- Providers/DeepSeek: expose DeepSeek V4 `xhigh` and `max` thinking levels through the lightweight provider-policy surface, so Control UI `/think` pickers keep showing the max reasoning options when the runtime plugin registry is not active. Fixes #77139. Thanks @bittoby.
|
||||
- Release/beta smoke: resolve the dispatched Telegram beta E2E run from `gh run list` when `gh workflow run` returns no run URL, so the maintainer helper does not fail immediately after dispatch. Thanks @vincentkoc.
|
||||
- Media/images: keep HEIC/HEIF attachments fail-closed when optional Sharp conversion is unavailable instead of sending originals that still need conversion. Thanks @vincentkoc.
|
||||
- Google Meet: fork the caller's current agent transcript into agent-mode meeting consultant sessions, so Meet replies inherit the context from the tool call that joined the meeting.
|
||||
- iOS/mobile pairing: reject non-loopback `ws://` setup URLs before QR/setup-code issuance and let the iOS Gateway settings screen scan QR codes or paste full setup-code messages. Thanks @BunsDev.
|
||||
- Control UI: keep Gateway Access inputs and locale picker contained inside the card at narrow and tablet widths.
|
||||
- Agents/trajectory: bound runtime trajectory capture and yield queued sidecar writes so oversized traces stop recording instead of monopolizing Gateway cleanup. Fixes #77124. Thanks @loyur.
|
||||
- Telegram/streaming: sanitize tool-progress draft preview backticks before shared compaction, so long backtick-heavy progress text still renders inside the safe code-formatted preview instead of collapsing to an ellipsis.
|
||||
- UI/chat: remove the unsupported `line-clamp` declaration from the chat queue text rule to eliminate Firefox console noise without changing visible truncation behavior. Thanks @ZanderH-code.
|
||||
- Control UI: add explicit feedback for repeated actions by announcing session switches, flashing the active session selector, showing inline Save/Apply/Update progress, and distinguishing filtered-empty session lists from genuinely empty session stores. Thanks @BunsDev.
|
||||
- Agents/Pi: suppress persistence for synthetic mid-turn overflow continuation prompts, so transcript-retry recovery does not write the "continue from transcript" prompt as a new user turn. Thanks @vincentkoc.
|
||||
- Agents/tools: strip reasoning text from visible rich presentation titles, blocks, buttons, and select labels before message-tool sends, so structured channel payloads cannot leak hidden planning. Thanks @vincentkoc.
|
||||
- Telegram: keep reply-dispatch lazy provider runtime chunks behind stable dist names and delete `/reasoning stream` previews after final delivery so package updates and live reasoning drafts do not leave Telegram turns broken or noisy. Thanks @BunsDev.
|
||||
- Discord: start the gateway monitor without waiting for the startup bot/application probe, so WSL2 hosts with a slow `/users/@me` REST path still bring the channel online while status enrichment finishes asynchronously. Fixes #77103. Thanks @Suited78.
|
||||
- Exec approvals: detect `env -S` split-string command-carrier risks when `-S`/`-s` is combined with other env short options, so approval explanations do not miss split payloads hidden behind `env -iS...`. Thanks @vincentkoc.
|
||||
- Google Meet: log the concrete agent-mode TTS provider, model, voice, output format, and sample rate after speech synthesis, so Meet logs show which voice backend spoke each reply.
|
||||
- Voice Call: mark realtime calls completed when the realtime provider closes normally, so Twilio/OpenAI/Google realtime stop events do not leave active call records behind. Thanks @vincentkoc.
|
||||
- Gateway/update: keep the shutdown close path behind a stable runtime chunk and ship compatibility aliases for recent `server-close-*` hashes, so manual npm package replacement cannot leave an already-running Gateway unable to shut down cleanly. Fixes #77087. Thanks @westlife219.
|
||||
- Control UI/media: mint short-lived scoped tickets for assistant media fetches and render ticketed URLs instead of exposing long-lived auth tokens in chat image URLs. Fixes #70830 and #77097. Thanks @hclsys.
|
||||
- Exec approvals: treat POSIX `exec` as a command carrier for inline eval, shell-wrapper, and eval/source detection, so approval explanations and command-risk checks do not miss payloads hidden behind `exec`. Thanks @vincentkoc.
|
||||
- Google Meet: log the resolved audio provider model when starting Chrome and paired-node Meet talk-back bridges, so agent-mode joins show the STT model and bidi joins show the realtime voice model.
|
||||
- Diagnostics: handle missing session-tail files in cron recovery context without tripping extension test typecheck. Thanks @vincentkoc.
|
||||
- QA/Slack: update the Slack dispatch preview fallback test SDK mock for structured progress draft helpers, so the rich progress draft regression suite covers the new imports instead of failing before assertions run. Thanks @vincentkoc.
|
||||
- Release validation: allow focused QA live reruns to select Matrix and Telegram without running Slack, so known Slack credential-pool outages do not block non-Slack live proof. Thanks @vincentkoc.
|
||||
- Plugins/loader: keep bundled plugin package `test-api.js` aliases behind private QA mode, so source transforms do not expose test-only public surfaces during normal plugin loading. Thanks @vincentkoc.
|
||||
- Gateway/startup: start cron and record the post-ready memory trace even when deferred maintenance timers fail after readiness, so a non-fatal timer setup issue does not silently leave scheduled jobs idle. Thanks @vincentkoc.
|
||||
- Exec approvals: unwrap BSD/macOS `env -P <path>` carrier commands before approval-command and strict inline-eval checks, so `/approve` shell execution and inline interpreter payloads are still blocked behind that env form.
|
||||
- Agents/session status: keep semantic `session_status({ sessionKey: "current" })` on the live run session even before that run has a persisted session-store entry, instead of falling back to the sandbox policy key. Thanks @vincentkoc.
|
||||
- QA/Slack: resolve bundled official plugin public-surface package aliases during source-mode QA runs, so release Slack live validation can load `@openclaw/slack/api.js` without workspace symlinks. Thanks @vincentkoc.
|
||||
- Codex: pass the live run session key into app-server dynamic tools when sandbox policy uses a separate session key, so `session_status({ sessionKey: "current" })` reports the active run instead of the sandbox policy key. Thanks @vincentkoc.
|
||||
- Web search: keep first-class assistant `web_search` auto-detect and configured runtime providers visible when active runtime metadata or the active plugin registry is incomplete. Fixes #77073. Thanks @joeykrug.
|
||||
- Plugins/tools: mark manifest-optional sibling tools as optional even when they come from a shared non-optional factory, so cached/status/MCP metadata keeps opt-in tool policy accurate. Thanks @vincentkoc.
|
||||
- Matrix: keep `streaming.progress.toolProgress` scoped to progress draft mode, so partial and quiet Matrix previews do not lose tool progress unless `streaming.preview.toolProgress` is disabled. Thanks @vincentkoc.
|
||||
- Gateway/validation: isolate gateway server validation files, ignore unrelated startup logs in request-trace coverage, and fail fast on stuck shared-auth sockets, reducing false main-branch CI failures for contributors. Thanks @amknight.
|
||||
- Channels/streaming: keep `streaming.progress.toolProgress` scoped to progress draft mode, so disabling compact progress lines does not silence partial/block preview tool updates. Thanks @vincentkoc.
|
||||
- Plugins/update: treat OpenClaw stable correction versions like `2026.5.3-1` as stable releases for npm installs, plugin updates, and bundled-version comparisons, so `latest` can advance official plugins without prerelease opt-in. Thanks @vincentkoc.
|
||||
- Control UI: point the Appearance tweakcn browse action and docs at the live tweakcn editor route instead of the removed `/themes` page. Fixes #77048.
|
||||
- Control UI: render Dream Diary prose through the sanitized markdown pipeline, so diary bold/italic/header markdown no longer appears as literal source text. Fixes #62413.
|
||||
- Control UI: render tool results whose output arrives as text-block arrays and give expanded tool output a scrollable block, so read/exec output remains visible in WebChat. Fixes #77054.
|
||||
- MCP: include serialized conversation/message payloads in the primary text content for `conversations_list` and `messages_read`, while preserving `structuredContent` for capable clients. Fixes #77024.
|
||||
- Media: treat `EPERM` from the post-write media fsync step as best-effort, allowing WebChat and channel uploads to finish on Windows filesystems that reject `fsync` after a successful write. Fixes #76844.
|
||||
- Media/Telegram: send in-limit original images when optional image optimization is unavailable, so Telegram MEDIA replies and message-tool image sends do not fail just because `sharp` is missing. Fixes #77081. (#77117) Thanks @pfrederiksen.
|
||||
- Diagnostics: include last progress, cron job/run ids, stopped cron job name, and the last assistant transcript snippet in stalled-session and stuck-session recovery logs so cron stalls show what was stopped.
|
||||
- Streaming channels: add `streaming.preview.commandText: "status"` / `streaming.progress.commandText: "status"` to hide command/exec text in preview progress lines while keeping the released raw command text default. Fixes #77072.
|
||||
- Agents/cron: let explicit cron `timeoutSeconds` drive both CLI no-output and embedded LLM idle watchdogs instead of being capped by resume defaults. Fixes #76289.
|
||||
- Plugins/catalog: suppress missing `channelConfigs` compatibility diagnostics for external channel plugins that are disabled, denied, or outside a restrictive allowlist. Fixes #76095.
|
||||
- Diagnostics: keep webhook/message OTEL attributes and Prometheus delivery labels low-cardinality and omit raw chat/message IDs from spans, so progress-draft and message-tool modes do not leak high-cardinality messaging identifiers.
|
||||
- Google Meet: stop advertising legacy `mode: "realtime"` to agents and config UIs, while keeping it as a hidden compatibility alias for `mode: "agent"`, so new joins use the STT -> OpenClaw agent -> TTS path instead of selecting the direct realtime voice fallback.
|
||||
- Google Meet: add `chrome.audioBufferBytes` for generated command-pair SoX audio commands and lower the default buffer from SoX's 8192 bytes to 4096 bytes to reduce Chrome talk-back latency.
|
||||
- Google Meet: split realtime provider config into agent-mode transcription and bidi-mode voice providers, and migrate legacy Gemini Live bidi configs with `doctor --fix`, so Gemini Live can back direct bidi fallback without breaking the default OpenClaw agent talk-back path.
|
||||
- Google Meet: keep waiting for the Meet microphone to unmute during join intro readiness instead of permanently skipping talk-back when Meet briefly reports the local mic as muted.
|
||||
- Google Meet: expose `voiceCall.postDtmfSpeechDelayMs` in the plugin manifest schema and setup hints, so manifest-based config editing accepts the runtime-supported Twilio delay key. Thanks @vincentkoc.
|
||||
- Google Meet: keep explicit non-Google `realtime.provider` values as the transcription provider compatibility fallback when `realtime.transcriptionProvider` is unset. Thanks @vincentkoc.
|
||||
- Google Meet: make Twilio setup status require an enabled `voice-call` plugin entry instead of treating a missing entry as ready. Thanks @vincentkoc.
|
||||
- Telegram: render shared interactive reply buttons in reply delivery so plugin approval messages show inline keyboards. (#76238) Thanks @keshavbotagent.
|
||||
- Cron/sessions: keep cron metadata rows without an on-disk transcript non-resumable until a transcript exists, so doctor and `sessions cleanup --fix-missing` no longer report or prune pre-transcript cron rows as broken sessions. Refs #77011.
|
||||
- Agents/cli-runner: drop a saved `claude-cli` resume sessionId at preparation time when its on-disk transcript no longer exists in `~/.claude/projects/`, so a stale binding from a half-installed `update.run` cannot trap follow-up runs (auto-reply / Telegram direct) in a `claude --resume` timeout loop; the run starts fresh and the new sessionId is written back through the existing post-run flow. (#77030; refs #77011) Thanks @openperf.
|
||||
- Release validation: install the cross-OS TypeScript harness through Windows-safe Node/npm shims so native Windows package checks reach the OpenClaw smoke suites instead of exiting before artifact capture. Thanks @vincentkoc.
|
||||
- Release validation: let Windows packaged-upgrade checks continue after the shipped 2026.5.2 updater hits its native-module swap cleanup fallback, verifying the fallback-installed candidate through package metadata and downstream smoke instead of crashing on the immediate update-status probe. Thanks @vincentkoc.
|
||||
- Doctor/plugins: skip channel-derived official plugin installs when another configured plugin is the effective owner for the same channel, so `doctor --repair` does not reinstall `feishu` while `openclaw-lark` handles `channels.feishu`. Fixes #76623. Thanks @fuyizheng3120.
|
||||
- Gateway/sessions: memoize repeated thinking-option enrichment and skip unused cost fallback checks while listing sessions, reducing per-row work on large multi-agent stores. Fixes #76931.
|
||||
- Gateway/sessions: bound default `sessions.list` RPC responses and report truncation metadata, preventing Slack-heavy long-lived stores from forcing unbounded Gateway row construction. Fixes #77062.
|
||||
- Agents/tools: use config-only runtime snapshots for plugin tool registration and live runtime config getters, avoiding expensive full secrets snapshot clones on the core-plugin-tools prep path. Fixes #76295.
|
||||
- Agents/tools: honor the effective tool denylist before constructing optional PDF/media tool factories, so `tools.deny: ["pdf"]` skips PDF setup before later policy filtering. Fixes #76997.
|
||||
- MCP/plugin tools: apply global `tools.profile`, `tools.alsoAllow`, and `tools.deny` policy while exposing plugin tools over the standalone MCP bridge, so ACP clients do not see policy-hidden plugin tools or miss opt-in optional tools. Thanks @vincentkoc.
|
||||
- Plugin tools: honor explicit tool denylists while selecting plugin tool runtimes, so denied plugin tools are not materialized for direct command or gateway surfaces before later policy filtering. Thanks @vincentkoc.
|
||||
- Plugin tools: filter factory-returned tools by manifest per-tool optional policy, so optional sibling tools from a shared runtime factory stay hidden unless explicitly allowed. Thanks @vincentkoc.
|
||||
- Agents/transcripts: retry context-overflow compaction from the current transcript only after the inbound user turn was actually persisted, and keep WebChat agent-run live delivery from writing duplicate Pi-managed assistant turns. Fixes #76424. (#77033)
|
||||
- Agents/bootstrap: keep pending `BOOTSTRAP.md` and bootstrap truncation notices in system-prompt Project Context instead of copying setup text or raw warning diagnostics into WebChat user/runtime context. Fixes #76946.
|
||||
- Gateway/install: keep `.env`-managed values in the macOS LaunchAgent env file while still tracking `OPENCLAW_SERVICE_MANAGED_ENV_KEYS`, so regenerated services do not boot without managed auth/provider keys. Fixes #75374.
|
||||
- Gateway/restart: verify listener PIDs by argv when `lsof` reports only the Node process name, so stale gateway cleanup can find macOS `cnode` listeners. Fixes #70664.
|
||||
- Gateway/logging: expand leading `~` in `logging.file` before creating the file logger, preventing startup crash loops for home-relative log paths. Fixes #73587.
|
||||
- Channels/CLI: keep `openclaw channels list --json` usable when provider usage fetching fails, and report per-provider usage errors without aborting the channel list. Refs #67595.
|
||||
- Doctor/plugins: do not treat `plugins.allow` entries as configured plugins during missing-plugin repair, so restrictive allowlists no longer install allowed-but-unused plugins. Thanks @vincentkoc.
|
||||
- Agents/messaging: deliver distinct final commentary after same-target `message` tool sends while still deduping text/media already sent by the tool, so short closing remarks are no longer silently dropped. Fixes #76915. Thanks @hclsys.
|
||||
- Agents/messaging: preserve string thread IDs when matching message-tool reply dedupe routes, avoiding precision loss on numeric-looking topic IDs before channel plugin comparison. Thanks @vincentkoc.
|
||||
- Channels/streaming: honor `agents.defaults.toolProgressDetail: "raw"` in Slack, Discord, Telegram, Matrix, and Microsoft Teams progress drafts, so tool-start lines include raw command/detail output when debugging. Thanks @vincentkoc.
|
||||
- Channels/streaming: strip unmatched inline-code backticks from compacted raw progress draft lines, avoiding stray markdown markers after long command details are shortened. Thanks @vincentkoc.
|
||||
- Discord/Slack/Mattermost: align draft preview tool-progress config help with the runtime behavior that hides interim tool updates when `streaming.preview.toolProgress` is false. Thanks @vincentkoc.
|
||||
- Feishu: use the shared channel progress formatter for streaming-card tool status lines, including raw command/detail output and message-tool filtering. Thanks @vincentkoc.
|
||||
- Mattermost: use the shared progress draft formatter for tool status previews, including raw command/detail output when `agents.defaults.toolProgressDetail: "raw"` is enabled. Thanks @vincentkoc.
|
||||
- Mattermost: suppress standalone default tool-progress messages while draft previews are active, including when draft tool lines are disabled. Thanks @vincentkoc.
|
||||
- Telegram: deliver button-only interactive replies by sending the shared fallback button-label text with the inline keyboard instead of dropping the reply as empty. Thanks @vincentkoc.
|
||||
- OpenAI Codex: honor `auth.order.openai-codex` when starting app-server clients without an explicit auth profile, so status/model probes and implicit startup use the configured Codex account instead of falling back to the default profile. Thanks @vincentkoc.
|
||||
- OpenAI Codex: let SSRF-guarded provider requests inherit OpenClaw's undici IPv4/IPv6 fallback policy, so ChatGPT-backed Codex runs recover on IPv4-working hosts when DNS still returns unreachable IPv6 addresses. Fixes #76857. Thanks @jplavoiemtl and @SymbolStar.
|
||||
- Plugin updates: do not short-circuit trusted official npm updates as unchanged when the default/latest spec still resolves to an already-installed prerelease that the installer should replace with a stable fallback. Thanks @vincentkoc.
|
||||
- Plugin updates: clean stale bundled load paths for already-externalized npm installs whose legacy install record only preserved the resolved package name. Thanks @vincentkoc.
|
||||
- Plugin tools: keep auth-unavailable optional tools hidden even when another default tool from the same plugin is available and `tools.alsoAllow` names the optional tool. Thanks @vincentkoc.
|
||||
- Realtime transcription: report socket closes before provider readiness as closed-before-ready failures instead of mislabeling them as connection timeouts for OpenAI, xAI, and Deepgram streaming transcription. Thanks @vincentkoc.
|
||||
- OpenAI/Google Meet: fail realtime voice connection attempts when the socket closes before `session.updated`, avoiding stuck Meet joins waiting on a bridge that never became ready. Thanks @vincentkoc.
|
||||
- Google Meet: avoid treating repeated participant words as multiple assistant-overlap matches when suppressing realtime echo transcripts. Thanks @vincentkoc.
|
||||
- Google Meet: make `mode: "agent"` the default Chrome talk-back path, using realtime transcription for input and regular OpenClaw TTS for speech output, while keeping direct realtime voice answers available as `mode: "bidi"` and accepting `mode: "realtime"` as an agent-mode compatibility alias.
|
||||
- Slack/Discord: suppress standalone tool-progress chatter when partial preview streaming has `streaming.preview.toolProgress: false`, matching the documented quiet-preview behavior. Thanks @vincentkoc.
|
||||
- Matrix: bind native approval reaction targets before publishing option reactions, so fast approver reactions on threaded prompts are not dropped while the approval handler finishes setup. Thanks @vincentkoc.
|
||||
- Google Meet: make realtime talk-back agent-driven by default with `realtime.strategy: "agent"`, keep the previous direct bidirectional model behavior available as `realtime.strategy: "bidi"`, route the Meet tab speaker output to `BlackHole 2ch` automatically for local Chrome realtime joins, coalesce nearby speech transcript fragments before consulting the agent, and avoid cutting off agent speech from server VAD or stale playback pipe errors.
|
||||
- Google Meet: suppress queued assistant playback and assistant-like transcript echoes from the realtime input path, so the meeting does not hear the agent's own speech as a new user turn and loop or cut itself off.
|
||||
- Google Meet: keep Chrome realtime transport tests hermetic on Linux prerelease shards while preserving the macOS-only runtime guard. Thanks @vincentkoc.
|
||||
- QA/Matrix: let the live tool-progress preview and error checks verify progress replacement events without depending on the preview saying `Working`, `tool: read`, an unlabelled/pathless `read from`, or the original draft root being observed. Thanks @vincentkoc.
|
||||
- QA/Matrix: keep the target=both approval scenario focused on channel and DM metadata delivery by resolving the accepted approval through the gateway after both Matrix events are observed. Thanks @vincentkoc.
|
||||
- QA/Matrix: wait for live approval reactions to echo before starting the threaded approval decision timeout. Thanks @vincentkoc.
|
||||
- QA/Matrix: reuse the primed driver sync stream when confirming approval reaction echoes, avoiding missed self-reactions in live release runs. Thanks @vincentkoc.
|
||||
- Channels/WhatsApp: apply the shared group/channel visible-reply mode during inbound dispatch so group replies stay message-tool-only by default without overriding direct-chat harness defaults. Refs #75178 and #67394. Thanks @scoootscooob.
|
||||
- Plugins/Codex: preserve Codex-native OAuth routing for `/codex bind` app-server turns so bound sessions keep the selected Codex auth profile instead of falling back to public OpenAI credentials. (#76714) Thanks @keshavbotagent.
|
||||
- Telegram: keep status checks pointed at the active chat so asking for the current session no longer reports an old direct-message conversation. (#76708) Thanks @amknight.
|
||||
- Gateway/install: prefer supported system Node over nvm/fnm/volta/asdf/mise when regenerating managed gateway services, so `gateway install --force` no longer recreates service definitions that doctor immediately flags as version-manager-backed. Fixes #76339. Thanks @brokemac79 and @BunsDev.
|
||||
- Google Chat: normalize Google auth certificate response headers before google-auth-library reads cache-control, so inbound webhook auth no longer rejects with `res?.headers.get is not a function`. Fixes #76880. Thanks @donbowman.
|
||||
- WhatsApp: route terminal login QR output through the active runtime for initial and restart sockets, so `openclaw channels login --channel whatsapp` does not lose the QR behind direct stdout writes. Fixes #76213. Thanks @dougvk.
|
||||
- Proxy/debugging: disable debug proxy direct upstream forwarding for proxy requests and CONNECT tunnels while managed proxy mode is active unless `OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY=1` is explicitly set for approved local diagnostics. Thanks @jesse-merhi and @mjamiv.
|
||||
- Direct APNs: route direct HTTP/2 delivery through the active managed proxy with redacted proxy diagnostics, so push requests honor configured egress controls and `openclaw proxy validate --apns-reachable` can prove APNs is reachable through the proxy before deployment. (#74905) Thanks @jesse-merhi.
|
||||
- Agents/subagents: detect prefix-only completion announce replies and fall back to the captured child result so requester chats no longer lose most of long sub-agent reports silently. Fixes #76412. Thanks @inxaos and @davemorin.
|
||||
- TUI: replace the stale-response watchdog notice with plain user-facing copy so stalled replies no longer surface backend or streaming internals. (#77120) Thanks @davemorin.
|
||||
- Security/Windows: validate `SystemRoot`/`WINDIR` env values through the Windows install-root validator and add them to the dangerous-host-env policy when resolving `icacls.exe`/`whoami.exe` for `openclaw security audit`, so workspace `.env` overrides and bare command names cannot redirect Windows ACL helpers to attacker-controlled binaries. (#74458) Thanks @mmaps.
|
||||
- Security/Windows: pin Windows registry-probe `reg.exe` resolution to the canonical Windows install root in install-root probing, so `SystemRoot`/`WINDIR` env overrides cannot redirect registry queries during Windows host detection. (#74454) Thanks @mmaps.
|
||||
- QQBot: preserve the framework command authorization decision when converting framework command contexts into engine slash command contexts, so downstream slash handlers see `commandAuthorized` matching the channel's resolved `isAuthorizedSender` instead of a hardcoded `true`. (#77453) Thanks @drobison00.
|
||||
- Security/Windows: block `LOCALAPPDATA` from workspace `.env` and resolve Windows update-flow portable Git path prepends from the trusted process-local `LOCALAPPDATA` only, so workspace-supplied values cannot redirect `git` discovery during `openclaw update`. (#77470) Thanks @drobison00.
|
||||
- Browser/SSRF: enforce the existing current-tab URL navigation policy before tab-scoped debug, export, and read routes (console, page errors, network requests, trace start/stop, response body, screenshot, snapshot, storage, etc.) collect from an already-selected tab, so blocked tabs return a policy error instead of being read first and redacted only at response time. (#75731) Thanks @eleqtrizit.
|
||||
- Security/Windows: route the `.cmd`/`.bat` process wrapper through the shared Windows install-root resolver instead of `process.env.ComSpec`, so workspace dotenv-blocked `SystemRoot`/`WINDIR` overrides and unsafe values like UNC paths or path-lists cannot redirect `cmd.exe` selection on Windows. (#77472) Thanks @drobison00.
|
||||
- Agents/bootstrap: honor `BOOTSTRAP.md` content injected by `agent:bootstrap` hooks when deciding whether bootstrap is pending, so hook-provided required setup instructions are included in the system prompt. (#77501) Thanks @ificator.
|
||||
|
||||
## 2026.5.3-1
|
||||
|
||||
### Fixes
|
||||
|
||||
- Plugins/security: stop the install scanner from blocking official bundled plugin packages when `process.env` access and normal API sends only appear in distant parts of the same compiled bundle. Thanks @vincentkoc.
|
||||
|
||||
## 2026.5.3
|
||||
|
||||
### Highlights
|
||||
|
||||
- Plugins/file-transfer: add bundled file-transfer plugin with `file_fetch`, `dir_list`, `dir_fetch`, and `file_write` agent tools for binary file ops on paired nodes; default-deny per-node path policy under `plugins.entries.file-transfer.config.nodes` with operator approval, symlink traversal refused by default (opt-in `followSymlinks`), and a 16 MB byte ceiling per round-trip. (#74742) Thanks @omarshahine.
|
||||
- Plugins/install: harden official plugin install, uninstall, update, onboarding, ClawHub fallback, npm dependency-state reporting, and beta-channel update paths so externalized plugins behave like first-class package installs.
|
||||
- Gateway/performance: trim startup and Control UI hot paths by lazy-loading plugin/runtime discovery, cron, schema, shutdown, sessions, and model metadata work only when needed.
|
||||
- Channels/replies: improve Discord status reactions and degraded transport reporting, add WhatsApp Channel/Newsletter targets, and tighten Telegram, Feishu, Matrix, Microsoft Teams, and Slack delivery/recovery behavior.
|
||||
- Install/update: recover broken macOS LaunchAgent upgrades, reject source-only plugin packages before runtime load, and repair stale Gateway/plugin state during updates and doctor runs.
|
||||
- Agent/runtime reliability: preserve streamed provider replies, delayed A2A session replies, prompt/tool delivery, memory recall, web search provider discovery, and provider-specific thinking/model metadata across common edge cases.
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -20,21 +270,30 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/performance: lazy-load early runtime discovery and shutdown-hook helpers, defer maintenance timers until after readiness, and trim duplicate plugin auto-enable work during Gateway startup.
|
||||
- QA/Mantis: add a `pnpm openclaw qa mantis discord-smoke` runner and manual GitHub workflow that verify the Mantis Discord bot can see the configured guild/channel, post a smoke message, add a reaction, and upload artifacts.
|
||||
- QA/Slack: add a Slack live transport QA runner with canary and mention-gating coverage for the private bot-to-bot harness. Thanks @vincentkoc.
|
||||
- Gateway/performance: lazy-load the heavy cron runtime after the rest of Gateway startup, defer restart-sentinel refresh after readiness, and let the Gateway startup benchmark write per-run V8 CPU profiles with `--cpu-prof-dir`.
|
||||
- Gateway/performance: keep raw channel-config schema parsing from discovering bundled plugin runtime metadata, and add `pnpm gateway:watch --benchmark-no-force` for profiling startup without the default port cleanup.
|
||||
- Plugins/onboarding: let Manual setup install optional official plugins, including ClawHub-backed diagnostics with npm fallback, and expose the external Codex plugin as a selectable provider setup choice. Thanks @vincentkoc.
|
||||
- Plugins/CLI: include package dependency install state in `openclaw plugins list --json` so scripts can spot missing plugin dependencies without runtime-loading plugins.
|
||||
- Discord/status: add degraded Discord transport and gateway event-loop starvation signals to `openclaw channels status`, `openclaw status --deep`, and fetch-timeout logs so intermittent socket resets do not look like a healthy running channel. (#76327) Thanks @joshavant.
|
||||
- Plugins/update: on the beta OpenClaw update channel, default-line npm and ClawHub plugin updates try `@beta` first and fall back to default/latest when no plugin beta release exists.
|
||||
- Plugins/CLI/update: include package dependency install state in `openclaw plugins list --json`, trust official externalized npm migrations, clean stale bundled load paths for externalized installs, try plugin `@beta` updates first on the beta OpenClaw channel, and fall back to default/latest when no plugin beta release exists.
|
||||
- Plugins/ClawHub: annotate 429 errors with reset windows and unauthenticated higher-rate-limit hints, so operators can tell when downloads recover and when signing in helps. Thanks @romneyda.
|
||||
- Gateway/performance: lazy-load early runtime discovery, shutdown hooks, cron, channel-config schema metadata, restart sentinels, and maintenance timers after readiness; trim duplicate plugin auto-enable work and add startup CPU/profile controls.
|
||||
- Gateway/config: stop Gateway startup and hot reload from auto-restoring invalid config; invalid config now fails closed and `openclaw doctor --fix` owns last-known-good repair.
|
||||
- Discord/status: let explicit reaction tool calls opt into tracking later tool progress with `trackToolCalls: true`, share tool display emoji mapping, and surface degraded Discord transport or gateway event-loop starvation in status output. (#76327) Thanks @joshavant.
|
||||
- Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow outbound target idea from #13424. Thanks @vincentkoc and @agentz-manfred.
|
||||
- Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi.
|
||||
- Agents/tools: skip optional media and PDF tool factories when the effective tool denylist already blocks them, avoiding unnecessary hot-path setup for tools that will be filtered out before model use. (#76773) Thanks @dorukardahan.
|
||||
- Agents/sandbox: store sandbox container and browser registry entries as per-runtime shard files, reducing unrelated session lock contention while `openclaw doctor --fix` migrates legacy monolithic registry files. (#74831) Thanks @luckylhb90.
|
||||
- Plugins/ClawHub: annotate 429 errors from ClawHub with the reset window from `RateLimit-Reset`/`Retry-After` and append a `Sign in for higher rate limits.` hint when the request was unauthenticated, so users can see when downloads will recover and how to lift the cap. Thanks @romneyda.
|
||||
- Tools/BTW: add `/side` as a text and native slash-command alias for `/btw` side questions.
|
||||
- Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi.
|
||||
- QA/Mantis: add a `pnpm openclaw qa mantis discord-smoke` runner and manual GitHub workflow that verify the Mantis Discord bot can see the configured guild/channel, post a smoke message, add a reaction, and upload artifacts.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Web fetch: late-bind `web_fetch` config and provider fallback metadata from the active runtime snapshot, matching `web_search` so long-lived tools do not use stale fetch provider settings. Thanks @vincentkoc.
|
||||
- Plugins/discovery: demote the source-only TypeScript runtime check on already-installed `origin: "global"` plugin packages from a config-blocking error to a warning and let the runtime fall through to the TypeScript source via jiti, so a single broken installed package no longer blocks `plugins install` for unrelated plugins; install-time rejection of newly-installed source-only packages is unchanged. Thanks @romneyda.
|
||||
- Providers/OpenAI Codex: stop the OAuth progress spinner before showing the manual redirect paste prompt, so callback timeouts do not spam `Browser callback did not finish` across terminals.
|
||||
- Channels/WhatsApp: allow `@whiskeysockets/libsignal-node` in `onlyBuiltDependencies` so pnpm v9+ `blockExoticSubdeps` no longer rejects the baileys git-tarball subdep and silences all inbound agent replies. Fixes #76539. Thanks @ottodeng and @vincentkoc.
|
||||
- Gateway/systemd: preserve operator-added secrets in the Gateway env file across re-stage while clearing OpenClaw-managed keys (such as `OPENCLAW_GATEWAY_TOKEN`) so a fresh staging value is never shadowed by a stale env-file copy; operator secrets are also retained when the state-dir `.env` is empty. Fixes #76860. Thanks @hclsys.
|
||||
- Plugin updates: do not short-circuit trusted official npm updates as unchanged when the default/latest spec still resolves to an already-installed prerelease that the installer should replace with a stable fallback. Thanks @vincentkoc.
|
||||
- Plugin tools: keep auth-unavailable optional tools hidden even when another default tool from the same plugin is available and `tools.alsoAllow` names the optional tool. Thanks @vincentkoc.
|
||||
- Realtime transcription: report socket closes before provider readiness as closed-before-ready failures instead of mislabeling them as connection timeouts for OpenAI, xAI, and Deepgram streaming transcription. Thanks @vincentkoc.
|
||||
- OpenAI/Google Meet: fail realtime voice connection attempts when the socket closes before `session.updated`, avoiding stuck Meet joins waiting on a bridge that never became ready. Thanks @vincentkoc.
|
||||
- QA/cache: require the full `CACHE-OK <suffix>` marker before live cache probes stop retrying, so suffix-only prose cannot hide a broken probe response. Thanks @vincentkoc.
|
||||
- Slack/Matrix: avoid creating blank progress-draft messages when `streaming.progress.label=false` and progress tool lines are disabled. Thanks @vincentkoc.
|
||||
- QA/Matrix: keep the mock OpenAI tool-progress provider aligned with exact-marker Matrix prompts so the hardened live preview scenario still forces a deterministic read before final delivery. Thanks @vincentkoc.
|
||||
@@ -62,6 +321,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord: resolve SecretRef-backed bot tokens from the active runtime snapshot for named accounts and keep unresolved configured tokens from crashing status or health checks. (#76987) Thanks @joshavant.
|
||||
- Channels/streaming: expose `streaming.progress.label`, `labels`, `maxLines`, and `toolProgress` in bundled channel config metadata so progress draft settings appear in config, docs, and control surfaces. Thanks @vincentkoc.
|
||||
- Channels/streaming: normalize whitespace and case for `streaming.progress.label: "auto"` so progress draft labels keep using the built-in label pool instead of rendering a literal `auto` title. Thanks @vincentkoc.
|
||||
- Plugins/Codex: preserve Codex-native OAuth routing for `/codex bind` app-server turns so bound sessions keep the selected Codex auth profile instead of falling back to public OpenAI credentials. (#76714) Thanks @keshavbotagent.
|
||||
- Gateway/install: prefer supported system Node over nvm/fnm/volta/asdf/mise when regenerating managed gateway services, so `gateway install --force` no longer recreates service definitions that doctor immediately flags as version-manager-backed. Fixes #76339. Thanks @brokemac79.
|
||||
- Cron/status: render explicit `delivery.mode: "none"` jobs as no-delivery previews and label cron session history distinctly instead of showing fallback delivery or direct-session rows. Fixes #76945.
|
||||
- Gateway/usage: serve `usage.cost` and `sessions.usage` from a durable transcript aggregate cache with lock-safe background refreshes and localized stale-cache status, so large usage views avoid repeated full scans. (#76650) Thanks @Marvinthebored.
|
||||
@@ -88,11 +348,15 @@ Docs: https://docs.openclaw.ai
|
||||
- Status/sessions: ignore malformed non-string persisted session provider/model metadata instead of throwing while rendering status summaries. Fixes #76206. Thanks @vincentkoc.
|
||||
- CLI/config: remove only the targeted array element for `openclaw config unset array[index]` instead of replaying the unset during config write and deleting the shifted next element. Fixes #76290. Thanks @SymbolStar and @vincentkoc.
|
||||
- Plugins/voice-call: treat abnormal local Gateway close code 1006 as a standalone CLI fallback case, so `voicecall smoke` and related commands can still run the provider check path when the Gateway socket closes before returning a response.
|
||||
- CLI/doctor: migrate legacy per-channel `streaming.progress` config into `streaming.preview.toolProgress`, so upgrades with stale Discord or Telegram streaming keys validate again instead of blocking plugin commands.
|
||||
- Plugins/release: reject ClawHub code-plugin packages that contain TypeScript runtime entries without compiled `dist/*.js` output, and run package-local runtime-build checks during npm and ClawHub plugin release previews.
|
||||
- Plugins/update: keep beta-installed OpenClaw package updates on the beta plugin channel even when config still says stable, so Discord and other externalized plugins update from compiled `@beta` packages instead of stale source-only `latest` artifacts.
|
||||
- Agents/tools: stop treating `tools.deny: ["write"]` as an implicit `apply_patch` deny; operators who want to block patch writes should deny `apply_patch` or `group:fs` explicitly. Fixes #76749. (#76795) Thanks @Nek-12 and @hclsys.
|
||||
- Plugins/release: verify published plugin npm tarballs expose compiled runtime entries after publish, catching TS-only package artifacts before release closeout. Thanks @vincentkoc.
|
||||
- CLI/message: exit cleanly with a nonzero status when message-command plugin registry loading fails before dispatch, preventing `openclaw-message` children from staying alive after plugin load errors. Fixes #76168.
|
||||
- Plugins/config: report configured plugins that are present but blocked by path-safety checks as blocked instead of stale `plugin not found` entries, and deduplicate repeated blocked-candidate warnings during discovery. Fixes #76144. Thanks @mayank6136.
|
||||
- Gateway/update: recover an installed-but-unloaded macOS LaunchAgent after package updates, rerun Gateway health/version/channel readiness checks, and print restart, reinstall, and rollback guidance before reporting update failure. (#76790) Thanks @jonathanlindsay.
|
||||
- Codex/runtime: preserve native Codex thread bindings across dynamic-tool reorder and no-tool maintenance turns, and project mirrored history when a legacy Codex run must start without a native binding, preventing follow-up requests from losing conversation context. (#76824) Thanks @VACInc.
|
||||
- CLI/plugins: explain when a missing plugin command alias belongs to a bundled plugin that is disabled by default, including the `openclaw plugins enable <plugin>` repair command. (#76835)
|
||||
- Gateway/Bonjour: auto-start LAN multicast discovery only on macOS hosts while preserving explicit `openclaw plugins enable bonjour` startup elsewhere, so Linux servers and containers that do not need LAN discovery avoid default mDNS probing and watchdog churn. Refs #74209.
|
||||
- Gateway/macOS: stop `doctor` and LaunchAgent recovery from running `launchctl kickstart -k` after a fresh bootstrap, avoiding an immediate SIGTERM of the just-started gateway while still nudging already-loaded launchd jobs. Fixes #76261. Thanks @solosage1.
|
||||
@@ -147,8 +411,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/onboarding: mask credential inputs (model-auth provider API keys, gateway tokens and passwords, web-search provider keys, and skill env-var values) in the interactive `openclaw onboard` wizard so pasted secrets no longer echo into terminal scrollback, `Start-Transcript` logs, or screenshots; existing tokens/passwords are preserved through a masked-preview confirm step before the sensitive prompt. Thanks @anurag-bg-neu.
|
||||
- Control UI/Talk: fix Talk (OpenAI Realtime WebRTC) CORS failure by stripping server-side-only attribution headers (`originator`, `version`, `User-Agent`) from browser offer headers; `api.openai.com/v1/realtime/calls` only allows `authorization` and `content-type` in its CORS preflight, so forwarding these headers caused the browser SDP exchange to fail. Fixes #76435. Thanks @hclsys.
|
||||
- Chat delivery: make `/verbose on|full|off` changes affect subsequent tool-use chat bubbles again, including channels with draft preview tool progress enabled, while preserving one-shot verbose directives.
|
||||
- CLI/logs: auto-reconnect `openclaw logs --follow` on transient gateway disconnects (WebSocket close, timeout, connection drop) with bounded exponential backoff (up to 8 retries, capped at 30 s) and stderr retry warnings, while still exiting immediately on non-recoverable auth or configuration errors. Fixes #74782. (#75059) Thanks @shashank-poola.
|
||||
- CLI/logs: announce `--follow` recovery with a `[logs] gateway reconnected` notice once a poll succeeds after a transient outage, and emit JSON `notice` records in `--json` mode for both the retry warning and the reconnect transition, so live monitoring scripts can react to the recovery. Carries forward #75059. (#75372) Thanks @romneyda.
|
||||
- CLI/logs: auto-reconnect `openclaw logs --follow` on transient gateway disconnects with bounded backoff, stderr retry warnings, `[logs] gateway reconnected` recovery notices, and JSON `notice` records while still exiting immediately on non-recoverable auth or configuration errors. Fixes #74782. (#75059, #75372) Thanks @shashank-poola and @romneyda.
|
||||
- Codex/WhatsApp: keep the `message` dynamic tool available when Codex source replies are configured for message-tool delivery, so coding-profile chat agents do not complete turns privately without a visible channel reply. Fixes #76660. (#76663) Thanks @VishalJ99.
|
||||
- Codex/heartbeat: send heartbeat-specific initiative guidance through Codex turn-scoped collaboration-mode instructions, keeping ordinary message-tool chat turns in Default mode without heartbeat prompt leakage. Thanks @pashpashpash.
|
||||
- Plugins/onboarding: trust optional official plugin and web-search installs selected from the official catalog so npm security scanning treats them like other source-linked official install paths. Thanks @vincentkoc.
|
||||
@@ -168,10 +431,8 @@ Docs: https://docs.openclaw.ai
|
||||
- TUI/Control UI: fix `/think` command showing only base thinking levels when the active session uses a different model from the default, so provider-specific levels like DeepSeek V4 Pro's `xhigh` and `max` are now visible and selectable. Fixes #76482. Thanks @amknight.
|
||||
- CLI/sessions: keep intentional empty agent replies silent after tool-delivered channel output, instead of surfacing a misleading "No reply from agent." fallback. Thanks @vincentkoc.
|
||||
- Config/doctor: cap `.clobbered.*` forensic snapshots per config path and serialize snapshot writes so repeated `doctor --fix` recovery loops cannot flood the config directory. Fixes #76454; carries forward #65649. Thanks @JUSTICEESSIELP, @rsnow, and @vincentkoc.
|
||||
- Feishu: suppress duplicate text when replies send native voice media while preserving captions for ordinary audio files and falling back to text plus attachment links when voice uploads fail.
|
||||
- Feishu: send the skipped reply text when `audioAsVoice` falls back to a generic file attachment after transcode failure, so voice-intent replies do not lose their caption.
|
||||
- TTS/plugins: activate the configured speech provider plugin during Gateway startup, so Microsoft and Local CLI voice replies work immediately after selecting them instead of staying invisible in the startup plugin set. Fixes #76481. Thanks @amknight.
|
||||
- TTS/plugins: include speech providers selected through inherited agent, channel, and account TTS personas during Gateway startup, matching the runtime TTS config merge. Carries forward #76481. Thanks @amknight.
|
||||
- Feishu: suppress duplicate text when replies send native voice media, preserve captions for ordinary audio files, and send fallback text plus attachment links when `audioAsVoice` transcode/upload fallback produces a generic file.
|
||||
- TTS/plugins: activate configured and inherited speech provider plugins during Gateway startup, so Microsoft and Local CLI voice replies work immediately after persona selection instead of staying invisible in the startup plugin set. Fixes #76481. Thanks @amknight.
|
||||
- Feishu: keep packaged Feishu startup from bundling the Lark SDK's ESM `__dirname` path by loading the SDK as a plugin-local runtime dependency. Fixes #76291 and #76494. (#76392) Thanks @zqchris.
|
||||
- Plugins/npm: build package-local runtime dist files for publishable plugins and stop listing root-package-excluded plugin sidecars in the core package metadata, so npm plugin installs such as `@openclaw/diffs` and `@openclaw/discord` no longer publish source-only runtime payloads. Fixes #76426. Thanks @PrinceOfEgypt.
|
||||
- Channels/secrets: resolve SecretRef-backed channel credentials through external plugin secret contracts after the plugin split, covering runtime startup, target discovery, webhook auth, disabled-account enumeration, and late-bound web_search config. Fixes #76371. (#76449) Thanks @joshavant and @neeravmakwana.
|
||||
@@ -185,22 +446,17 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/install: resolve bare official external plugin IDs such as `brave` through the official catalog when no bundled source is available, so packaged installs fetch the intended scoped npm package instead of an unrelated unscoped package. Fixes #76373. Thanks @bek91 and @vincentkoc.
|
||||
- Plugins/install: require OpenClaw-owned install provenance before granting official npm plugin scanner trust, so direct npm package names no longer bypass launch-code scanning while catalog, onboarding, and doctor installs stay trusted. Thanks @fede-kamel and @vincentkoc.
|
||||
- Network proxy: preserve target TLS hostname validation for Node HTTPS requests routed through the managed HTTP proxy, so Discord-style CONNECT traffic no longer validates certificates against the local proxy host. Fixes #74809. (#76442) Thanks @jesse-merhi and @abnershang.
|
||||
- Gateway/sessions: keep async `sessions.list` title and preview hydration bounded to transcript head/tail reads so Control UI polling cannot full-scan large session transcripts every refresh. Thanks @vincentkoc.
|
||||
- Gateway/sessions: cache manifest model-id normalization and bundled setup CLI fallback metadata against the active plugin metadata snapshot, so Control UI `sessions.list` polling avoids repeated plugin manifest scans while still refreshing after plugin reloads. Thanks @rolandrscheel.
|
||||
- Gateway/sessions: keep `sessions.list` rows lightweight by bounding title/preview hydration to transcript head/tail reads and caching manifest model-id normalization plus setup fallback metadata against the active plugin snapshot. Thanks @vincentkoc and @rolandrscheel.
|
||||
- Gateway/performance: cache per-run verbose-level session reads, skip a redundant `lsof` scan in `gateway --force` when no listener was killed, and make the Gateway startup benchmark print usage for `--help`.
|
||||
- Gateway/sessions: keep agent runtime metadata on lightweight `sessions.list` rows so model-only session patches do not make Control UI lose runtime identity. Thanks @vincentkoc.
|
||||
- Gateway/sessions: keep bulk `sessions.list` rows lightweight by skipping per-row transcript usage fallback, display model inference, and plugin projection, avoiding event-loop stalls in large session stores. Thanks @Marvinthebored and @vincentkoc.
|
||||
- Gateway/models: keep read-only `models.list` fallbacks on persisted/current metadata and configured rows while using static auth checks, so missing `models.json` files no longer runtime-load provider discovery or stall gateway after restart. Fixes #76382; refs #76360 and #75707. Thanks @trojy13, @RayWoo, @AnathemaOfficial, and @vincentkoc.
|
||||
- Gateway/models: keep agent image attachment capability checks on the full catalog while preserving the read-only `models.list` path, so image sends are not rejected after static catalog fallback.
|
||||
- Gateway/sessions: keep agent runtime metadata on lightweight `sessions.list` rows and skip per-row transcript usage fallback, display model inference, and plugin projection, avoiding identity loss and event-loop stalls in large session stores. Thanks @Marvinthebored and @vincentkoc.
|
||||
- Gateway/models: keep read-only `models.list` fallbacks on persisted/current metadata, configured rows, registry-compatible fallbacks, and static auth checks while preserving full-catalog image attachment capability checks. Fixes #76382; refs #76360 and #75707. Thanks @trojy13, @RayWoo, @AnathemaOfficial, @Marvinthebored, and @vincentkoc.
|
||||
- CLI/plugins: reject missing plugin ids before config writes in `plugins enable` and `plugins disable` so a typo no longer persists a stale config entry. (#73554) Thanks @ai-hpc.
|
||||
- Agents/sessions: preserve delivered trailing assistant replies during session-file repair so Telegram/WebChat history is not rewritten to drop already-delivered responses. Fixes #76329. Thanks @obviyus.
|
||||
- Gateway/chat history: preserve oversized transcript turns as explicit omitted-message placeholders while avoiding large JSONL parse stalls. Thanks @Marvinthebored and @vincentkoc.
|
||||
- Gateway/models: keep read-only model-list responses on registry-compatible fallbacks and metadata defaults, so empty or minimal persisted model files do not hide built-ins or custom model capabilities. Thanks @Marvinthebored.
|
||||
- CLI/doctor: load the configured memory-slot plugin when resolving memory diagnostics so bundled `memory-core` no longer triggers a false “no active memory plugin” warning on standalone `doctor` / `status` runs. Fixes #76367. Thanks @neeravmakwana.
|
||||
- Gateway: preserve stack diagnostics when `chat.send` or agent attachment parsing/staging fails, improving image-send failure triage. Refs #63432. (#75135) Thanks @keen0206.
|
||||
- Agents/idle-timeout: add a cost-runaway breaker to the outer embedded-run retry loop that halts further attempts after 5 consecutive idle timeouts without completed model progress, so a wedged provider can no longer fan paid model calls out across the same run; completed text or tool-call progress resets the breaker, but partial tool-argument token dribbles do not. Fixes #76293. Thanks @ThePuma312.
|
||||
- Heartbeats/Codex: stop sending the legacy `HEARTBEAT_OK` prompt instruction when heartbeat turns have the structured `heartbeat_respond` tool, while keeping the text sentinel for legacy automatic heartbeat replies. Thanks @pashpashpash.
|
||||
- Heartbeats/Codex: keep structured heartbeat prompts aligned with actual `heartbeat_respond` tool availability and keep tool-disabled commitment check-ins on the legacy ack path. Thanks @pashpashpash and @vincentkoc.
|
||||
- Heartbeats/Codex: align structured heartbeat prompts with actual `heartbeat_respond` tool availability, stop sending legacy `HEARTBEAT_OK` when the tool exists, and keep tool-disabled commitment check-ins on the legacy ack path. Thanks @pashpashpash and @vincentkoc.
|
||||
- Agent runtimes: fail explicit plugin runtime selections honestly when the requested harness is unavailable instead of silently falling back to the embedded PI runtime. Thanks @pashpashpash.
|
||||
- Maintainer workflow: push prepared PR heads through GitHub's verified commit API by default and require an explicit override before git-protocol pushes can publish unsigned commits. Thanks @BunsDev.
|
||||
- Feishu: resolve setup/status probes through the selected/default account so multi-account configs with account-scoped app credentials show as configured and probeable. Fixes #72930. Thanks @brokemac79.
|
||||
@@ -215,8 +471,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Cron: preserve manual `cron.run` IDs in `cron.runs` history so manual run acknowledgements can be correlated with finished run records. Fixes #76276.
|
||||
- CLI/devices: request `operator.admin` for `openclaw devices approve <requestId>` only when the exact pending device request would mint or inherit admin-scoped operator access, while keeping lower-scope approvals on the pairing scope.
|
||||
- Memory/embedding: broaden the embedding reindex retry classifier to include transient socket-layer errors (`fetch failed`, `ECONNRESET`, `socket hang up`, `UND_ERR_*`, `closed`) so memory reindex survives provider network hiccups instead of aborting mid-run. Related #56815, #44166. (#76311) Thanks @buyitsydney.
|
||||
- Memory/sessions: keep rotated and deleted session transcripts (`.jsonl.reset.<iso>` / `.jsonl.deleted.<iso>`) searchable end-to-end by indexing their real content in `buildSessionEntry` instead of short-circuiting to empty entries, and by mapping archive hit paths back to their live transcript stem during `memory_search` visibility filtering so hits are no longer dropped at the guard. `.jsonl.bak.<iso>` backups and compaction checkpoints remain opaque. Refs #56131. Thanks @buyitsydney.
|
||||
- Memory/sessions: emit a `sessionTranscriptUpdate` event when `archiveFileOnDisk` rotates a live session transcript into `.jsonl.reset.<iso>` / `.jsonl.deleted.<iso>` / `.jsonl.bak.<iso>`, and bypass the delta-bytes / delta-messages threshold gate in `processSessionDeltaBatch` for usage-counted archive paths (`.jsonl.reset.<iso>` and `.jsonl.deleted.<iso>`). Without the bypass the archive event was forwarded to the listener but dropped at the threshold check, because an archive is a one-shot file-rename mutation rather than an incremental append and would typically land below the default `deltaBytes: 100000` / `deltaMessages: 50` reindex thresholds. Archives now feed the memory sync incremental path the same way `appendMessage` / compaction / tool-result rewrite / chat inject / command execution events already do. Refs #56131. Thanks @buyitsydney.
|
||||
- Memory/sessions: keep rotated and deleted transcripts (`.jsonl.reset.<iso>` / `.jsonl.deleted.<iso>`) searchable by indexing archive content, mapping archive hits back to live transcript stems, emitting transcript update events on archive rotation, and bypassing incremental delta thresholds for one-shot archive mutations while keeping backups and compaction checkpoints opaque. Refs #56131. Thanks @buyitsydney.
|
||||
- Memory/search: keep sqlite-vec optional in packaged installs and point missing-extension recovery at the valid `agents.defaults.memorySearch.store.vector.extensionPath` setting. Thanks @willemsej and @vincentkoc.
|
||||
- Gateway: keep directly requested plugin tools invokable under restrictive tool profiles while preserving explicit deny lists and the HTTP safety deny list, preventing catalog/invoke mismatches that surface as "Tool not available". Thanks @BunsDev.
|
||||
- Gateway/update: allow beta binaries to refresh gateway services when the config was last written by the matching stable release version, avoiding false newer-config downgrade blocks during beta channel updates.
|
||||
@@ -238,6 +493,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Status/update: resolve beta update-channel checks from the installed version when config still says `stable`, and let `status --deep` reuse live gateway channel credential state instead of warning on command-path-only token misses.
|
||||
- Doctor/plugins: preserve unmanaged third-party plugin `node_modules` during `doctor --fix`, while still pruning OpenClaw-managed runtime dependency caches.
|
||||
- Gateway/restart: add `openclaw gateway restart --force` and `--wait <duration>`, log active task run IDs before restart deferral timers, and report timeout restarts as explicit forced restarts.
|
||||
- Gateway/restart: align `gateway.restart.safe` preflight with scheduled restart deferral by counting only active restart blockers (running non-ended tasks), so queued task records no longer keep "safe" restarts deferred indefinitely. (#76923) Thanks @NikolaFC.
|
||||
- Discord: persist slash-command deploy hashes across process restarts so unchanged command sets skip redeploy and avoid restart-loop 429s.
|
||||
- Providers/LM Studio: normalize binary `off`/`on` reasoning metadata from Gemma 4 and other local models to LM Studio's accepted OpenAI-compatible `reasoning_effort` values.
|
||||
- Plugins/externalization: keep official external install docs, update examples, and live Codex npm checks on default npm tags instead of `@beta`. Thanks @vincentkoc.
|
||||
@@ -245,6 +501,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/ClawHub: fall back to version metadata when the artifact resolver route is missing and keep the Docker ClawHub fixture aligned with npm-pack artifact resolution, avoiding false version-not-found failures during plugin install validation. Thanks @vincentkoc.
|
||||
- Providers/openai-codex: honor `providerConfig.baseUrl` in the dynamic-model synthesis fallback so codex providers configured with a custom upstream (for example a forwarding proxy) no longer silently bypass the configured URL when the registry has no template row to clone for the requested model id. (#76428) Thanks @arniesaha.
|
||||
- Status/channels: show configured channels in `openclaw status` and config-only `openclaw channels status` output even when the Gateway is unreachable, avoiding empty Channels tables on WSL and other no-Gateway paths. Thanks @vincentkoc.
|
||||
- Agents/main-session: keep pending final delivery markers until the final reply is actually routed or queued, so restart and heartbeat recovery can retry failed delivery. Refs #65037. (#75280) Thanks @MertBasar0.
|
||||
- Plugins/ClawHub: explain unavailable explicit ClawHub ClawPack artifact downloads with a temporary npm install hint while ClawHub artifact routing rolls out. Thanks @vincentkoc.
|
||||
- Media: accept home-relative `MEDIA:~/...` attachment paths while preserving existing file-read policy, traversal checks, and media type validation. Fixes #73796. Thanks @fabkury.
|
||||
- Onboarding/search: install official external web-search plugins such as Brave before saving provider config, and make doctor repair reconcile selected external search providers whose npm payload is missing. Thanks @vincentkoc.
|
||||
@@ -258,6 +515,9 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/CLI: make `openclaw gateway start` repair stale managed service definitions that point at old OpenClaw versions, missing binaries, or temporary installer paths before starting.
|
||||
- Heartbeat/scheduler: make heartbeat phase scheduling active-hours-aware so the scheduler seeks forward to the first in-window phase slot instead of arming timers for quiet-hours slots and relying solely on the runtime guard. Non-UTC `activeHours.timezone` values (e.g. `Asia/Shanghai`) now correctly influence when the next heartbeat timer fires, avoiding wasted quiet-hours ticks and long dormant gaps after gateway restarts. Fixes #75487. Thanks @amknight.
|
||||
- Providers/Arcee AI: mark Trinity Large Thinking as tool-incompatible so main-session runs use the same text-only request shape that made subagent runs recover, avoiding the remaining main-session response-shape mismatch after the #62848 transport failover fix. Fixes #62851 and #62847; carries forward #62848. Thanks @Adam-Researchh.
|
||||
- Plugins/SDK: harden run-scoped plugin context cleanup so finalized workflow runs do not leak per-run state. Thanks @100yenadmin.
|
||||
- Plugins/SDK: keep stale async registry cleanup from clearing restored plugin run context and scheduler state after a plugin registry is reactivated. (#75600) Thanks @100yenadmin.
|
||||
- Plugins/SDK: preserve restored plugin scheduler state when earlier delayed replacement cleanup finishes after reactivation. Thanks @100yenadmin.
|
||||
- Status: show the `openai-codex` OAuth profile for `openai/gpt-*` sessions running through the native Codex runtime instead of reporting auth as unknown. (#76197) Thanks @mbelinky.
|
||||
- Gateway: avoid repeated plugin tool descriptor config hashing so large runtime configs do not block reply startup and trigger reconnect/timeouts. (#75944) Thanks @joshavant.
|
||||
- Plugins/externalization: keep diagnostics ClawHub packages and persisted bundled-plugin relocation on npm-first install metadata for launch, and omit Discord from the core package now that its external package is published. Thanks @vincentkoc.
|
||||
@@ -335,6 +595,12 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI: allow deployments to configure grouped chat message max-width with a validated `gateway.controlUi.chatMessageMaxWidth` setting instead of patching bundled CSS after upgrades. Fixes #67935. Thanks @xiew4589-lang.
|
||||
- Control UI/Cron: ignore malformed persisted cron rows without valid payloads before they enter UI state and guard stale cron render paths, preventing blank Control UI sections after a bad cron snapshot. Fixes #55047 and #54439; supersedes #54550 and #54552.
|
||||
- Control UI/sessions: bound the default Sessions tab query to recent activity and fewer rows, avoiding expensive full-history loads while keeping filters editable. Fixes #76050. (#76051) Thanks @Neomail2.
|
||||
- Control UI/sessions: apply reliable `sessions.changed` snapshots in-place and refetch only for partial events, avoiding redundant `sessions.list` regeneration during active session updates.
|
||||
- Control UI/sessions: explain the Sessions filter controls with hover tooltips and raise the default list limit to 200 rows.
|
||||
- Control UI/sessions: expand compaction checkpoint details from checkpoint-bearing rows and keep token totals on one line.
|
||||
- Control UI/sessions: group Active and Limit filters together, streamline source toggles, and make the filter section collapsible.
|
||||
- Control UI/sessions: shorten filter tooltips and remove duplicate browser-native tooltip popovers.
|
||||
- Control UI/sessions: keep the expanded filter controls on one row on large screens.
|
||||
- Gateway/channels: cap startup fanout at four channel/account handoffs and recover from Bonjour ciao self-probe races, reducing Windows startup stalls with many Telegram accounts. Fixes #75687.
|
||||
- Gateway/sessions: keep `sessions.list` polling responsive on large session stores by reusing list-safe session cache/indexes and returning a lightweight compaction checkpoint preview instead of heavyweight summaries. Thanks @rolandrscheel.
|
||||
- Control UI/Gateway: keep long-running dashboard WebSocket sessions alive with protocol pings and keep Stop available after reconnect or reload by recovering session-scoped active-run abort state. Fixes #70991. Thanks @alexandre-leng.
|
||||
|
||||
@@ -93,9 +93,9 @@ Welcome to the lobster tank! 🦞
|
||||
|
||||
## PR Limits
|
||||
|
||||
We cap at **10 open PRs per author**. If you exceed this, the `r: too-many-prs` label is added and your PR is auto-closed. This is a hard limit.
|
||||
We cap at **20 open PRs per author**. If you exceed this, the `r: too-many-prs` label is added and your PR is auto-closed. This is a hard limit.
|
||||
|
||||
For coordinated change sets that genuinely need more than 10 PRs, join the **#clawtributors** channel in Discord and talk to maintainers first.
|
||||
For coordinated change sets that genuinely need more than 20 PRs, join the **#clawtributors** channel in Discord and talk to maintainers first.
|
||||
|
||||
## Before You PR
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026050300
|
||||
versionName = "2026.5.3"
|
||||
versionCode = 2026050400
|
||||
versionName = "2026.5.4"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.5.4 - 2026-05-04
|
||||
|
||||
Maintenance update for the current OpenClaw development release.
|
||||
|
||||
- Gateway pairing now supports scanning QR codes from Settings and accepts full copied setup-code messages while keeping non-loopback `ws://` setup links blocked.
|
||||
|
||||
## 2026.5.3 - 2026-05-03
|
||||
|
||||
Maintenance update for the current OpenClaw development release.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.5.3
|
||||
OPENCLAW_MARKETING_VERSION = 2026.5.3
|
||||
OPENCLAW_IOS_VERSION = 2026.5.4
|
||||
OPENCLAW_MARKETING_VERSION = 2026.5.4
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -241,7 +241,7 @@ gateway can only send pushes for iOS devices that paired with that gateway.
|
||||
|
||||
## What Works Now (Concrete)
|
||||
|
||||
- Pairing via setup code flow (`/pair` then `/pair approve` in Telegram).
|
||||
- Pairing via QR or setup code flow (`/pair qr` or `/pair`, then `/pair approve` in Telegram).
|
||||
- Gateway connection via discovery or manual host/port with TLS fingerprint trust prompt.
|
||||
- Chat + Talk surfaces through the operator gateway session.
|
||||
- iPhone node commands in foreground: camera snap/clip, canvas present/navigate/eval/snapshot, screen record, location, contacts, calendar, reminders, photos, motion, local notifications.
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
struct GatewaySetupPayload: Codable {
|
||||
var url: String?
|
||||
var host: String?
|
||||
var port: Int?
|
||||
var tls: Bool?
|
||||
var bootstrapToken: String?
|
||||
var token: String?
|
||||
var password: String?
|
||||
}
|
||||
|
||||
enum GatewaySetupCode {
|
||||
static func decode(raw: String) -> GatewaySetupPayload? {
|
||||
if let payload = decodeFromJSON(raw) {
|
||||
return payload
|
||||
}
|
||||
if let decoded = decodeBase64Payload(raw),
|
||||
let payload = decodeFromJSON(decoded)
|
||||
{
|
||||
return payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func decodeFromJSON(_ json: String) -> GatewaySetupPayload? {
|
||||
guard let data = json.data(using: .utf8) else { return nil }
|
||||
return try? JSONDecoder().decode(GatewaySetupPayload.self, from: data)
|
||||
}
|
||||
|
||||
private static func decodeBase64Payload(_ raw: String) -> String? {
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
let normalized = trimmed
|
||||
.replacingOccurrences(of: "-", with: "+")
|
||||
.replacingOccurrences(of: "_", with: "/")
|
||||
let padding = normalized.count % 4
|
||||
let padded = padding == 0 ? normalized : normalized + String(repeating: "=", count: 4 - padding)
|
||||
guard let data = Data(base64Encoded: padded) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
@@ -248,38 +248,23 @@ private struct ManualEntryStep: View {
|
||||
return
|
||||
}
|
||||
|
||||
guard let payload = GatewaySetupCode.decode(raw: raw) else {
|
||||
self.setupStatusText = "Setup code not recognized."
|
||||
guard let link = GatewayConnectDeepLink.fromSetupInput(raw) else {
|
||||
self.setupStatusText = "Setup code not recognized or uses an insecure ws:// gateway URL."
|
||||
return
|
||||
}
|
||||
|
||||
if let urlString = payload.url, let url = URL(string: urlString) {
|
||||
self.applyURL(url)
|
||||
} else if let host = payload.host, !host.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
self.manualHost = host.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let port = payload.port {
|
||||
self.manualPortText = String(port)
|
||||
} else {
|
||||
self.manualPortText = ""
|
||||
}
|
||||
if let tls = payload.tls {
|
||||
self.manualUseTLS = tls
|
||||
}
|
||||
} else if let url = URL(string: raw), url.scheme != nil {
|
||||
self.applyURL(url)
|
||||
} else {
|
||||
self.setupStatusText = "Setup code missing URL or host."
|
||||
return
|
||||
}
|
||||
self.manualHost = link.host
|
||||
self.manualPortText = String(link.port)
|
||||
self.manualUseTLS = link.tls
|
||||
|
||||
if let token = payload.token, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
if let token = link.token, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
self.manualToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
} else if payload.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
|
||||
} else if link.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
|
||||
self.manualToken = ""
|
||||
}
|
||||
if let password = payload.password, !password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
if let password = link.password, !password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
self.manualPassword = password.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
} else if payload.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
|
||||
} else if link.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
|
||||
self.manualPassword = ""
|
||||
}
|
||||
|
||||
@@ -287,30 +272,12 @@ private struct ManualEntryStep: View {
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !trimmedInstanceId.isEmpty {
|
||||
let trimmedBootstrapToken =
|
||||
payload.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
link.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
GatewaySettingsStore.saveGatewayBootstrapToken(trimmedBootstrapToken, instanceId: trimmedInstanceId)
|
||||
}
|
||||
|
||||
self.setupStatusText = "Setup code applied."
|
||||
}
|
||||
|
||||
private func applyURL(_ url: URL) {
|
||||
guard let host = url.host, !host.isEmpty else { return }
|
||||
self.manualHost = host
|
||||
if let port = url.port {
|
||||
self.manualPortText = String(port)
|
||||
} else {
|
||||
self.manualPortText = ""
|
||||
}
|
||||
let scheme = (url.scheme ?? "").lowercased()
|
||||
if scheme == "wss" || scheme == "https" {
|
||||
self.manualUseTLS = true
|
||||
} else if scheme == "ws" || scheme == "http" {
|
||||
self.manualUseTLS = false
|
||||
}
|
||||
}
|
||||
|
||||
// (GatewaySetupCode) decode raw setup codes.
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@@ -203,14 +203,7 @@ struct OnboardingWizardView: View {
|
||||
return
|
||||
}
|
||||
if let message = self.detectQRCode(from: data) {
|
||||
if let link = GatewayConnectDeepLink.fromSetupCode(message) {
|
||||
self.handleScannedLink(link)
|
||||
return
|
||||
}
|
||||
if let url = URL(string: message),
|
||||
let route = DeepLinkParser.parse(url),
|
||||
case let .gateway(link) = route
|
||||
{
|
||||
if let link = GatewayConnectDeepLink.fromSetupInput(message) {
|
||||
self.handleScannedLink(link)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -65,20 +65,11 @@ struct QRScannerView: UIViewControllerRepresentable {
|
||||
let payload = barcode.payloadStringValue
|
||||
else { continue }
|
||||
|
||||
// Try setup code format first (base64url JSON from /pair qr).
|
||||
if let link = GatewayConnectDeepLink.fromSetupCode(payload) {
|
||||
if let link = GatewayConnectDeepLink.fromSetupInput(payload) {
|
||||
self.handled = true
|
||||
self.parent.onGatewayLink(link)
|
||||
return
|
||||
}
|
||||
|
||||
// Fall back to deep link URL format (openclaw://gateway?...).
|
||||
if let url = URL(string: payload),
|
||||
let route = DeepLinkParser.parse(url),
|
||||
case let .gateway(link) = route
|
||||
{
|
||||
self.handled = true
|
||||
self.parent.onGatewayLink(link)
|
||||
Task { @MainActor in
|
||||
self.parent.onGatewayLink(link)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ struct SettingsTab: View {
|
||||
@State private var defaultShareInstruction: String = ""
|
||||
@AppStorage("gateway.setupCode") private var setupCode: String = ""
|
||||
@State private var setupStatusText: String?
|
||||
@State private var showQRScanner: Bool = false
|
||||
@State private var scannerError: String?
|
||||
@State private var manualGatewayPortText: String = ""
|
||||
@State private var gatewayExpanded: Bool = true
|
||||
@State private var selectedAgentPickerId: String = ""
|
||||
@@ -98,6 +100,13 @@ struct SettingsTab: View {
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
|
||||
Button {
|
||||
self.openGatewayQRScanner()
|
||||
} label: {
|
||||
Label("Scan QR Code", systemImage: "qrcode.viewfinder")
|
||||
}
|
||||
.disabled(self.connectingGatewayID != nil)
|
||||
|
||||
Button {
|
||||
Task { await self.applySetupCodeAndConnect() }
|
||||
} label: {
|
||||
@@ -430,6 +439,30 @@ struct SettingsTab: View {
|
||||
})
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: self.$showQRScanner) {
|
||||
NavigationStack {
|
||||
QRScannerView(
|
||||
onGatewayLink: { link in
|
||||
self.handleScannedGatewayLink(link)
|
||||
},
|
||||
onError: { error in
|
||||
self.showQRScanner = false
|
||||
self.setupStatusText = "Scanner error: \(error)"
|
||||
self.scannerError = error
|
||||
},
|
||||
onDismiss: {
|
||||
self.showQRScanner = false
|
||||
})
|
||||
.ignoresSafeArea()
|
||||
.navigationTitle("Scan QR Code")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button("Cancel") { self.showQRScanner = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Reset Onboarding?", isPresented: self.$showResetOnboardingAlert) {
|
||||
Button("Reset", role: .destructive) {
|
||||
self.resetOnboarding()
|
||||
@@ -446,6 +479,14 @@ struct SettingsTab: View {
|
||||
message: Text(help.message),
|
||||
dismissButton: .default(Text("OK")))
|
||||
}
|
||||
.alert("QR Scanner Unavailable", isPresented: Binding(
|
||||
get: { self.scannerError != nil },
|
||||
set: { if !$0 { self.scannerError = nil } }))
|
||||
{
|
||||
Button("OK", role: .cancel) {}
|
||||
} message: {
|
||||
Text(self.scannerError ?? "")
|
||||
}
|
||||
.onAppear {
|
||||
self.lastLocationModeRaw = self.locationEnabledModeRaw
|
||||
self.syncManualPortText()
|
||||
@@ -769,39 +810,28 @@ struct SettingsTab: View {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let payload = GatewaySetupCode.decode(raw: raw) else {
|
||||
self.setupStatusText = "Setup code not recognized."
|
||||
guard let link = GatewayConnectDeepLink.fromSetupInput(raw) else {
|
||||
self.setupStatusText = "Setup code not recognized or uses an insecure ws:// gateway URL."
|
||||
return false
|
||||
}
|
||||
|
||||
if let urlString = payload.url, let url = URL(string: urlString) {
|
||||
self.applySetupURL(url)
|
||||
} else if let host = payload.host, !host.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
self.manualGatewayHost = host.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let port = payload.port {
|
||||
self.manualGatewayPort = port
|
||||
self.manualGatewayPortText = String(port)
|
||||
} else {
|
||||
self.manualGatewayPort = 0
|
||||
self.manualGatewayPortText = ""
|
||||
}
|
||||
if let tls = payload.tls {
|
||||
self.manualGatewayTLS = tls
|
||||
}
|
||||
} else if let url = URL(string: raw), url.scheme != nil {
|
||||
self.applySetupURL(url)
|
||||
} else {
|
||||
self.setupStatusText = "Setup code missing URL or host."
|
||||
return false
|
||||
}
|
||||
self.applyGatewayLink(link)
|
||||
return true
|
||||
}
|
||||
|
||||
private func applyGatewayLink(_ link: GatewayConnectDeepLink) {
|
||||
self.manualGatewayHost = link.host
|
||||
self.manualGatewayPort = link.port
|
||||
self.manualGatewayPortText = String(link.port)
|
||||
self.manualGatewayTLS = link.tls
|
||||
|
||||
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let trimmedBootstrapToken =
|
||||
payload.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
link.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if !trimmedInstanceId.isEmpty {
|
||||
GatewaySettingsStore.saveGatewayBootstrapToken(trimmedBootstrapToken, instanceId: trimmedInstanceId)
|
||||
}
|
||||
if let token = payload.token, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
if let token = link.token, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.gatewayToken = trimmedToken
|
||||
if !trimmedInstanceId.isEmpty {
|
||||
@@ -813,7 +843,7 @@ struct SettingsTab: View {
|
||||
GatewaySettingsStore.saveGatewayToken("", instanceId: trimmedInstanceId)
|
||||
}
|
||||
}
|
||||
if let password = payload.password, !password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
if let password = link.password, !password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
let trimmedPassword = password.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.gatewayPassword = trimmedPassword
|
||||
if !trimmedInstanceId.isEmpty {
|
||||
@@ -825,26 +855,33 @@ struct SettingsTab: View {
|
||||
GatewaySettingsStore.saveGatewayPassword("", instanceId: trimmedInstanceId)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func applySetupURL(_ url: URL) {
|
||||
guard let host = url.host, !host.isEmpty else { return }
|
||||
self.manualGatewayHost = host
|
||||
if let port = url.port {
|
||||
self.manualGatewayPort = port
|
||||
self.manualGatewayPortText = String(port)
|
||||
} else {
|
||||
self.manualGatewayPort = 0
|
||||
self.manualGatewayPortText = ""
|
||||
}
|
||||
let scheme = (url.scheme ?? "").lowercased()
|
||||
if scheme == "wss" || scheme == "https" {
|
||||
self.manualGatewayTLS = true
|
||||
} else if scheme == "ws" || scheme == "http" {
|
||||
self.manualGatewayTLS = false
|
||||
private func openGatewayQRScanner() {
|
||||
self.appModel.disconnectGateway()
|
||||
self.connectingGatewayID = nil
|
||||
self.setupStatusText = "Opening QR scanner…"
|
||||
self.showQRScanner = true
|
||||
}
|
||||
|
||||
private func handleScannedGatewayLink(_ link: GatewayConnectDeepLink) {
|
||||
self.showQRScanner = false
|
||||
self.setupCode = ""
|
||||
self.applyGatewayLink(link)
|
||||
self.setupStatusText = "QR loaded. Connecting to \(link.host):\(link.port)…"
|
||||
Task { await self.connectAfterScannedGatewayLink() }
|
||||
}
|
||||
|
||||
private func connectAfterScannedGatewayLink() async {
|
||||
let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let resolvedPort = self.resolvedManualPort(host: host)
|
||||
guard let port = resolvedPort else {
|
||||
self.setupStatusText = "Failed: invalid port"
|
||||
return
|
||||
}
|
||||
let ok = await self.preflightGateway(host: host, port: port, useTLS: self.manualGatewayTLS)
|
||||
guard ok else { return }
|
||||
await self.connectManual()
|
||||
}
|
||||
|
||||
private func resolvedManualPort(host: String) -> Int? {
|
||||
@@ -892,8 +929,6 @@ struct SettingsTab: View {
|
||||
queueLabel: "gateway.preflight")
|
||||
}
|
||||
|
||||
// (GatewaySetupCode) decode raw setup codes.
|
||||
|
||||
private func connectManual() async {
|
||||
let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !host.isEmpty else {
|
||||
|
||||
@@ -21,7 +21,6 @@ Sources/Gateway/GatewayProblemView.swift
|
||||
Sources/Gateway/GatewayQuickSetupSheet.swift
|
||||
Sources/Gateway/GatewayServiceResolver.swift
|
||||
Sources/Gateway/GatewaySettingsStore.swift
|
||||
Sources/Gateway/GatewaySetupCode.swift
|
||||
Sources/Gateway/GatewayTrustPromptAlert.swift
|
||||
Sources/Gateway/KeychainStore.swift
|
||||
Sources/Gateway/TCPProbe.swift
|
||||
|
||||
@@ -161,4 +161,34 @@ private func agentAction(
|
||||
token: nil,
|
||||
password: nil))
|
||||
}
|
||||
|
||||
@Test func parseGatewaySetupInputParsesFullCopiedSetupMessage() {
|
||||
let payload = #"{"url":"wss://gateway.example.com","bootstrapToken":"tok"}"#
|
||||
let link = GatewayConnectDeepLink.fromSetupInput("""
|
||||
Pairing setup code generated.
|
||||
|
||||
Setup code:
|
||||
\(setupCode(from: payload))
|
||||
""")
|
||||
|
||||
#expect(link == .init(
|
||||
host: "gateway.example.com",
|
||||
port: 443,
|
||||
tls: true,
|
||||
bootstrapToken: "tok",
|
||||
token: nil,
|
||||
password: nil))
|
||||
}
|
||||
|
||||
@Test func parseGatewaySetupInputParsesRawGatewayURL() {
|
||||
let link = GatewayConnectDeepLink.fromSetupInput("wss://gateway.example.com:444")
|
||||
|
||||
#expect(link == .init(
|
||||
host: "gateway.example.com",
|
||||
port: 444,
|
||||
tls: true,
|
||||
bootstrapToken: nil,
|
||||
token: nil,
|
||||
password: nil))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
Maintenance update for the current OpenClaw development release.
|
||||
|
||||
- Gateway pairing now supports scanning QR codes from Settings and accepts full copied setup-code messages while keeping non-loopback `ws://` setup links blocked.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.5.3"
|
||||
"version": "2026.5.4"
|
||||
}
|
||||
|
||||
@@ -425,6 +425,7 @@ enum HostEnvSecurityPolicy {
|
||||
"SSL_CERT_DIR",
|
||||
"SSL_CERT_FILE",
|
||||
"SUDO_EDITOR",
|
||||
"SYSTEMROOT",
|
||||
"TF_CLI_CONFIG_FILE",
|
||||
"TF_PLUGIN_CACHE_DIR",
|
||||
"UV_DEFAULT_INDEX",
|
||||
@@ -435,6 +436,7 @@ enum HostEnvSecurityPolicy {
|
||||
"VIRTUAL_ENV",
|
||||
"VISUAL",
|
||||
"WGETRC",
|
||||
"WINDIR",
|
||||
"XDG_CONFIG_DIRS",
|
||||
"XDG_CONFIG_HOME",
|
||||
"YARN_RC_FILENAME",
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.5.3</string>
|
||||
<string>2026.5.4</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026050300</string>
|
||||
<string>2026050400</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -4323,6 +4323,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
public let status: AnyCodable?
|
||||
public let error: String?
|
||||
public let summary: String?
|
||||
public let diagnostics: [String: AnyCodable]?
|
||||
public let delivered: Bool?
|
||||
public let deliverystatus: AnyCodable?
|
||||
public let deliveryerror: String?
|
||||
@@ -4344,6 +4345,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
status: AnyCodable?,
|
||||
error: String?,
|
||||
summary: String?,
|
||||
diagnostics: [String: AnyCodable]?,
|
||||
delivered: Bool?,
|
||||
deliverystatus: AnyCodable?,
|
||||
deliveryerror: String?,
|
||||
@@ -4364,6 +4366,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
self.status = status
|
||||
self.error = error
|
||||
self.summary = summary
|
||||
self.diagnostics = diagnostics
|
||||
self.delivered = delivered
|
||||
self.deliverystatus = deliverystatus
|
||||
self.deliveryerror = deliveryerror
|
||||
@@ -4386,6 +4389,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
case status
|
||||
case error
|
||||
case summary
|
||||
case diagnostics
|
||||
case delivered
|
||||
case deliverystatus = "deliveryStatus"
|
||||
case deliveryerror = "deliveryError"
|
||||
|
||||
@@ -6,6 +6,16 @@ public enum DeepLinkRoute: Sendable, Equatable {
|
||||
}
|
||||
|
||||
public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
|
||||
private struct SetupPayload: Decodable {
|
||||
let url: String?
|
||||
let host: String?
|
||||
let port: Int?
|
||||
let tls: Bool?
|
||||
let bootstrapToken: String?
|
||||
let token: String?
|
||||
let password: String?
|
||||
}
|
||||
|
||||
public let host: String
|
||||
public let port: Int
|
||||
public let tls: Bool
|
||||
@@ -27,28 +37,118 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
|
||||
return URL(string: "\(scheme)://\(self.host):\(self.port)")
|
||||
}
|
||||
|
||||
/// Parse a device-pair setup code (base64url-encoded JSON: `{url, bootstrapToken?, token?, password?}`).
|
||||
/// Parse a gateway setup input from the QR/scanner/manual entry surfaces.
|
||||
///
|
||||
/// Accepted inputs are:
|
||||
/// - device-pair setup code (base64url-encoded JSON)
|
||||
/// - raw setup JSON
|
||||
/// - a copied message containing a `Setup code:` line
|
||||
/// - an `openclaw://gateway?...` deep link
|
||||
/// - a raw `ws://` or `wss://` gateway URL
|
||||
public static func fromSetupInput(_ input: String) -> GatewayConnectDeepLink? {
|
||||
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
if let link = fromSetupCode(trimmed) {
|
||||
return link
|
||||
}
|
||||
if let url = URL(string: trimmed),
|
||||
let route = DeepLinkParser.parse(url),
|
||||
case let .gateway(link) = route
|
||||
{
|
||||
return link
|
||||
}
|
||||
return fromGatewayURLString(
|
||||
trimmed,
|
||||
bootstrapToken: nil,
|
||||
token: nil,
|
||||
password: nil)
|
||||
}
|
||||
|
||||
/// Parse a gateway setup payload from a device-pair setup code or copied setup text.
|
||||
///
|
||||
/// Accepted inputs are:
|
||||
/// - base64url-encoded setup JSON
|
||||
/// - raw setup JSON
|
||||
/// - copied text/message content containing one or more extractable setup-code candidates
|
||||
///
|
||||
/// Accepted payload shapes are:
|
||||
/// - `{url, bootstrapToken?, token?, password?}`
|
||||
/// - `{host, port?, tls?, bootstrapToken?, token?, password?}`
|
||||
///
|
||||
/// URL-based payloads provide the gateway WebSocket URL via `url`. Host-based payloads
|
||||
/// provide `host` plus optional `port` and `tls`. In both cases, the optional
|
||||
/// `bootstrapToken`, `token`, and `password` fields are also supported.
|
||||
public static func fromSetupCode(_ code: String) -> GatewayConnectDeepLink? {
|
||||
guard let data = decodeBase64Url(code) else { return nil }
|
||||
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil }
|
||||
guard let urlString = json["url"] as? String,
|
||||
let parsed = URLComponents(string: urlString),
|
||||
let trimmed = code.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
if let link = decodeSetupPayload(from: Data(trimmed.utf8)) {
|
||||
return link
|
||||
}
|
||||
if let data = decodeBase64Url(trimmed),
|
||||
let link = decodeSetupPayload(from: data)
|
||||
{
|
||||
return link
|
||||
}
|
||||
for candidate in setupCodeCandidates(in: trimmed) where candidate != trimmed {
|
||||
if let data = decodeBase64Url(candidate),
|
||||
let link = decodeSetupPayload(from: data)
|
||||
{
|
||||
return link
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func decodeSetupPayload(from data: Data) -> GatewayConnectDeepLink? {
|
||||
guard let payload = try? JSONDecoder().decode(SetupPayload.self, from: data) else { return nil }
|
||||
if let urlString = payload.url?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!urlString.isEmpty
|
||||
{
|
||||
return fromGatewayURLString(
|
||||
urlString,
|
||||
bootstrapToken: payload.bootstrapToken,
|
||||
token: payload.token,
|
||||
password: payload.password)
|
||||
}
|
||||
guard let host = payload.host?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!host.isEmpty
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let tls = payload.tls ?? true
|
||||
if !tls, !LoopbackHost.isLoopbackHost(host) {
|
||||
return nil
|
||||
}
|
||||
return GatewayConnectDeepLink(
|
||||
host: host,
|
||||
port: payload.port ?? (tls ? 443 : 18789),
|
||||
tls: tls,
|
||||
bootstrapToken: payload.bootstrapToken,
|
||||
token: payload.token,
|
||||
password: payload.password)
|
||||
}
|
||||
|
||||
private static func fromGatewayURLString(
|
||||
_ urlString: String,
|
||||
bootstrapToken: String?,
|
||||
token: String?,
|
||||
password: String?) -> GatewayConnectDeepLink?
|
||||
{
|
||||
guard let parsed = URLComponents(string: urlString),
|
||||
let hostname = parsed.host, !hostname.isEmpty
|
||||
else { return nil }
|
||||
|
||||
let scheme = (parsed.scheme ?? "ws").lowercased()
|
||||
guard scheme == "ws" || scheme == "wss" else { return nil }
|
||||
let tls = scheme == "wss"
|
||||
guard scheme == "ws" || scheme == "wss" || scheme == "http" || scheme == "https" else {
|
||||
return nil
|
||||
}
|
||||
let tls = scheme == "wss" || scheme == "https"
|
||||
if !tls, !LoopbackHost.isLoopbackHost(hostname) {
|
||||
return nil
|
||||
}
|
||||
let port = parsed.port ?? (tls ? 443 : 18789)
|
||||
let bootstrapToken = json["bootstrapToken"] as? String
|
||||
let token = json["token"] as? String
|
||||
let password = json["password"] as? String
|
||||
return GatewayConnectDeepLink(
|
||||
host: hostname,
|
||||
port: port,
|
||||
port: parsed.port ?? (tls ? 443 : 18789),
|
||||
tls: tls,
|
||||
bootstrapToken: bootstrapToken,
|
||||
token: token,
|
||||
@@ -65,6 +165,19 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
|
||||
}
|
||||
return Data(base64Encoded: base64)
|
||||
}
|
||||
|
||||
private static func setupCodeCandidates(in input: String) -> [String] {
|
||||
let surroundingPunctuation = CharacterSet(charactersIn: "`'\"“”‘’()[]{}<>.,;:")
|
||||
return input
|
||||
.components(separatedBy: .whitespacesAndNewlines)
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines.union(surroundingPunctuation)) }
|
||||
.filter { candidate in
|
||||
guard candidate.count >= 24 else { return false }
|
||||
return candidate.allSatisfy { ch in
|
||||
ch.isLetter || ch.isNumber || ch == "-" || ch == "_" || ch == "="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct AgentDeepLink: Codable, Sendable, Equatable {
|
||||
|
||||
@@ -4323,6 +4323,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
public let status: AnyCodable?
|
||||
public let error: String?
|
||||
public let summary: String?
|
||||
public let diagnostics: [String: AnyCodable]?
|
||||
public let delivered: Bool?
|
||||
public let deliverystatus: AnyCodable?
|
||||
public let deliveryerror: String?
|
||||
@@ -4344,6 +4345,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
status: AnyCodable?,
|
||||
error: String?,
|
||||
summary: String?,
|
||||
diagnostics: [String: AnyCodable]?,
|
||||
delivered: Bool?,
|
||||
deliverystatus: AnyCodable?,
|
||||
deliveryerror: String?,
|
||||
@@ -4364,6 +4366,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
self.status = status
|
||||
self.error = error
|
||||
self.summary = summary
|
||||
self.diagnostics = diagnostics
|
||||
self.delivered = delivered
|
||||
self.deliverystatus = deliverystatus
|
||||
self.deliveryerror = deliveryerror
|
||||
@@ -4386,6 +4389,7 @@ public struct CronRunLogEntry: Codable, Sendable {
|
||||
case status
|
||||
case error
|
||||
case summary
|
||||
case diagnostics
|
||||
case delivered
|
||||
case deliverystatus = "deliveryStatus"
|
||||
case deliveryerror = "deliveryError"
|
||||
|
||||
@@ -2,6 +2,14 @@ import Foundation
|
||||
import OpenClawKit
|
||||
import Testing
|
||||
|
||||
private func setupCode(from payload: String) -> String {
|
||||
Data(payload.utf8)
|
||||
.base64EncodedString()
|
||||
.replacingOccurrences(of: "+", with: "-")
|
||||
.replacingOccurrences(of: "/", with: "_")
|
||||
.replacingOccurrences(of: "=", with: "")
|
||||
}
|
||||
|
||||
@Suite struct DeepLinksSecurityTests {
|
||||
@Test func gatewayDeepLinkRejectsInsecureNonLoopbackWs() {
|
||||
let url = URL(
|
||||
@@ -31,33 +39,18 @@ import Testing
|
||||
|
||||
@Test func setupCodeRejectsInsecureNonLoopbackWs() {
|
||||
let payload = #"{"url":"ws://attacker.example:18789","bootstrapToken":"tok"}"#
|
||||
let encoded = Data(payload.utf8)
|
||||
.base64EncodedString()
|
||||
.replacingOccurrences(of: "+", with: "-")
|
||||
.replacingOccurrences(of: "/", with: "_")
|
||||
.replacingOccurrences(of: "=", with: "")
|
||||
#expect(GatewayConnectDeepLink.fromSetupCode(encoded) == nil)
|
||||
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
|
||||
}
|
||||
|
||||
@Test func setupCodeRejectsInsecurePrefixBypassHost() {
|
||||
let payload = #"{"url":"ws://127.attacker.example:18789","bootstrapToken":"tok"}"#
|
||||
let encoded = Data(payload.utf8)
|
||||
.base64EncodedString()
|
||||
.replacingOccurrences(of: "+", with: "-")
|
||||
.replacingOccurrences(of: "/", with: "_")
|
||||
.replacingOccurrences(of: "=", with: "")
|
||||
#expect(GatewayConnectDeepLink.fromSetupCode(encoded) == nil)
|
||||
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
|
||||
}
|
||||
|
||||
@Test func setupCodeAllowsLoopbackWs() {
|
||||
let payload = #"{"url":"ws://127.0.0.1:18789","bootstrapToken":"tok"}"#
|
||||
let encoded = Data(payload.utf8)
|
||||
.base64EncodedString()
|
||||
.replacingOccurrences(of: "+", with: "-")
|
||||
.replacingOccurrences(of: "/", with: "_")
|
||||
.replacingOccurrences(of: "=", with: "")
|
||||
#expect(
|
||||
GatewayConnectDeepLink.fromSetupCode(encoded) == .init(
|
||||
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
|
||||
host: "127.0.0.1",
|
||||
port: 18789,
|
||||
tls: false,
|
||||
@@ -65,4 +58,62 @@ import Testing
|
||||
token: nil,
|
||||
password: nil))
|
||||
}
|
||||
|
||||
@Test func setupCodeParsesHostPayload() {
|
||||
let payload = #"{"host":"gateway.tailnet.ts.net","port":443,"tls":true,"bootstrapToken":"tok"}"#
|
||||
#expect(
|
||||
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
|
||||
host: "gateway.tailnet.ts.net",
|
||||
port: 443,
|
||||
tls: true,
|
||||
bootstrapToken: "tok",
|
||||
token: nil,
|
||||
password: nil))
|
||||
}
|
||||
|
||||
@Test func setupCodeParsesHostPayloadWithTLSDefaultPort() {
|
||||
let payload = #"{"host":"gateway.tailnet.ts.net","tls":true,"bootstrapToken":"tok"}"#
|
||||
#expect(
|
||||
GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == .init(
|
||||
host: "gateway.tailnet.ts.net",
|
||||
port: 443,
|
||||
tls: true,
|
||||
bootstrapToken: "tok",
|
||||
token: nil,
|
||||
password: nil))
|
||||
}
|
||||
|
||||
@Test func setupCodeRejectsInsecureHostPayload() {
|
||||
let payload = #"{"host":"gateway.tailnet.ts.net","port":18789,"tls":false,"bootstrapToken":"tok"}"#
|
||||
#expect(GatewayConnectDeepLink.fromSetupCode(setupCode(from: payload)) == nil)
|
||||
}
|
||||
|
||||
@Test func setupInputParsesFullCopiedSetupMessage() {
|
||||
let payload = #"{"url":"wss://gateway.tailnet.ts.net","bootstrapToken":"tok"}"#
|
||||
let message = """
|
||||
Pairing setup code generated.
|
||||
|
||||
Setup code:
|
||||
\(setupCode(from: payload))
|
||||
"""
|
||||
#expect(
|
||||
GatewayConnectDeepLink.fromSetupInput(message) == .init(
|
||||
host: "gateway.tailnet.ts.net",
|
||||
port: 443,
|
||||
tls: true,
|
||||
bootstrapToken: "tok",
|
||||
token: nil,
|
||||
password: nil))
|
||||
}
|
||||
|
||||
@Test func setupInputParsesRawGatewayURL() {
|
||||
#expect(
|
||||
GatewayConnectDeepLink.fromSetupInput("wss://gateway.example.com:444") == .init(
|
||||
host: "gateway.example.com",
|
||||
port: 444,
|
||||
tls: true,
|
||||
bootstrapToken: nil,
|
||||
token: nil,
|
||||
password: nil))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
34e7f2742624de44bfd1df7743e65ff33a04b0f6fe251bc417a6b33f85529772 config-baseline.json
|
||||
5b5ebd95939d75496597d9858a375e27544812d0f79dc3b4bf87c794ada2ba08 config-baseline.core.json
|
||||
655d1309b70505e73198df20c5088784290b33098efd42027d3c09beeb3704a7 config-baseline.channel.json
|
||||
055fae0d0067a751dc10125af7421da45633f73519c94c982d02b0c4eb2bdf67 config-baseline.plugin.json
|
||||
2c78fb7af01e2ee9e919be5ab7b675347b36cae1e347f97fd2640a6f7c72f3ac config-baseline.json
|
||||
31ec333df9f8b92c7656ac7107cecd5860dd02e08f7e18c7c674dc47a8811baa config-baseline.core.json
|
||||
cd7c0c7fb1435bc7e59099e9ac334462d5ad444016e9ab4512aae63a238f78dc config-baseline.channel.json
|
||||
9832b30a696930a3da7efccf38073137571e1b66cae84e54d747b733fdafcc54 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
0dd4f5abaf72f0d6b3fe5777cbf16c7a8c8052eece17436dc0ac2809b0ea27de plugin-sdk-api-baseline.json
|
||||
2c2170cf2f1193f7dbecdef3ccd1b601992407e3d99863d1aa13cb1817c238fd plugin-sdk-api-baseline.jsonl
|
||||
a7116e6c0cae4c7b9ee7cd6dc48f2978812f4b5be647f3e36eee91ec9a81d85e plugin-sdk-api-baseline.json
|
||||
2b6c9883d701379761724e21946d417399c1247e6a244d6b00c4a982c8ef5968 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -252,6 +252,8 @@ Once DMs are working, you can set up your Discord server as a full workspace whe
|
||||
|
||||
In guild channels, normal assistant final replies stay private by default. Visible Discord output must be sent explicitly with the `message` tool, so the agent can lurk by default and only post when it decides a channel reply is useful.
|
||||
|
||||
This means the selected model must reliably call tools. If Discord shows typing and the logs show token usage but no posted message, check the session log for assistant text with `didSendViaMessagingTool: false`. That means the model produced a private final answer instead of calling `message(action=send)`. Switch to a stronger tool-calling model, or use the config below to restore legacy automatic final replies.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Ask your agent">
|
||||
> "Allow my agent to respond on this server without having to be @mentioned"
|
||||
@@ -683,6 +685,25 @@ Default slash command settings:
|
||||
- `block` emits draft-sized chunks (use `draftChunk` to tune size and breakpoints, clamped to `textChunkLimit`).
|
||||
- Media, error, and explicit-reply finals cancel pending preview edits.
|
||||
- `streaming.preview.toolProgress` (default `true`) controls whether tool/progress updates reuse the preview message.
|
||||
- `streaming.preview.commandText` / `streaming.progress.commandText` controls command/exec detail in compact progress lines: `raw` (default) or `status` (tool label only).
|
||||
|
||||
Hide raw command/exec text while keeping compact progress lines:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"discord": {
|
||||
"streaming": {
|
||||
"mode": "progress",
|
||||
"progress": {
|
||||
"toolProgress": true,
|
||||
"commandText": "status"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Preview streaming is text-only; media replies fall back to normal delivery. When `block` streaming is explicitly enabled, OpenClaw skips the preview stream to avoid double-streaming.
|
||||
|
||||
|
||||
@@ -44,6 +44,14 @@ For group/channel rooms, OpenClaw defaults to `messages.groupChat.visibleReplies
|
||||
`openclaw doctor --fix` writes this default into configured-channel configs that omit it.
|
||||
That means the agent still processes the turn and can update memory/session state, but its normal final answer is not automatically posted back into the room. To speak visibly, the agent uses `message(action=send)`.
|
||||
|
||||
This default depends on a model/runtime that reliably calls tools. If logs show
|
||||
assistant text but `didSendViaMessagingTool: false`, the model answered
|
||||
privately instead of calling the message tool. That is not a
|
||||
Discord/Slack/Telegram send failure. Use a tool-call-reliable model for
|
||||
group/channel sessions, or set
|
||||
`messages.groupChat.visibleReplies: "automatic"` to restore legacy visible
|
||||
final replies.
|
||||
|
||||
If the message tool is unavailable under the active tool policy, OpenClaw falls
|
||||
back to automatic visible replies instead of silently suppressing the response.
|
||||
`openclaw doctor` warns about this mismatch.
|
||||
|
||||
@@ -39,6 +39,7 @@ openclaw gateway run
|
||||
|
||||
## Security defaults
|
||||
|
||||
- IRC uses raw TCP/TLS sockets outside OpenClaw operator-managed forward proxy routing. In deployments that require all egress through that forward proxy, set `channels.irc.enabled=false` unless direct IRC egress is explicitly approved.
|
||||
- `channels.irc.dmPolicy` defaults to `"pairing"`.
|
||||
- `channels.irc.groupPolicy` defaults to `"allowlist"`.
|
||||
- With `groupPolicy="allowlist"`, set `channels.irc.groups` to define allowed channels.
|
||||
|
||||
@@ -113,7 +113,7 @@ If you use the `device-pair` plugin, you can do first-time device pairing entire
|
||||
1. In Telegram, message your bot: `/pair`
|
||||
2. The bot replies with two messages: an instruction message and a separate **setup code** message (easy to copy/paste in Telegram).
|
||||
3. On your phone, open the OpenClaw iOS app → Settings → Gateway.
|
||||
4. Paste the setup code and connect.
|
||||
4. Scan the QR code or paste the setup code and connect.
|
||||
5. Back in Telegram: `/pair pending` (review request IDs, role, and scopes), then approve.
|
||||
|
||||
The setup code is a base64-encoded JSON payload that contains:
|
||||
@@ -134,6 +134,13 @@ That bootstrap token carries the built-in pairing bootstrap profile:
|
||||
|
||||
Treat the setup code like a password while it is valid.
|
||||
|
||||
For Tailscale, public, or other non-loopback mobile pairing, use Tailscale
|
||||
Serve/Funnel or another `wss://` Gateway URL. Direct non-loopback `ws://` setup
|
||||
URLs are rejected before QR/setup-code issuance. Plaintext `ws://` setup codes
|
||||
are limited to loopback URLs; private-network `ws://` clients still require the explicit
|
||||
`OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` break-glass described in the remote
|
||||
Gateway guide.
|
||||
|
||||
### Approve a node device
|
||||
|
||||
```bash
|
||||
|
||||
@@ -666,6 +666,25 @@ Notes:
|
||||
- `block`: append chunked preview updates.
|
||||
- `progress`: show progress status text while generating, then send final text.
|
||||
- `streaming.preview.toolProgress`: when draft preview is active, route tool/progress updates into the same edited preview message (default: `true`). Set `false` to keep separate tool/progress messages.
|
||||
- `streaming.preview.commandText` / `streaming.progress.commandText`: set to `status` to keep compact tool-progress lines while hiding raw command/exec text (default: `raw`).
|
||||
|
||||
Hide raw command/exec text while keeping compact progress lines:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"slack": {
|
||||
"streaming": {
|
||||
"mode": "progress",
|
||||
"progress": {
|
||||
"toolProgress": true,
|
||||
"commandText": "status"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`channels.slack.streaming.nativeTransport` controls Slack native text streaming when `channels.slack.streaming.mode` is `partial` (default: `true`).
|
||||
|
||||
|
||||
@@ -280,6 +280,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
- `channels.telegram.streaming` is `off | partial | block | progress` (default: `partial`)
|
||||
- `progress` keeps one editable status draft and updates it with tool progress until final delivery
|
||||
- `streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true` when preview streaming is active)
|
||||
- `streaming.preview.commandText` controls command/exec detail inside those tool-progress lines: `raw` (default, preserves released behavior) or `status` (tool label only)
|
||||
- legacy `channels.telegram.streamMode` and boolean `streaming` values are detected; run `openclaw doctor --fix` to migrate them to `channels.telegram.streaming.mode`
|
||||
|
||||
Tool-progress preview updates are the short status lines shown while tools run, for example command execution, file reads, planning updates, or patch summaries. Telegram keeps these enabled by default to match released OpenClaw behavior from `v2026.4.22` and later. To keep the edited preview for answer text but hide tool-progress lines, set:
|
||||
@@ -299,6 +300,41 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
}
|
||||
```
|
||||
|
||||
To keep tool-progress visible but hide command/exec text, set:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"streaming": {
|
||||
"mode": "partial",
|
||||
"preview": {
|
||||
"commandText": "status"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For progress-draft mode, put the same command-text policy under `streaming.progress`:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"streaming": {
|
||||
"mode": "progress",
|
||||
"progress": {
|
||||
"toolProgress": true,
|
||||
"commandText": "status"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `streaming.mode: "off"` only when you want final-only delivery: Telegram preview edits are disabled and generic tool/progress chatter is suppressed instead of being sent as standalone status messages. Approval prompts, media payloads, and errors still route through normal final delivery. Use `streaming.preview.toolProgress: false` when you only want to keep answer preview edits while hiding the tool-progress status lines.
|
||||
|
||||
<Note>
|
||||
@@ -318,6 +354,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
Telegram-only reasoning stream:
|
||||
|
||||
- `/reasoning stream` sends reasoning to the live preview while generating
|
||||
- the reasoning preview is deleted after final delivery; use `/reasoning on` when reasoning should remain visible
|
||||
- final answer is sent without reasoning text
|
||||
|
||||
</Accordion>
|
||||
@@ -740,11 +777,12 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
- `channels.telegram.dms["<user_id>"].historyLimit`
|
||||
- `channels.telegram.retry` config applies to Telegram send helpers (CLI/tools/actions) for recoverable outbound API errors. Inbound final-reply delivery also uses a bounded safe-send retry for Telegram pre-connect failures, but it does not retry ambiguous post-send network envelopes that could duplicate visible messages.
|
||||
|
||||
CLI send target can be numeric chat ID or username:
|
||||
CLI and message-tool send targets can be numeric chat ID, username, or a forum topic target:
|
||||
|
||||
```bash
|
||||
openclaw message send --channel telegram --target 123456789 --message "hi"
|
||||
openclaw message send --channel telegram --target @name --message "hi"
|
||||
openclaw message send --channel telegram --target -1001234567890:topic:42 --message "hi topic"
|
||||
```
|
||||
|
||||
Telegram polls use `openclaw message poll` and support forum topics:
|
||||
|
||||
@@ -60,11 +60,12 @@ Full troubleshooting: [Telegram troubleshooting](/channels/telegram#troubleshoot
|
||||
|
||||
### Discord failure signatures
|
||||
|
||||
| Symptom | Fastest check | Fix |
|
||||
| ------------------------------- | ----------------------------------- | --------------------------------------------------------- |
|
||||
| Bot online but no guild replies | `openclaw channels status --probe` | Allow guild/channel and verify message content intent. |
|
||||
| Group messages ignored | Check logs for mention gating drops | Mention bot or set guild/channel `requireMention: false`. |
|
||||
| DM replies missing | `openclaw pairing list discord` | Approve DM pairing or adjust DM policy. |
|
||||
| Symptom | Fastest check | Fix |
|
||||
| ----------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Bot online but no guild replies | `openclaw channels status --probe` | Allow guild/channel and verify message content intent. |
|
||||
| Group messages ignored | Check logs for mention gating drops | Mention bot or set guild/channel `requireMention: false`. |
|
||||
| Typing/token usage but no Discord message | Session log shows assistant text with `didSendViaMessagingTool: false` | The model answered privately instead of calling the message tool. Use a tool-call-reliable model, or set `messages.groupChat.visibleReplies: "automatic"` to auto-post. |
|
||||
| DM replies missing | `openclaw pairing list discord` | Approve DM pairing or adjust DM policy. |
|
||||
|
||||
Full troubleshooting: [Discord troubleshooting](/channels/discord#troubleshooting)
|
||||
|
||||
|
||||
@@ -81,7 +81,9 @@ openclaw directory groups list --channel zalouser --query "work"
|
||||
|
||||
`channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`).
|
||||
|
||||
`channels.zalouser.allowFrom` accepts user IDs or names. During setup, names are resolved to IDs using the plugin's in-process contact lookup.
|
||||
`channels.zalouser.allowFrom` should use stable Zalo user IDs. During interactive setup, entered names can be resolved to IDs using the plugin's in-process contact lookup.
|
||||
|
||||
If a raw name remains in config, startup resolves it only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled. Without that opt-in, runtime sender checks are ID-only and raw names are ignored for authorization.
|
||||
|
||||
Approve via:
|
||||
|
||||
@@ -93,13 +95,13 @@ Approve via:
|
||||
- Default: `channels.zalouser.groupPolicy = "open"` (groups allowed). Use `channels.defaults.groupPolicy` to override the default when unset.
|
||||
- Restrict to an allowlist with:
|
||||
- `channels.zalouser.groupPolicy = "allowlist"`
|
||||
- `channels.zalouser.groups` (keys should be stable group IDs; names are resolved to IDs on startup when possible)
|
||||
- `channels.zalouser.groups` (keys should be stable group IDs; names are resolved to IDs on startup only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled)
|
||||
- `channels.zalouser.groupAllowFrom` (controls which senders in allowed groups can trigger the bot)
|
||||
- Block all groups: `channels.zalouser.groupPolicy = "disabled"`.
|
||||
- The configure wizard can prompt for group allowlists.
|
||||
- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping.
|
||||
- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled.
|
||||
- Group allowlist matching is ID-only by default. Unresolved names are ignored for auth unless `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled.
|
||||
- `channels.zalouser.dangerouslyAllowNameMatching: true` is a break-glass compatibility mode that re-enables mutable group-name matching.
|
||||
- `channels.zalouser.dangerouslyAllowNameMatching: true` is a break-glass compatibility mode that re-enables mutable startup name resolution and runtime group-name matching.
|
||||
- If `groupAllowFrom` is unset, runtime falls back to `allowFrom` for group sender checks.
|
||||
- Sender checks apply to both normal group messages and control commands (for example `/new`, `/reset`).
|
||||
|
||||
@@ -181,7 +183,7 @@ Accounts map to `zalouser` profiles in OpenClaw state. Example:
|
||||
|
||||
**Allowlist/group name didn't resolve:**
|
||||
|
||||
- Use numeric IDs in `allowFrom`/`groupAllowFrom`/`groups`, or exact friend/group names.
|
||||
- Use numeric IDs in `allowFrom`/`groupAllowFrom` and stable group IDs in `groups`. If you intentionally need exact friend/group names, enable `channels.zalouser.dangerouslyAllowNameMatching: true`.
|
||||
|
||||
**Upgraded from old CLI-based setup:**
|
||||
|
||||
|
||||
89
docs/ci.md
89
docs/ci.md
@@ -493,16 +493,93 @@ The sanity check fails fast when required root files such as `pnpm-lock.yaml` di
|
||||
|
||||
`pnpm testbox:run` also terminates a local Blacksmith CLI invocation that stays in the sync phase for more than five minutes without post-sync output. Set `OPENCLAW_TESTBOX_SYNC_TIMEOUT_MS=0` to disable that guard, or use a larger millisecond value for unusually large local diffs.
|
||||
|
||||
Crabbox is the repo-owned second remote-box path for Linux proof when Blacksmith is unavailable or when owned cloud capacity is preferable. Warm a box, hydrate it through the project workflow, then run commands through the Crabbox CLI:
|
||||
Crabbox is the repo-owned remote-box wrapper for maintainer Linux proof. Use it when a check is too broad for a local edit loop, when CI parity matters, or when the proof needs secrets, Docker, package lanes, reusable boxes, or remote logs. The normal OpenClaw backend is `blacksmith-testbox`; owned AWS/Hetzner capacity is a fallback for Blacksmith outages, quota issues, or explicit owned-capacity testing.
|
||||
|
||||
Before a first run, check the wrapper from the repo root:
|
||||
|
||||
```bash
|
||||
pnpm crabbox:warmup -- --idle-timeout 90m
|
||||
pnpm crabbox:hydrate -- --id <cbx_id>
|
||||
pnpm crabbox:run -- --id <cbx_id> --shell "OPENCLAW_TESTBOX=1 pnpm check:changed"
|
||||
pnpm crabbox:stop -- <cbx_id>
|
||||
pnpm crabbox:run -- --help | sed -n '1,120p'
|
||||
```
|
||||
|
||||
`.crabbox.yaml` owns provider, sync, and GitHub Actions hydration defaults. It excludes local `.git` so the hydrated Actions checkout keeps its own remote Git metadata instead of syncing maintainer-local remotes and object stores, and it excludes local runtime/build artifacts that should never be transferred. `.github/workflows/crabbox-hydrate.yml` owns checkout, Node/pnpm setup, `origin/main` fetch, and the non-secret environment handoff that later `crabbox run --id <cbx_id>` commands source.
|
||||
The repo wrapper refuses a stale Crabbox binary that does not advertise `blacksmith-testbox`. Pass the provider explicitly even though `.crabbox.yaml` has owned-cloud defaults.
|
||||
|
||||
Changed gate:
|
||||
|
||||
```bash
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox \
|
||||
--blacksmith-org openclaw \
|
||||
--blacksmith-workflow .github/workflows/ci-check-testbox.yml \
|
||||
--blacksmith-job check \
|
||||
--blacksmith-ref main \
|
||||
--idle-timeout 90m \
|
||||
--ttl 240m \
|
||||
--timing-json \
|
||||
--shell -- \
|
||||
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm check:changed"
|
||||
```
|
||||
|
||||
Focused test rerun:
|
||||
|
||||
```bash
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox \
|
||||
--blacksmith-org openclaw \
|
||||
--blacksmith-workflow .github/workflows/ci-check-testbox.yml \
|
||||
--blacksmith-job check \
|
||||
--blacksmith-ref main \
|
||||
--idle-timeout 90m \
|
||||
--ttl 240m \
|
||||
--timing-json \
|
||||
--shell -- \
|
||||
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test <path-or-filter>"
|
||||
```
|
||||
|
||||
Full suite:
|
||||
|
||||
```bash
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox \
|
||||
--blacksmith-org openclaw \
|
||||
--blacksmith-workflow .github/workflows/ci-check-testbox.yml \
|
||||
--blacksmith-job check \
|
||||
--blacksmith-ref main \
|
||||
--idle-timeout 90m \
|
||||
--ttl 240m \
|
||||
--timing-json \
|
||||
--shell -- \
|
||||
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test"
|
||||
```
|
||||
|
||||
Read the final JSON summary. The useful fields are `provider`, `leaseId`, `syncDelegated`, `exitCode`, `commandMs`, and `totalMs`. One-shot Blacksmith-backed Crabbox runs should stop the Testbox automatically; if a run is interrupted or cleanup is unclear, inspect live boxes and stop only the boxes you created:
|
||||
|
||||
```bash
|
||||
blacksmith testbox list
|
||||
blacksmith testbox stop --id <tbx_id>
|
||||
```
|
||||
|
||||
Use reuse only when you intentionally need multiple commands on the same hydrated box:
|
||||
|
||||
```bash
|
||||
pnpm crabbox:run -- --provider blacksmith-testbox --id <tbx_id> --no-sync --timing-json --shell -- "pnpm test <path-or-filter>"
|
||||
pnpm crabbox:stop -- <tbx_id>
|
||||
```
|
||||
|
||||
If Crabbox is the broken layer but Blacksmith itself works, use direct Blacksmith as a narrow fallback:
|
||||
|
||||
```bash
|
||||
blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90
|
||||
blacksmith testbox run --id <tbx_id> "env CI=1 NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm check:changed"
|
||||
blacksmith testbox stop --id <tbx_id>
|
||||
```
|
||||
|
||||
Escalate to owned Crabbox capacity only when Blacksmith is down, quota-limited, missing the needed environment, or owned capacity is explicitly the goal:
|
||||
|
||||
```bash
|
||||
pnpm crabbox:warmup -- --provider aws --class beast --market on-demand --idle-timeout 90m
|
||||
pnpm crabbox:hydrate -- --id <cbx_id-or-slug>
|
||||
pnpm crabbox:run -- --id <cbx_id-or-slug> --timing-json --shell -- "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm check:changed"
|
||||
pnpm crabbox:stop -- <cbx_id-or-slug>
|
||||
```
|
||||
|
||||
`.crabbox.yaml` owns provider, sync, and GitHub Actions hydration defaults for owned-cloud lanes. It excludes local `.git` so the hydrated Actions checkout keeps its own remote Git metadata instead of syncing maintainer-local remotes and object stores, and it excludes local runtime/build artifacts that should never be transferred. `.github/workflows/crabbox-hydrate.yml` owns checkout, Node/pnpm setup, `origin/main` fetch, and the non-secret environment handoff for owned-cloud `crabbox run --id <cbx_id>` commands.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ openclaw daemon uninstall
|
||||
|
||||
- `status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
|
||||
- `install`: `--port`, `--runtime <node|bun>`, `--token`, `--force`, `--json`
|
||||
- `restart`: `--force`, `--wait <duration>`, `--json`
|
||||
- `restart`: `--safe`, `--force`, `--wait <duration>`, `--json`
|
||||
- lifecycle (`uninstall|start|stop`): `--json`
|
||||
|
||||
Notes:
|
||||
@@ -53,6 +53,7 @@ Notes:
|
||||
- If both `gateway.auth.token` and `gateway.auth.password` are configured and `gateway.auth.mode` is unset, install is blocked until mode is set explicitly.
|
||||
- On macOS, `install` keeps LaunchAgent plists owner-only and loads managed service environment values through an owner-only file and wrapper instead of serializing API keys or auth-profile env refs into `EnvironmentVariables`.
|
||||
- If you intentionally run multiple gateways on one host, isolate ports, config/state, and workspaces; see [/gateway#multiple-gateways-same-host](/gateway#multiple-gateways-same-host).
|
||||
- `restart --safe` asks the running Gateway to preflight active work and schedule one coalesced restart after active work drains. Plain `restart` keeps the existing service-manager behavior; `--force` remains the immediate override path.
|
||||
|
||||
## Prefer
|
||||
|
||||
|
||||
@@ -105,6 +105,16 @@ openclaw gateway run
|
||||
Raw stream jsonl path.
|
||||
</ParamField>
|
||||
|
||||
## Restart the Gateway
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
openclaw gateway restart --safe
|
||||
openclaw gateway restart --force
|
||||
```
|
||||
|
||||
`openclaw gateway restart --safe` asks the running Gateway to preflight active OpenClaw work before restarting. If queued operations, reply delivery, embedded runs, or task runs are active, the Gateway reports the blockers, coalesces duplicate safe restart requests, and restarts once the active work drains. Plain `restart` keeps the existing service-manager behavior for compatibility. Use `--force` only when you explicitly want the immediate override path.
|
||||
|
||||
<Warning>
|
||||
Inline `--password` can be exposed in local process listings. Prefer `--password-file`, env, or a SecretRef-backed `gateway.auth.password`.
|
||||
</Warning>
|
||||
|
||||
@@ -27,7 +27,7 @@ Channel selection:
|
||||
Target formats (`--target`):
|
||||
|
||||
- WhatsApp: E.164, group JID, or WhatsApp Channel/Newsletter JID (`...@newsletter`)
|
||||
- Telegram: chat id or `@username`
|
||||
- Telegram: chat id, `@username`, or forum topic target (`-1001234567890:topic:42`, or `--thread-id 42`)
|
||||
- Discord: `channel:<id>` or `user:<id>` (or `<@id>` mention; raw numeric ids are treated as channels)
|
||||
- Google Chat: `spaces/<spaceId>` or `users/<userId>`
|
||||
- Slack: `channel:<id>` or `user:<id>` (raw channel id is accepted)
|
||||
|
||||
@@ -162,6 +162,7 @@ openclaw models fallbacks list
|
||||
|
||||
```bash
|
||||
openclaw models auth add
|
||||
openclaw models auth list [--provider <id>] [--json]
|
||||
openclaw models auth login --provider <id>
|
||||
openclaw models auth setup-token --provider <id>
|
||||
openclaw models auth paste-token
|
||||
@@ -171,16 +172,22 @@ openclaw models auth paste-token
|
||||
flow (OAuth/API key) or guide you into manual token paste, depending on the
|
||||
provider you choose.
|
||||
|
||||
`models auth list` lists saved auth profiles for the selected agent without
|
||||
printing token, API-key, or OAuth secret material. Use `--provider <id>` to
|
||||
filter to one provider, such as `openai-codex`, and `--json` for scripting.
|
||||
|
||||
`models auth login` runs a provider plugin’s auth flow (OAuth/API key). Use
|
||||
`openclaw plugins list` to see which providers are installed.
|
||||
Use `openclaw models auth --agent <id> <subcommand>` to write auth results to a
|
||||
specific configured agent store. The parent `--agent` flag is honored by
|
||||
`add`, `login`, `setup-token`, `paste-token`, and `login-github-copilot`.
|
||||
`add`, `list`, `login`, `setup-token`, `paste-token`, and
|
||||
`login-github-copilot`.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
openclaw models auth login --provider openai-codex --set-default
|
||||
openclaw models auth list --provider openai-codex
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -134,7 +134,7 @@ is available, then fall back to `latest`.
|
||||
|
||||
Use `npm:<package>` when you want to make npm resolution explicit. Bare package specs also install directly from npm during the launch cutover.
|
||||
|
||||
Bare specs and `@latest` stay on the stable track. If npm resolves either of those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a prerelease tag such as `@beta`/`@rc` or an exact prerelease version such as `@1.2.3-beta.4`.
|
||||
Bare specs and `@latest` stay on the stable track. OpenClaw date-stamped correction versions such as `2026.5.3-1` are stable releases for this check. If npm resolves either of those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a prerelease tag such as `@beta`/`@rc` or an exact prerelease version such as `@1.2.3-beta.4`.
|
||||
|
||||
If a bare install spec matches an official plugin id (for example `diffs`), OpenClaw installs the catalog entry directly. To install an npm package with the same name, use an explicit scoped spec (for example `@scope/diffs`).
|
||||
|
||||
@@ -387,6 +387,8 @@ The local plugin registry is OpenClaw's persisted cold read model for installed
|
||||
|
||||
Use `plugins registry` to inspect whether the persisted registry is present, current, or stale. Use `--refresh` to rebuild it from the persisted plugin index, config policy, and manifest/package metadata. This is a repair path, not a runtime activation path.
|
||||
|
||||
`openclaw doctor --fix` also repairs registry-adjacent managed npm drift: if an orphaned or recovered `@openclaw/*` package under the managed plugin npm root shadows a bundled plugin, doctor removes that stale package and rebuilds the registry so startup validates against the bundled manifest.
|
||||
|
||||
<Warning>
|
||||
`OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY=1` is a deprecated break-glass compatibility switch for registry read failures. Prefer `plugins registry --refresh` or `openclaw doctor --fix`; the env fallback is only for emergency startup recovery while the migration rolls out.
|
||||
</Warning>
|
||||
|
||||
@@ -23,7 +23,7 @@ captured blobs, and purge local capture data.
|
||||
```bash
|
||||
openclaw proxy start [--host <host>] [--port <port>]
|
||||
openclaw proxy run [--host <host>] [--port <port>] -- <cmd...>
|
||||
openclaw proxy validate [--json] [--proxy-url <url>] [--allowed-url <url>] [--denied-url <url>] [--timeout-ms <ms>]
|
||||
openclaw proxy validate [--json] [--proxy-url <url>] [--allowed-url <url>] [--denied-url <url>] [--apns-reachable] [--apns-authority <url>] [--timeout-ms <ms>]
|
||||
openclaw proxy coverage
|
||||
openclaw proxy sessions [--limit <count>]
|
||||
openclaw proxy query --preset <name> [--session <id>]
|
||||
@@ -40,7 +40,10 @@ before changing config. By default it verifies that a public destination succeed
|
||||
through the proxy and that the proxy cannot reach a temporary loopback canary.
|
||||
Custom denied destinations are fail-closed: HTTP responses and ambiguous
|
||||
transport failures both fail unless you can verify a deployment-specific denial
|
||||
signal separately.
|
||||
signal separately. Add `--apns-reachable` to also open an APNs HTTP/2 CONNECT
|
||||
tunnel through the proxy and confirm sandbox APNs responds; the probe uses an
|
||||
intentionally invalid provider token, so an APNs `403 InvalidProviderToken`
|
||||
response is a successful reachability signal.
|
||||
|
||||
Options:
|
||||
|
||||
@@ -48,6 +51,8 @@ Options:
|
||||
- `--proxy-url <url>`: validate this proxy URL instead of config or env.
|
||||
- `--allowed-url <url>`: add a destination expected to succeed through the proxy. Repeat to check multiple destinations.
|
||||
- `--denied-url <url>`: add a destination expected to be blocked by the proxy. Repeat to check multiple destinations.
|
||||
- `--apns-reachable`: also verify sandbox APNs HTTP/2 is reachable through the proxy.
|
||||
- `--apns-authority <url>`: APNs authority to probe with `--apns-reachable` (`https://api.sandbox.push.apple.com` by default; production is `https://api.push.apple.com`).
|
||||
- `--timeout-ms <ms>`: per-request timeout in milliseconds.
|
||||
|
||||
See [Network Proxy](/security/network-proxy) for deployment guidance and denial
|
||||
@@ -68,6 +73,7 @@ semantics.
|
||||
|
||||
- `start` defaults to `127.0.0.1` unless `--host` is set.
|
||||
- `run` starts a local debug proxy and then runs the command after `--`.
|
||||
- The debug proxy's direct upstream forwarding opens upstream sockets for diagnostics. When OpenClaw managed proxy mode is active, direct forwarding for proxy requests and CONNECT tunnels is disabled by default; set `OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY=1` only for approved local diagnostics.
|
||||
- `validate` exits with code 1 when proxy config or destination checks fail.
|
||||
- Captures are local debugging data; use `openclaw proxy purge` when finished.
|
||||
|
||||
|
||||
@@ -16,11 +16,19 @@ until a message is processed. Use `openclaw channels status --probe`,
|
||||
`openclaw status --deep`, or `openclaw health --verbose` when you need live
|
||||
channel connectivity.
|
||||
|
||||
`openclaw sessions` and Gateway `sessions.list` responses are bounded by
|
||||
default so large long-lived stores cannot monopolize the CLI process or Gateway
|
||||
event loop. The CLI returns the newest 100 sessions by default; pass
|
||||
`--limit <n>` for a smaller/larger window or `--limit all` when you intentionally
|
||||
need the full store. JSON responses include `totalCount`, `limitApplied`, and
|
||||
`hasMore` when callers need to show that more rows exist.
|
||||
|
||||
```bash
|
||||
openclaw sessions
|
||||
openclaw sessions --agent work
|
||||
openclaw sessions --all-agents
|
||||
openclaw sessions --active 120
|
||||
openclaw sessions --limit 25
|
||||
openclaw sessions --verbose
|
||||
openclaw sessions --json
|
||||
```
|
||||
@@ -32,6 +40,7 @@ Scope selection:
|
||||
- `--agent <id>`: one configured agent store
|
||||
- `--all-agents`: aggregate all configured agent stores
|
||||
- `--store <path>`: explicit store path (cannot be combined with `--agent` or `--all-agents`)
|
||||
- `--limit <n|all>`: max rows to output (default `100`; `all` restores full output)
|
||||
|
||||
Export a trajectory bundle for a stored session:
|
||||
|
||||
@@ -63,6 +72,9 @@ JSON examples:
|
||||
],
|
||||
"allAgents": true,
|
||||
"count": 2,
|
||||
"totalCount": 2,
|
||||
"limitApplied": 100,
|
||||
"hasMore": false,
|
||||
"activeMinutes": null,
|
||||
"sessions": [
|
||||
{ "agentId": "main", "key": "agent:main:main", "model": "gpt-5" },
|
||||
|
||||
@@ -168,8 +168,9 @@ manually.
|
||||
|
||||
On the beta update channel, tracked npm and ClawHub plugin installs that follow
|
||||
the default/latest line try a plugin `@beta` release first. If the plugin has no
|
||||
beta release, OpenClaw falls back to the recorded default/latest spec. Exact
|
||||
versions and explicit tags are not rewritten.
|
||||
beta release, OpenClaw falls back to the recorded default/latest spec. For npm
|
||||
plugins, OpenClaw also falls back when the beta package exists but fails install
|
||||
validation. Exact versions and explicit tags are not rewritten.
|
||||
|
||||
<Warning>
|
||||
If an exact pinned npm plugin update resolves to an artifact whose integrity differs from the stored install record, `openclaw update` aborts that plugin artifact update instead of installing it. Reinstall or update the plugin explicitly only after verifying that you trust the new artifact.
|
||||
|
||||
@@ -33,13 +33,13 @@ Inside `agents.defaults.workspace`, OpenClaw expects these user-editable files:
|
||||
- `IDENTITY.md` — agent name/vibe/emoji
|
||||
- `USER.md` — user profile + preferred address
|
||||
|
||||
On the first turn of a new session, OpenClaw injects the contents of these files directly into the agent context.
|
||||
On the first turn of a new session, OpenClaw injects the contents of these files into the system prompt's Project Context.
|
||||
|
||||
Blank files are skipped. Large files are trimmed and truncated with a marker so prompts stay lean (read the file for full content).
|
||||
|
||||
If a file is missing, OpenClaw injects a single “missing file” marker line (and `openclaw setup` will create a safe default template).
|
||||
|
||||
`BOOTSTRAP.md` is only created for a **brand new workspace** (no other bootstrap files present). If you delete it after completing the ritual, it should not be recreated on later restarts.
|
||||
`BOOTSTRAP.md` is only created for a **brand new workspace** (no other bootstrap files present). While it is pending, OpenClaw keeps it in Project Context and adds system-prompt bootstrap guidance for the initial ritual instead of copying it into the user message. If you delete it after completing the ritual, it should not be recreated on later restarts.
|
||||
|
||||
To disable bootstrap file creation entirely (for pre-seeded workspaces), set:
|
||||
|
||||
|
||||
@@ -89,6 +89,73 @@ directory, installs dependencies, builds each ref, runs the scenario with
|
||||
and `mantis-report.md`. For the first Discord scenario, a successful verification
|
||||
means baseline status is `fail` and candidate status is `pass`.
|
||||
|
||||
The first VM/browser primitive is the desktop smoke:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa mantis desktop-browser-smoke \
|
||||
--output-dir .artifacts/qa-e2e/mantis/desktop-browser
|
||||
```
|
||||
|
||||
It leases or reuses a Crabbox desktop machine, starts a visible browser inside the
|
||||
VNC session, captures the desktop, pulls artifacts back to the local output
|
||||
directory, and writes the reconnect command into the report. The command defaults
|
||||
to the Hetzner provider because it is the first provider with working desktop/VNC
|
||||
coverage in the Mantis lane. Override it with `--provider`, `--crabbox-bin`, or
|
||||
`OPENCLAW_MANTIS_CRABBOX_PROVIDER` when running against another Crabbox fleet.
|
||||
|
||||
Useful desktop smoke flags:
|
||||
|
||||
- `--lease-id <cbx_...>` or `OPENCLAW_MANTIS_CRABBOX_LEASE_ID` reuses a warmed desktop.
|
||||
- `--browser-url <url>` changes the page opened in the visible browser.
|
||||
- `--html-file <path>` renders a repo-local HTML artifact in the visible browser. Mantis uses this to capture the generated Discord status-reaction timeline through a real Crabbox desktop.
|
||||
- `--keep-lease` or `OPENCLAW_MANTIS_KEEP_VM=1` keeps a newly created passing lease open for VNC inspection. Failed runs keep the lease by default when one was created so an operator can reconnect.
|
||||
- `--class`, `--idle-timeout`, and `--ttl` tune machine size and lease lifetime.
|
||||
|
||||
The first full desktop transport primitive is the Slack desktop smoke:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa mantis slack-desktop-smoke \
|
||||
--output-dir .artifacts/qa-e2e/mantis/slack-desktop \
|
||||
--gateway-setup \
|
||||
--scenario slack-canary \
|
||||
--keep-lease
|
||||
```
|
||||
|
||||
It leases or reuses a Crabbox desktop machine, syncs the current checkout into
|
||||
the VM, runs `pnpm openclaw qa slack` inside that VM, opens Slack Web in the VNC
|
||||
browser, captures the visible desktop, and copies both the Slack QA artifacts and
|
||||
the VNC screenshot back to the local output directory. This is the first Mantis
|
||||
shape where the SUT OpenClaw gateway and the browser both live inside the same
|
||||
Linux desktop VM.
|
||||
|
||||
With `--gateway-setup`, the command prepares a persistent disposable OpenClaw
|
||||
home at `$HOME/.openclaw-mantis/slack-openclaw`, patches Slack Socket Mode
|
||||
configuration for the selected channel, starts `openclaw gateway run` on port
|
||||
`38973`, and keeps Chrome running in the VNC session. This is the "leave me a
|
||||
Linux desktop with Slack and a claw running" mode; the bot-to-bot Slack QA lane
|
||||
remains the default when `--gateway-setup` is omitted.
|
||||
|
||||
Required inputs for `--credential-source env`:
|
||||
|
||||
- `OPENCLAW_QA_SLACK_CHANNEL_ID`
|
||||
- `OPENCLAW_QA_SLACK_DRIVER_BOT_TOKEN`
|
||||
- `OPENCLAW_QA_SLACK_SUT_BOT_TOKEN`
|
||||
- `OPENCLAW_QA_SLACK_SUT_APP_TOKEN`
|
||||
- `OPENCLAW_LIVE_OPENAI_KEY` for the remote model lane. If only
|
||||
`OPENAI_API_KEY` is set locally, Mantis maps it to `OPENCLAW_LIVE_OPENAI_KEY`
|
||||
before invoking Crabbox so Crabbox's `OPENCLAW_*` env forwarding can carry it
|
||||
into the VM.
|
||||
|
||||
Useful Slack desktop flags:
|
||||
|
||||
- `--lease-id <cbx_...>` reruns against a machine where an operator already logged in to Slack Web through VNC.
|
||||
- `--gateway-setup` starts a persistent OpenClaw Slack gateway in the VM instead of only running the bot-to-bot QA lane.
|
||||
- `--slack-url <url>` opens a specific Slack Web URL. Without it, Mantis derives `https://app.slack.com/client/<team>/<channel>` from Slack `auth.test` when the SUT bot token is available.
|
||||
- `--slack-channel-id <id>` controls the Slack channel allowlist used by gateway setup.
|
||||
- `OPENCLAW_MANTIS_SLACK_BROWSER_PROFILE_DIR` controls the persistent Chrome profile inside the VM. The default is `$HOME/.config/openclaw-mantis/slack-chrome-profile`, so a manual Slack Web login survives reruns on the same lease.
|
||||
- `--credential-source convex --credential-role ci` uses the shared credential pool instead of direct Slack env tokens.
|
||||
- `--provider-mode`, `--model`, `--alt-model`, and `--fast` pass through to the Slack live lane.
|
||||
|
||||
The GitHub smoke workflow is `Mantis Discord Smoke`. The before and after GitHub
|
||||
workflow for the first real scenario is `Mantis Discord Status Reactions`. It
|
||||
accepts:
|
||||
@@ -99,7 +166,11 @@ accepts:
|
||||
It checks out the workflow harness ref, builds separate baseline and candidate
|
||||
worktrees, runs `discord-status-reactions-tool-only` against each worktree, and
|
||||
uploads `baseline/`, `candidate/`, `comparison.json`, and `mantis-report.md` as
|
||||
Actions artifacts.
|
||||
Actions artifacts. It also renders each lane's timeline HTML in a Crabbox
|
||||
desktop browser and publishes those VNC screenshots beside the deterministic
|
||||
timeline PNGs in the PR comment. The workflow builds the Crabbox CLI from
|
||||
`openclaw/crabbox` main so it can use the current desktop/browser lease flags
|
||||
before the next Crabbox binary release is cut.
|
||||
|
||||
You can also trigger the status-reactions run directly from a PR comment:
|
||||
|
||||
@@ -132,18 +203,19 @@ ClawSweeper review findings.
|
||||
|
||||
1. Acquire credentials.
|
||||
2. Allocate or reuse a VM.
|
||||
3. Prepare a clean checkout for the baseline ref.
|
||||
4. Install dependencies and build only what the scenario needs.
|
||||
5. Start a child OpenClaw Gateway with an isolated state directory.
|
||||
6. Configure the live transport, provider, model, and browser profile.
|
||||
7. Run the scenario and capture baseline evidence.
|
||||
8. Stop the gateway and preserve logs.
|
||||
9. Prepare the candidate ref in the same VM.
|
||||
10. Run the same scenario and capture candidate evidence.
|
||||
11. Compare the oracle results and visual evidence.
|
||||
12. Write Markdown, JSON, logs, screenshots, and optional trace artifacts.
|
||||
13. Upload GitHub Actions artifacts.
|
||||
14. Post a concise PR or Discord status message.
|
||||
3. Prepare the desktop/browser profile when the scenario needs UI evidence.
|
||||
4. Prepare a clean checkout for the baseline ref.
|
||||
5. Install dependencies and build only what the scenario needs.
|
||||
6. Start a child OpenClaw Gateway with an isolated state directory.
|
||||
7. Configure the live transport, provider, model, and browser profile.
|
||||
8. Run the scenario and capture baseline evidence.
|
||||
9. Stop the gateway and preserve logs.
|
||||
10. Prepare the candidate ref in the same VM.
|
||||
11. Run the same scenario and capture candidate evidence.
|
||||
12. Compare the oracle results and visual evidence.
|
||||
13. Write Markdown, JSON, logs, screenshots, and optional trace artifacts.
|
||||
14. Upload GitHub Actions artifacts.
|
||||
15. Post a concise PR or Discord status message.
|
||||
|
||||
The scenario should be able to fail in two different ways:
|
||||
|
||||
@@ -345,9 +417,15 @@ Recommended secret names:
|
||||
- `OPENCLAW_QA_REDACT_PUBLIC_METADATA=1` for public GitHub artifact uploads
|
||||
- `OPENCLAW_QA_CONVEX_SITE_URL`
|
||||
- `OPENCLAW_QA_CONVEX_SECRET_CI`
|
||||
- `OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR`
|
||||
- `OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN`
|
||||
|
||||
Long term, the Convex credential pool should remain the normal source for live
|
||||
transport credentials. GitHub secrets bootstrap the broker and fallback lanes.
|
||||
The Discord status-reactions workflow maps the Mantis Crabbox secrets back to
|
||||
the `CRABBOX_COORDINATOR` and `CRABBOX_COORDINATOR_TOKEN` environment variables
|
||||
that the Crabbox CLI expects. The plain `CRABBOX_*` GitHub secret names remain
|
||||
accepted as a compatibility fallback.
|
||||
|
||||
The Mantis runner must never print:
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ OpenClaw can expose or hide model reasoning:
|
||||
|
||||
- `/reasoning on|off|stream` controls visibility.
|
||||
- Reasoning content still counts toward token usage when produced by the model.
|
||||
- Telegram supports reasoning stream into the draft bubble.
|
||||
- Telegram supports reasoning stream into a transient draft bubble that is deleted after final delivery; use `/reasoning on` for persistent reasoning output.
|
||||
|
||||
Details: [Thinking + reasoning directives](/tools/thinking) and [Token use](/reference/token-use).
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ into the final answer when the channel can do that safely.
|
||||
|
||||
```text
|
||||
Shelling...
|
||||
- reading recent channel context
|
||||
- checking matching issues
|
||||
- preparing reply
|
||||
📖 Read: from docs/concepts/progress-drafts.md
|
||||
🔎 Web Search: for "discord edit message"
|
||||
🛠️ Exec: run tests
|
||||
```
|
||||
|
||||
Use progress drafts when you want one tidy status message during tool-heavy work
|
||||
@@ -60,6 +60,9 @@ The label appears after the agent starts meaningful work and either remains busy
|
||||
for five seconds or emits a second work event. Plain text-only replies do not
|
||||
show a progress draft. Progress lines are added only when the agent emits useful
|
||||
work updates, for example `🛠️ Exec`, `🔎 Web Search`, or `✍️ Write: to /tmp/file`.
|
||||
By default they use the same compact explain mode as `/verbose`; set
|
||||
`agents.defaults.toolProgressDetail: "raw"` when debugging and you also want raw
|
||||
commands/details appended.
|
||||
The final answer replaces the draft when possible; otherwise
|
||||
OpenClaw sends the final answer normally and cleans up or stops updating the
|
||||
draft according to the channel's transport.
|
||||
@@ -173,6 +176,30 @@ Progress lines are enabled by default in progress mode. They come from real run
|
||||
events: tool starts, item updates, task plans, approvals, command output, patch
|
||||
summaries, and similar agent activity.
|
||||
|
||||
OpenClaw uses the same formatter for progress drafts and `/verbose`:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
toolProgressDetail: "explain", // explain | raw
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`"explain"` is the default and keeps drafts stable with concise labels like
|
||||
`🛠️ Exec: check JS syntax for /tmp/app.js`. `"raw"` appends the underlying
|
||||
command/detail when available, which is useful while debugging but noisier in
|
||||
chat.
|
||||
|
||||
For example, the same command appears differently depending on the detail mode:
|
||||
|
||||
| Mode | Progress line |
|
||||
| --------- | -------------------------------------------------------------------- |
|
||||
| `explain` | `🛠️ Exec: check JS syntax for /tmp/app.js` |
|
||||
| `raw` | `🛠️ Exec: check JS syntax for /tmp/app.js, node --check /tmp/app.js` |
|
||||
|
||||
Limit how many lines stay visible:
|
||||
|
||||
```json5
|
||||
@@ -190,6 +217,33 @@ Limit how many lines stay visible:
|
||||
}
|
||||
```
|
||||
|
||||
Progress lines are compacted automatically to reduce chat-bubble reflow while the draft is edited.
|
||||
|
||||
OpenClaw truncates long progress lines by default so repeated draft edits do not
|
||||
wrap differently on every update. The prefix stays readable, and long details
|
||||
such as paths or raw commands are shortened with an ellipsis.
|
||||
|
||||
Slack can render progress lines as structured Block Kit fields instead of a
|
||||
single text body:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
streaming: {
|
||||
mode: "progress",
|
||||
progress: {
|
||||
render: "rich",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Rich rendering keeps the same plain-text fallback so channels and clients that
|
||||
do not support the richer shape can still show the compact progress text.
|
||||
|
||||
Keep the single progress draft but hide tool and task lines:
|
||||
|
||||
```json5
|
||||
|
||||
@@ -29,26 +29,26 @@ Current pieces:
|
||||
Every QA flow runs under `pnpm openclaw qa <subcommand>`. Many have `pnpm qa:*`
|
||||
script aliases; both forms are supported.
|
||||
|
||||
| Command | Purpose |
|
||||
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `qa run` | Bundled QA self-check; writes a Markdown report. |
|
||||
| `qa suite` | Run repo-backed scenarios against the QA gateway lane. Aliases: `pnpm openclaw qa suite --runner multipass` for a disposable Linux VM. |
|
||||
| `qa coverage` | Print the markdown scenario-coverage inventory (`--json` for machine output). |
|
||||
| `qa parity-report` | Compare two `qa-suite-summary.json` files and write the agentic parity report. |
|
||||
| `qa character-eval` | Run the character QA scenario across multiple live models with a judged report. See [Reporting](#reporting). |
|
||||
| `qa manual` | Run a one-off prompt against the selected provider/model lane. |
|
||||
| `qa ui` | Start the QA debugger UI and local QA bus (alias: `pnpm qa:lab:ui`). |
|
||||
| `qa docker-build-image` | Build the prebaked QA Docker image. |
|
||||
| `qa docker-scaffold` | Write a docker-compose scaffold for the QA dashboard + gateway lane. |
|
||||
| `qa up` | Build the QA site, start the Docker-backed stack, print the URL (alias: `pnpm qa:lab:up`; `:fast` variant adds `--use-prebuilt-image --bind-ui-dist --skip-ui-build`). |
|
||||
| `qa aimock` | Start only the AIMock provider server. |
|
||||
| `qa mock-openai` | Start only the scenario-aware `mock-openai` provider server. |
|
||||
| `qa credentials doctor` / `add` / `list` / `remove` | Manage the shared Convex credential pool. |
|
||||
| `qa matrix` | Live transport lane against a disposable Tuwunel homeserver. See [Matrix QA](/concepts/qa-matrix). |
|
||||
| `qa telegram` | Live transport lane against a real private Telegram group. |
|
||||
| `qa discord` | Live transport lane against a real private Discord guild channel. |
|
||||
| `qa slack` | Live transport lane against a real private Slack channel. |
|
||||
| `qa mantis` | Before and after verification runner for live transport bugs, with the first Discord status-reactions scenario. See [Mantis](/concepts/mantis). |
|
||||
| Command | Purpose |
|
||||
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `qa run` | Bundled QA self-check; writes a Markdown report. |
|
||||
| `qa suite` | Run repo-backed scenarios against the QA gateway lane. Aliases: `pnpm openclaw qa suite --runner multipass` for a disposable Linux VM. |
|
||||
| `qa coverage` | Print the markdown scenario-coverage inventory (`--json` for machine output). |
|
||||
| `qa parity-report` | Compare two `qa-suite-summary.json` files and write the agentic parity report. |
|
||||
| `qa character-eval` | Run the character QA scenario across multiple live models with a judged report. See [Reporting](#reporting). |
|
||||
| `qa manual` | Run a one-off prompt against the selected provider/model lane. |
|
||||
| `qa ui` | Start the QA debugger UI and local QA bus (alias: `pnpm qa:lab:ui`). |
|
||||
| `qa docker-build-image` | Build the prebaked QA Docker image. |
|
||||
| `qa docker-scaffold` | Write a docker-compose scaffold for the QA dashboard + gateway lane. |
|
||||
| `qa up` | Build the QA site, start the Docker-backed stack, print the URL (alias: `pnpm qa:lab:up`; `:fast` variant adds `--use-prebuilt-image --bind-ui-dist --skip-ui-build`). |
|
||||
| `qa aimock` | Start only the AIMock provider server. |
|
||||
| `qa mock-openai` | Start only the scenario-aware `mock-openai` provider server. |
|
||||
| `qa credentials doctor` / `add` / `list` / `remove` | Manage the shared Convex credential pool. |
|
||||
| `qa matrix` | Live transport lane against a disposable Tuwunel homeserver. See [Matrix QA](/concepts/qa-matrix). |
|
||||
| `qa telegram` | Live transport lane against a real private Telegram group. |
|
||||
| `qa discord` | Live transport lane against a real private Discord guild channel. |
|
||||
| `qa slack` | Live transport lane against a real private Slack channel. |
|
||||
| `qa mantis` | Before and after verification runner for live transport bugs, with Discord status-reactions evidence, Crabbox desktop/browser smoke, and Slack-in-VNC smoke. See [Mantis](/concepts/mantis). |
|
||||
|
||||
## Operator flow
|
||||
|
||||
@@ -121,6 +121,23 @@ pnpm openclaw qa slack
|
||||
|
||||
They target a pre-existing real channel with two bots (driver + SUT). Required env vars, scenario lists, output artifacts, and the Convex credential pool are documented in [Telegram, Discord, and Slack QA reference](#telegram-discord-and-slack-qa-reference) below.
|
||||
|
||||
For a full Slack desktop VM run with VNC rescue, run:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa mantis slack-desktop-smoke \
|
||||
--gateway-setup \
|
||||
--scenario slack-canary \
|
||||
--keep-lease
|
||||
```
|
||||
|
||||
That command leases a Crabbox desktop/browser machine, runs the Slack live lane
|
||||
inside the VM, opens Slack Web in the VNC browser, captures the desktop, and
|
||||
copies `slack-qa/` plus `slack-desktop-smoke.png` back to the Mantis artifact
|
||||
directory. Reuse `--lease-id <cbx_...>` after logging in to Slack Web manually
|
||||
through VNC. With `--gateway-setup`, Mantis leaves a persistent OpenClaw Slack
|
||||
gateway running inside the VM on port `38973`; without it, the command runs the
|
||||
normal bot-to-bot Slack QA lane and exits after artifact capture.
|
||||
|
||||
Before using pooled live credentials, run:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -162,7 +162,7 @@ Telegram:
|
||||
- Uses `sendMessage` + `editMessageText` preview updates across DMs and group/topics.
|
||||
- Sends a fresh final message instead of editing in place when a preview has been visible for about one minute, then cleans up the preview so Telegram's timestamp reflects reply completion.
|
||||
- Preview streaming is skipped when Telegram block streaming is explicitly enabled (to avoid double-streaming).
|
||||
- `/reasoning stream` can write reasoning to preview.
|
||||
- `/reasoning stream` can write reasoning to a transient preview that is deleted after final delivery.
|
||||
|
||||
Discord:
|
||||
|
||||
@@ -201,10 +201,10 @@ Supported surfaces:
|
||||
- Telegram has shipped with tool-progress preview updates enabled since `v2026.4.22`; keeping them enabled preserves that released behavior.
|
||||
- **Mattermost** already folds tool activity into its single draft preview post (see above).
|
||||
- Tool-progress edits follow the active preview streaming mode; they are skipped when preview streaming is `off` or when block streaming has taken over the message. On Telegram, `streaming.mode: "off"` is final-only: generic progress chatter is also suppressed instead of being delivered as standalone status messages, while approval prompts, media payloads, and errors still route normally.
|
||||
- To keep preview streaming but hide tool-progress lines, set `streaming.preview.toolProgress` to `false` for that channel. To disable preview edits entirely, set `streaming.mode` to `off`.
|
||||
- To keep preview streaming but hide tool-progress lines, set `streaming.preview.toolProgress` to `false` for that channel. To keep tool-progress lines visible while hiding command/exec text, set `streaming.preview.commandText` to `"status"` or `streaming.progress.commandText` to `"status"`; the default is `"raw"` to preserve released behavior. This policy is shared by draft/progress channels that use OpenClaw's compact progress renderer, including Discord, Matrix, Microsoft Teams, Mattermost, Slack draft previews, and Telegram. To disable preview edits entirely, set `streaming.mode` to `off`.
|
||||
- Telegram selected quote replies are an exception: when `replyToMode` is not `"off"` and selected quote text is present, OpenClaw skips the answer preview stream for that turn so tool-progress preview lines cannot render. Current-message replies without selected quote text still keep preview streaming. See [Telegram channel docs](/channels/telegram) for details.
|
||||
|
||||
Example:
|
||||
Keep progress lines visible but hide raw command/exec text:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -213,7 +213,26 @@ Example:
|
||||
"streaming": {
|
||||
"mode": "partial",
|
||||
"preview": {
|
||||
"toolProgress": false
|
||||
"toolProgress": true,
|
||||
"commandText": "status"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use the same shape under another compact progress channel key, for example `channels.discord`, `channels.matrix`, `channels.msteams`, `channels.mattermost`, or Slack draft previews. For progress-draft mode, put the same policy under `streaming.progress`:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"streaming": {
|
||||
"mode": "progress",
|
||||
"progress": {
|
||||
"toolProgress": true,
|
||||
"commandText": "status"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,9 +176,10 @@ Large files are truncated with a marker. The max per-file size is controlled by
|
||||
`agents.defaults.bootstrapMaxChars` (default: 12000). Total injected bootstrap
|
||||
content across files is capped by `agents.defaults.bootstrapTotalMaxChars`
|
||||
(default: 60000). Missing files inject a short missing-file marker. When truncation
|
||||
occurs, OpenClaw can inject a warning block in Project Context; control this with
|
||||
occurs, OpenClaw can inject a concise system-prompt warning notice; control this with
|
||||
`agents.defaults.bootstrapPromptTruncationWarning` (`off`, `once`, `always`;
|
||||
default: `once`).
|
||||
default: `once`). Detailed raw/injected counts stay in diagnostics such as
|
||||
`/context`, `/status`, doctor, and logs.
|
||||
|
||||
Sub-agent sessions only inject `AGENTS.md` and `TOOLS.md` (other bootstrap files
|
||||
are filtered out to keep the sub-agent context small).
|
||||
|
||||
@@ -178,6 +178,12 @@ that agent. To force a different Claude mode, set explicit raw backend args
|
||||
such as `--permission-mode default` or `--permission-mode acceptEdits` under
|
||||
`agents.defaults.cliBackends.claude-cli.args` and matching `resumeArgs`.
|
||||
|
||||
The bundled Anthropic `claude-cli` backend also maps OpenClaw `/think` levels
|
||||
to Claude Code's native `--effort` flag for non-off levels. `minimal` and
|
||||
`low` map to `low`, `adaptive` and `medium` map to `medium`, and `high`,
|
||||
`xhigh`, and `max` map directly. Other CLI backends need their owning plugin to
|
||||
declare an equivalent argv mapper before `/think` can affect the spawned CLI.
|
||||
|
||||
Before OpenClaw can use the bundled `claude-cli` backend, Claude Code itself
|
||||
must already be logged in on the same host:
|
||||
|
||||
|
||||
@@ -116,12 +116,16 @@ Max total characters injected across all workspace bootstrap files. Default: `60
|
||||
|
||||
### `agents.defaults.bootstrapPromptTruncationWarning`
|
||||
|
||||
Controls agent-visible warning text when bootstrap context is truncated.
|
||||
Controls the agent-visible system-prompt notice when bootstrap context is truncated.
|
||||
Default: `"once"`.
|
||||
|
||||
- `"off"`: never inject warning text into the system prompt.
|
||||
- `"once"`: inject warning once per unique truncation signature (recommended).
|
||||
- `"always"`: inject warning on every run when truncation exists.
|
||||
- `"off"`: never inject truncation notice text into the system prompt.
|
||||
- `"once"`: inject a concise notice once per unique truncation signature (recommended).
|
||||
- `"always"`: inject a concise notice on every run when truncation exists.
|
||||
|
||||
Detailed raw/injected counts and config tuning fields stay in diagnostics such
|
||||
as context/status reports and logs; routine WebChat user/runtime context only
|
||||
gets the concise recovery notice.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -339,6 +343,7 @@ Time format in system prompt. Default: `auto` (OS preference).
|
||||
pdfMaxPages: 20,
|
||||
thinkingDefault: "low",
|
||||
verboseDefault: "off",
|
||||
toolProgressDetail: "explain",
|
||||
reasoningDefault: "off",
|
||||
elevatedDefault: "on",
|
||||
timeoutSeconds: 600,
|
||||
@@ -379,6 +384,7 @@ Time format in system prompt. Default: `auto` (OS preference).
|
||||
- `pdfMaxBytesMb`: default PDF size limit for the `pdf` tool when `maxBytesMb` is not passed at call time.
|
||||
- `pdfMaxPages`: default maximum pages considered by extraction fallback mode in the `pdf` tool.
|
||||
- `verboseDefault`: default verbose level for agents. Values: `"off"`, `"on"`, `"full"`. Default: `"off"`.
|
||||
- `toolProgressDetail`: detail mode for `/verbose` tool summaries and progress-draft tool lines. Values: `"explain"` (default, compact human labels) or `"raw"` (append raw command/detail when available). Per-agent `agents.list[].toolProgressDetail` overrides this default.
|
||||
- `reasoningDefault`: default reasoning visibility for agents. Values: `"off"`, `"on"`, `"stream"`. Per-agent `agents.list[].reasoningDefault` overrides this default. Configured reasoning defaults are only applied for owners, authorized senders, or operator-admin gateway contexts when no per-message or session reasoning override is set.
|
||||
- `elevatedDefault`: default elevated-output level for agents. Values: `"off"`, `"on"`, `"ask"`, `"full"`. Default: `"on"`.
|
||||
- `model.primary`: format `provider/model` (e.g. `openai/gpt-5.5` for API-key access or `openai-codex/gpt-5.5` for Codex OAuth). If you omit the provider, OpenClaw tries an alias first, then a unique configured-provider match for that exact model id, and only then falls back to the configured default provider (deprecated compatibility behavior, so prefer explicit `provider/model`). If that provider no longer exposes the configured default model, OpenClaw falls back to the first configured provider/model instead of surfacing a stale removed-provider default.
|
||||
|
||||
@@ -777,6 +777,13 @@ Group messages default to **require mention** (metadata mention or safe regex pa
|
||||
|
||||
Visible replies are controlled separately. Group/channel rooms default to `messages.groupChat.visibleReplies: "message_tool"`: OpenClaw still processes the turn, but normal final replies stay private and visible room output requires `message(action=send)`. Set `"automatic"` only when you want the legacy behavior where normal replies are posted back to the room. To apply the same tool-only visible-reply behavior to direct chats too, set `messages.visibleReplies: "message_tool"`; the Codex harness also uses that tool-only behavior as its unset direct-chat default.
|
||||
|
||||
Tool-only visible replies require a model/runtime that reliably calls tools. If
|
||||
the session log shows assistant text with `didSendViaMessagingTool: false`, the
|
||||
model produced a private final answer instead of calling the message tool.
|
||||
Switch to a stronger tool-calling model for that channel, or set
|
||||
`messages.groupChat.visibleReplies: "automatic"` to restore legacy visible final
|
||||
replies.
|
||||
|
||||
If the message tool is unavailable under the active tool policy, OpenClaw falls back to automatic visible replies instead of silently suppressing the response. `openclaw doctor` warns about this mismatch.
|
||||
|
||||
The gateway hot-reloads `messages` config after the file is saved. Restart only when file watching or config reload is disabled in the deployment.
|
||||
|
||||
@@ -249,6 +249,7 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
|
||||
skills: ["github", "weather"], // inherited by agents that omit list[].skills
|
||||
thinkingDefault: "low",
|
||||
verboseDefault: "off",
|
||||
toolProgressDetail: "explain",
|
||||
reasoningDefault: "off",
|
||||
elevatedDefault: "on",
|
||||
blockStreamingDefault: "off",
|
||||
|
||||
@@ -117,12 +117,19 @@ diagnostics are enabled. It is for operational facts, not content.
|
||||
The same diagnostic heartbeat records liveness samples when the Gateway keeps
|
||||
running but the Node.js event loop or CPU looks saturated. These
|
||||
`diagnostic.liveness.warning` events include event-loop delay, event-loop
|
||||
utilization, CPU-core ratio, and active/waiting/queued session counts. Idle
|
||||
samples stay in telemetry at `info` level. Liveness samples become Gateway
|
||||
warnings only when work is waiting or queued, or when active work overlaps with
|
||||
sustained event-loop delay. Transient max-delay spikes during otherwise healthy
|
||||
background work stay in debug logs. They do not restart the Gateway by
|
||||
themselves.
|
||||
utilization, CPU-core ratio, active/waiting/queued session counts, the current
|
||||
startup/runtime phase when known, recent phase spans, and bounded active/queued
|
||||
work labels. Idle samples stay in telemetry at `info` level. Liveness samples
|
||||
become Gateway warnings only when work is waiting or queued, or when active work
|
||||
overlaps with sustained event-loop delay. Transient max-delay spikes during
|
||||
otherwise healthy background work stay in debug logs. They do not restart the
|
||||
Gateway by themselves.
|
||||
|
||||
Startup phases also emit `diagnostic.phase.completed` events with wall-clock and
|
||||
CPU timing. Stalled embedded-run diagnostics mark `terminalProgressStale=true`
|
||||
when the last bridge progress looked terminal, such as a raw response item or
|
||||
response completion event, but the Gateway still considers the embedded run
|
||||
active.
|
||||
|
||||
Inspect the live recorder:
|
||||
|
||||
|
||||
@@ -189,6 +189,7 @@ That stages grounded durable candidates into the short-term dreaming store while
|
||||
- `routing.groupChat.requireMention` → `channels.whatsapp/telegram/imessage.groups."*".requireMention`
|
||||
- `routing.groupChat.historyLimit` → `messages.groupChat.historyLimit`
|
||||
- `routing.groupChat.mentionPatterns` → `messages.groupChat.mentionPatterns`
|
||||
- `channels.telegram.requireMention` → `channels.telegram.groups."*".requireMention`
|
||||
- configured-channel configs missing visible reply policy → `messages.groupChat.visibleReplies: "message_tool"`
|
||||
- `routing.queue` → `messages.queue`
|
||||
- `routing.bindings` → top-level `bindings`
|
||||
@@ -344,7 +345,7 @@ That stages grounded durable candidates into the short-term dreaming store while
|
||||
When sandboxing is enabled, doctor checks Docker images and offers to build or switch to legacy names if the current image is missing.
|
||||
</Accordion>
|
||||
<Accordion title="7b. Plugin install cleanup">
|
||||
Doctor removes legacy OpenClaw-generated plugin dependency staging state in `openclaw doctor --fix` / `openclaw doctor --repair` mode. This covers stale generated dependency roots, old install-stage directories, and package-local debris from earlier bundled-plugin dependency repair code.
|
||||
Doctor removes legacy OpenClaw-generated plugin dependency staging state in `openclaw doctor --fix` / `openclaw doctor --repair` mode. This covers stale generated dependency roots, old install-stage directories, package-local debris from earlier bundled-plugin dependency repair code, and orphaned or recovered managed npm copies of bundled `@openclaw/*` plugins that can shadow the current bundled manifest.
|
||||
|
||||
Doctor can also reinstall configured downloadable plugins when the config references them but the local plugin registry cannot find them. For the 2026.5.2 bundled-plugin externalization, doctor automatically installs downloadable plugins that the existing config already uses and then relies on `meta.lastTouchedVersion` to run that release pass only once. Gateway startup and config reload do not run package managers; plugin installs remain explicit doctor/install/update work.
|
||||
|
||||
|
||||
@@ -268,11 +268,11 @@ heartbeat tick. For the config knob and defaults, see
|
||||
- `openclaw.exec`
|
||||
- `openclaw.exec.target`, `openclaw.exec.mode`, `openclaw.outcome`, `openclaw.failureKind`, `openclaw.exec.command_length`, `openclaw.exec.exit_code`, `openclaw.exec.timed_out`
|
||||
- `openclaw.webhook.processed`
|
||||
- `openclaw.channel`, `openclaw.webhook`, `openclaw.chatId`
|
||||
- `openclaw.channel`, `openclaw.webhook`
|
||||
- `openclaw.webhook.error`
|
||||
- `openclaw.channel`, `openclaw.webhook`, `openclaw.chatId`, `openclaw.error`
|
||||
- `openclaw.channel`, `openclaw.webhook`, `openclaw.error`
|
||||
- `openclaw.message.processed`
|
||||
- `openclaw.channel`, `openclaw.outcome`, `openclaw.chatId`, `openclaw.messageId`, `openclaw.reason`
|
||||
- `openclaw.channel`, `openclaw.outcome`, `openclaw.reason`
|
||||
- `openclaw.message.delivery`
|
||||
- `openclaw.channel`, `openclaw.delivery.kind`, `openclaw.outcome`, `openclaw.errorCategory`, `openclaw.delivery.result_count`
|
||||
- `openclaw.session.stuck`
|
||||
|
||||
@@ -89,6 +89,17 @@ OPENCLAW_RUN_NODE_CPU_PROF_DIR=.artifacts/cli-cpu pnpm openclaw status
|
||||
The source runner adds Node CPU profile flags and writes a `.cpuprofile` for the
|
||||
command. Use this before adding temporary instrumentation to command code.
|
||||
|
||||
For startup stalls that look like synchronous filesystem or module-loader work,
|
||||
add Node's sync I/O trace flag through the source runner:
|
||||
|
||||
```bash
|
||||
OPENCLAW_TRACE_SYNC_IO=1 pnpm openclaw gateway --force
|
||||
```
|
||||
|
||||
`pnpm gateway:watch` enables this flag by default for the watched Gateway child.
|
||||
Set `OPENCLAW_TRACE_SYNC_IO=0` to suppress Node sync I/O trace output in watch
|
||||
mode.
|
||||
|
||||
## Gateway watch mode
|
||||
|
||||
For fast iteration, run the gateway under the file watcher:
|
||||
|
||||
@@ -203,6 +203,15 @@ Notes:
|
||||
- The live CLI-backend smoke now exercises the same end-to-end flow for Claude, Codex, and Gemini: text turn, image classification turn, then MCP `cron` tool call verified through the gateway CLI.
|
||||
- Claude's default smoke also patches the session from Sonnet to Opus and verifies the resumed session still remembers an earlier note.
|
||||
|
||||
## Live: APNs HTTP/2 proxy reachability
|
||||
|
||||
- Test: `src/infra/push-apns-http2.live.test.ts`
|
||||
- Goal: tunnel through a local HTTP CONNECT proxy to Apple's sandbox APNs endpoint, send the APNs HTTP/2 validation request, and assert Apple's real `403 InvalidProviderToken` response comes back through the proxy path.
|
||||
- Enable:
|
||||
- `OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_APNS_REACHABILITY=1 pnpm test:live src/infra/push-apns-http2.live.test.ts`
|
||||
- Optional timeout:
|
||||
- `OPENCLAW_LIVE_APNS_TIMEOUT_MS=30000`
|
||||
|
||||
## Live: ACP bind smoke (`/acp spawn ... --bind here`)
|
||||
|
||||
- Test: `src/gateway/gateway-acp-bind.live.test.ts`
|
||||
|
||||
@@ -123,8 +123,8 @@ pnpm test:docker:published-upgrade-survivor
|
||||
```
|
||||
|
||||
Available scenarios are `base`, `feishu-channel`, `bootstrap-persona`,
|
||||
`plugin-deps-cleanup`, `configured-plugin-installs`, `tilde-log-path`, and
|
||||
`versioned-runtime-deps`. In aggregate runs,
|
||||
`plugin-deps-cleanup`, `configured-plugin-installs`,
|
||||
`stale-source-plugin-shadow`, `tilde-log-path`, and `versioned-runtime-deps`. In aggregate runs,
|
||||
`OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues` expands to all reported
|
||||
issue-shaped scenarios, including the configured-plugin install migration.
|
||||
|
||||
|
||||
@@ -144,6 +144,14 @@ inside every shard.
|
||||
`aimock` starts a local AIMock-backed provider server for experimental
|
||||
fixture and protocol-mock coverage without replacing the scenario-aware
|
||||
`mock-openai` lane.
|
||||
- `pnpm test:plugins:kitchen-sink-live`
|
||||
- Runs the live OpenAI Kitchen Sink plugin gauntlet through QA Lab. It
|
||||
installs the external Kitchen Sink package, verifies the plugin SDK surface
|
||||
inventory, probes `/healthz` and `/readyz`, records gateway CPU/RSS
|
||||
evidence, runs a live OpenAI turn, and checks adversarial diagnostics.
|
||||
Requires live OpenAI auth such as `OPENAI_API_KEY`. In hydrated Testbox
|
||||
sessions it automatically sources the Testbox live-auth profile when the
|
||||
`openclaw-testbox-env` helper is present.
|
||||
- `pnpm test:gateway:cpu-scenarios`
|
||||
- Runs the gateway startup bench plus a small mock QA Lab scenario pack
|
||||
(`channel-chat-baseline`, `memory-failure-fallback`,
|
||||
@@ -195,6 +203,9 @@ inside every shard.
|
||||
`OPENCLAW_QA_CONVEX_SITE_URL` and the role secret. If
|
||||
`OPENCLAW_QA_CONVEX_SITE_URL` and a Convex role secret are present in CI,
|
||||
the Docker wrapper selects Convex automatically.
|
||||
- The wrapper validates Telegram or Convex credential env on the host before
|
||||
Docker build/install work. Set `OPENCLAW_NPM_TELEGRAM_SKIP_CREDENTIAL_PREFLIGHT=1`
|
||||
only when deliberately debugging pre-credential setup.
|
||||
- `OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE=ci|maintainer` overrides the shared
|
||||
`OPENCLAW_QA_CREDENTIAL_ROLE` for this lane only.
|
||||
- GitHub Actions exposes this lane as the manual maintainer workflow
|
||||
|
||||
@@ -93,6 +93,12 @@ curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method npm --ve
|
||||
npm i -g openclaw@latest
|
||||
```
|
||||
|
||||
Prefer `openclaw update` for supervised installs because it can coordinate the
|
||||
package swap with the running Gateway service. If you update manually while a
|
||||
managed Gateway is running, restart the Gateway immediately after the package
|
||||
manager finishes so the old process does not keep serving from replaced package
|
||||
files.
|
||||
|
||||
When `openclaw update` manages a global npm install, it installs the target into
|
||||
a temporary npm prefix first, verifies the packaged `dist` inventory, then swaps
|
||||
the clean package tree into the real global prefix. That avoids npm overlaying a
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "Google Meet plugin: join explicit Meet URLs through Chrome or Twilio with realtime voice defaults"
|
||||
summary: "Google Meet plugin: join explicit Meet URLs through Chrome or Twilio with agent talk-back defaults"
|
||||
read_when:
|
||||
- You want an OpenClaw agent to join a Google Meet call
|
||||
- You want an OpenClaw agent to create a new Google Meet call
|
||||
@@ -12,12 +12,12 @@ Google Meet participant support for OpenClaw — the plugin is explicit by desig
|
||||
- It only joins an explicit `https://meet.google.com/...` URL.
|
||||
- It can create a new Meet space through the Google Meet API, then join the
|
||||
returned URL.
|
||||
- `realtime` voice is the default mode.
|
||||
- Realtime voice can call back into the full OpenClaw agent when deeper
|
||||
reasoning or tools are needed.
|
||||
- Agents choose the join behavior with `mode`: use `realtime` for live
|
||||
listen/talk-back, or `transcribe` to join/control the browser without the
|
||||
realtime voice bridge.
|
||||
- `agent` is the default talk-back mode: realtime transcription listens, the
|
||||
configured OpenClaw agent answers, and regular OpenClaw TTS speaks into Meet.
|
||||
- `bidi` remains available as the fallback direct realtime voice model mode.
|
||||
- Agents choose the join behavior with `mode`: use `agent` for live
|
||||
listen/talk-back, `bidi` for direct realtime voice fallback, or `transcribe`
|
||||
to join/control the browser without the talk-back bridge.
|
||||
- Auth starts as personal Google OAuth or an already signed-in Chrome profile.
|
||||
- There is no automatic consent announcement.
|
||||
- The default Chrome audio backend is `BlackHole 2ch`.
|
||||
@@ -29,14 +29,15 @@ Google Meet participant support for OpenClaw — the plugin is explicit by desig
|
||||
|
||||
## Quick start
|
||||
|
||||
Install the local audio dependencies and configure a backend realtime voice
|
||||
provider. OpenAI is the default; Google Gemini Live also works with
|
||||
`realtime.provider: "google"`:
|
||||
Install the local audio dependencies and configure a realtime transcription
|
||||
provider plus regular OpenClaw TTS. OpenAI is the default transcription
|
||||
provider; Google Gemini Live also works as a separate `bidi` voice fallback with
|
||||
`realtime.voiceProvider: "google"`:
|
||||
|
||||
```bash
|
||||
brew install blackhole-2ch sox
|
||||
export OPENAI_API_KEY=sk-...
|
||||
# or
|
||||
# only needed when realtime.voiceProvider is "google" for bidi mode
|
||||
export GEMINI_API_KEY=...
|
||||
```
|
||||
|
||||
@@ -116,21 +117,21 @@ Or let an agent join through the `google_meet` tool:
|
||||
"action": "join",
|
||||
"url": "https://meet.google.com/abc-defg-hij",
|
||||
"transport": "chrome-node",
|
||||
"mode": "realtime"
|
||||
"mode": "agent"
|
||||
}
|
||||
```
|
||||
|
||||
The agent-facing `google_meet` tool stays available on non-macOS hosts for
|
||||
artifact, calendar, setup, transcribe, Twilio, and `chrome-node` flows. Local
|
||||
Chrome realtime actions are blocked there because the bundled realtime Chrome
|
||||
audio path currently depends on macOS `BlackHole 2ch`. On Linux, use
|
||||
`mode: "transcribe"`, Twilio dial-in, or a macOS `chrome-node` host for realtime
|
||||
Chrome participation.
|
||||
Chrome talk-back actions are blocked there because the bundled Chrome audio path
|
||||
currently depends on macOS `BlackHole 2ch`. On Linux, use `mode: "transcribe"`,
|
||||
Twilio dial-in, or a macOS `chrome-node` host for Chrome talk-back
|
||||
participation.
|
||||
|
||||
Create a new meeting and join it:
|
||||
|
||||
```bash
|
||||
openclaw googlemeet create --transport chrome-node --mode realtime
|
||||
openclaw googlemeet create --transport chrome-node --mode agent
|
||||
```
|
||||
|
||||
For API-created rooms, use Google Meet `SpaceConfig.accessType` when you want
|
||||
@@ -138,7 +139,7 @@ the room's no-knock policy to be explicit instead of inherited from the Google
|
||||
account defaults:
|
||||
|
||||
```bash
|
||||
openclaw googlemeet create --access-type OPEN --transport chrome-node --mode realtime
|
||||
openclaw googlemeet create --access-type OPEN --transport chrome-node --mode agent
|
||||
```
|
||||
|
||||
`OPEN` lets anyone with the Meet URL join without knocking. `TRUSTED` lets the
|
||||
@@ -177,20 +178,20 @@ can explain which path was used. `create` joins the new meeting by default and
|
||||
returns `joined: true` plus the join session. To only mint the URL, use
|
||||
`create --no-join` on the CLI or pass `"join": false` to the tool.
|
||||
|
||||
Or tell an agent: "Create a Google Meet, join it with realtime voice, and send
|
||||
me the link." The agent should call `google_meet` with `action: "create"` and
|
||||
then share the returned `meetingUri`.
|
||||
Or tell an agent: "Create a Google Meet, join it with the agent talk-back mode,
|
||||
and send me the link." The agent should call `google_meet` with
|
||||
`action: "create"` and then share the returned `meetingUri`.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "create",
|
||||
"transport": "chrome-node",
|
||||
"mode": "realtime"
|
||||
"mode": "agent"
|
||||
}
|
||||
```
|
||||
|
||||
For an observe-only/browser-control join, set `"mode": "transcribe"`. That does
|
||||
not start the duplex realtime model bridge, does not require BlackHole or SoX,
|
||||
not start the duplex realtime voice bridge, does not require BlackHole or SoX,
|
||||
and will not talk back into the meeting. Chrome joins in this mode also avoid
|
||||
OpenClaw's microphone/camera permission grant and avoid the Meet **Use
|
||||
microphone** path. If Meet shows an audio-choice interstitial, automation tries
|
||||
@@ -395,7 +396,7 @@ Common failure checks:
|
||||
|
||||
## Install notes
|
||||
|
||||
The Chrome realtime default uses two external tools:
|
||||
The Chrome talk-back default uses two external tools:
|
||||
|
||||
- `sox`: command-line audio utility. The plugin uses explicit CoreAudio
|
||||
device commands for the default 24 kHz PCM16 audio bridge.
|
||||
@@ -444,7 +445,7 @@ Enable the Voice Call plugin on the Gateway host, not on the Chrome node:
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
allow: ["google-meet", "voice-call"],
|
||||
allow: ["google-meet", "voice-call", "google"],
|
||||
entries: {
|
||||
"google-meet": {
|
||||
enabled: true,
|
||||
@@ -457,8 +458,24 @@ Enable the Voice Call plugin on the Gateway host, not on the Chrome node:
|
||||
enabled: true,
|
||||
config: {
|
||||
provider: "twilio",
|
||||
inboundPolicy: "allowlist",
|
||||
realtime: {
|
||||
enabled: true,
|
||||
provider: "google",
|
||||
instructions: "Join this Google Meet as an OpenClaw agent. Be brief.",
|
||||
toolPolicy: "safe-read-only",
|
||||
providers: {
|
||||
google: {
|
||||
silenceDurationMs: 500,
|
||||
startSensitivity: "high",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
google: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -471,8 +488,12 @@ secrets out of `openclaw.json`:
|
||||
export TWILIO_ACCOUNT_SID=AC...
|
||||
export TWILIO_AUTH_TOKEN=...
|
||||
export TWILIO_FROM_NUMBER=+15550001234
|
||||
export GEMINI_API_KEY=...
|
||||
```
|
||||
|
||||
Use `realtime.provider: "openai"` with the OpenAI provider plugin and
|
||||
`OPENAI_API_KEY` instead if that is your realtime voice provider.
|
||||
|
||||
Restart or reload the Gateway after enabling `voice-call`; plugin config changes
|
||||
do not appear in an already running Gateway process until it reloads.
|
||||
|
||||
@@ -818,7 +839,7 @@ Agents can also create an API-backed room with an explicit access policy:
|
||||
{
|
||||
"action": "create",
|
||||
"transport": "chrome-node",
|
||||
"mode": "realtime",
|
||||
"mode": "agent",
|
||||
"accessType": "OPEN"
|
||||
}
|
||||
```
|
||||
@@ -970,9 +991,11 @@ Workspace Developer Preview Program for Meet media APIs.
|
||||
|
||||
## Config
|
||||
|
||||
The common Chrome realtime path only needs the plugin enabled, BlackHole, SoX,
|
||||
and a backend realtime voice provider key. OpenAI is the default; set
|
||||
`realtime.provider: "google"` to use Google Gemini Live:
|
||||
The common Chrome agent path only needs the plugin enabled, BlackHole, SoX, a
|
||||
realtime transcription provider key, and a configured OpenClaw TTS provider.
|
||||
OpenAI is the default transcription provider; set `realtime.voiceProvider` to
|
||||
`"google"` and `realtime.model` to use Google Gemini Live for `bidi` mode
|
||||
without changing the default agent-mode transcription provider:
|
||||
|
||||
```bash
|
||||
brew install blackhole-2ch sox
|
||||
@@ -999,7 +1022,8 @@ Set the plugin config under `plugins.entries.google-meet.config`:
|
||||
Defaults:
|
||||
|
||||
- `defaultTransport: "chrome"`
|
||||
- `defaultMode: "realtime"`
|
||||
- `defaultMode: "agent"` (`"realtime"` is accepted only as a legacy
|
||||
compatibility alias for `"agent"`; new tool calls should say `"agent"`)
|
||||
- `chromeNode.node`: optional node id/name/IP for `chrome-node`
|
||||
- `chrome.audioBackend: "blackhole-2ch"`
|
||||
- `chrome.guestName: "OpenClaw Agent"`: name used on the signed-out Meet guest
|
||||
@@ -1009,10 +1033,14 @@ Defaults:
|
||||
- `chrome.reuseExistingTab: true`: activate an existing Meet tab instead of
|
||||
opening duplicates
|
||||
- `chrome.waitForInCallMs: 20000`: wait for the Meet tab to report in-call
|
||||
before the realtime intro is triggered
|
||||
before the talk-back intro is triggered
|
||||
- `chrome.audioFormat: "pcm16-24khz"`: command-pair audio format. Use
|
||||
`"g711-ulaw-8khz"` only for legacy/custom command pairs that still emit
|
||||
telephony audio.
|
||||
- `chrome.audioBufferBytes: 4096`: SoX processing buffer for generated Chrome
|
||||
command-pair audio commands. This is half of SoX's default 8192-byte buffer,
|
||||
reducing default pipe latency while leaving room to raise it on busy hosts.
|
||||
Values below SoX's minimum are clamped to 17 bytes.
|
||||
- `chrome.audioInputCommand`: SoX command reading from CoreAudio `BlackHole 2ch`
|
||||
and writing audio in `chrome.audioFormat`
|
||||
- `chrome.audioOutputCommand`: SoX command reading audio in `chrome.audioFormat`
|
||||
@@ -1027,7 +1055,21 @@ Defaults:
|
||||
interruption on `chrome.bargeInInputCommand`
|
||||
- `chrome.bargeInCooldownMs: 900`: minimum delay between repeated human
|
||||
interruption clears
|
||||
- `realtime.provider: "openai"`
|
||||
- `mode: "agent"`: default talk-back mode. Participant speech is transcribed by
|
||||
the configured realtime transcription provider, sent to the configured
|
||||
OpenClaw agent in a per-meeting sub-agent session, and spoken back through the
|
||||
normal OpenClaw TTS runtime.
|
||||
- `mode: "bidi"`: fallback direct bidirectional realtime model mode. The
|
||||
realtime voice provider answers participant speech directly and may call
|
||||
`openclaw_agent_consult` for deeper/tool-backed answers.
|
||||
- `mode: "transcribe"`: observe-only mode without the talk-back bridge.
|
||||
- `realtime.provider: "openai"`: compatibility fallback used when the scoped
|
||||
provider fields below are unset.
|
||||
- `realtime.transcriptionProvider: "openai"`: provider id used by `agent` mode
|
||||
for realtime transcription.
|
||||
- `realtime.voiceProvider`: provider id used by `bidi` mode for direct realtime
|
||||
voice. Set this to `"google"` to use Gemini Live while keeping agent-mode
|
||||
transcription on OpenAI.
|
||||
- `realtime.toolPolicy: "safe-read-only"`
|
||||
- `realtime.instructions`: brief spoken replies, with
|
||||
`openclaw_agent_consult` for deeper answers
|
||||
@@ -1071,14 +1113,17 @@ Optional overrides:
|
||||
chromeNode: {
|
||||
node: "parallels-macos",
|
||||
},
|
||||
defaultMode: "agent",
|
||||
realtime: {
|
||||
provider: "google",
|
||||
provider: "openai",
|
||||
transcriptionProvider: "openai",
|
||||
voiceProvider: "google",
|
||||
model: "gemini-2.5-flash-native-audio-preview-12-2025",
|
||||
agentId: "jay",
|
||||
toolPolicy: "owner",
|
||||
introMessage: "Say exactly: I'm here.",
|
||||
providers: {
|
||||
google: {
|
||||
model: "gemini-2.5-flash-native-audio-preview-12-2025",
|
||||
voice: "Kore",
|
||||
},
|
||||
},
|
||||
@@ -1086,6 +1131,50 @@ Optional overrides:
|
||||
}
|
||||
```
|
||||
|
||||
ElevenLabs for both agent-mode listening and speaking:
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
tts: {
|
||||
provider: "elevenlabs",
|
||||
providers: {
|
||||
elevenlabs: {
|
||||
modelId: "eleven_v3",
|
||||
voiceId: "pMsXgVXv3BLzUgSXRplE",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"google-meet": {
|
||||
config: {
|
||||
realtime: {
|
||||
transcriptionProvider: "elevenlabs",
|
||||
providers: {
|
||||
elevenlabs: {
|
||||
modelId: "scribe_v2_realtime",
|
||||
audioFormat: "ulaw_8000",
|
||||
sampleRate: 8000,
|
||||
commitStrategy: "vad",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The persistent Meet voice comes from
|
||||
`messages.tts.providers.elevenlabs.voiceId`. Agent replies can also use
|
||||
per-reply `[[tts:voiceId=... model=eleven_v3]]` directives when TTS model
|
||||
overrides are enabled, but config is the deterministic default for meetings.
|
||||
On join, the logs should show `transcriptionProvider=elevenlabs` and each
|
||||
spoken reply should log `provider=elevenlabs model=eleven_v3 voice=<voiceId>`.
|
||||
|
||||
Twilio-only config:
|
||||
|
||||
```json5
|
||||
@@ -1117,20 +1206,28 @@ Agents can use the `google_meet` tool:
|
||||
"action": "join",
|
||||
"url": "https://meet.google.com/abc-defg-hij",
|
||||
"transport": "chrome-node",
|
||||
"mode": "realtime"
|
||||
"mode": "agent"
|
||||
}
|
||||
```
|
||||
|
||||
Use `transport: "chrome"` when Chrome runs on the Gateway host. Use
|
||||
`transport: "chrome-node"` when Chrome runs on a paired node such as a Parallels
|
||||
VM. In both cases the realtime model and `openclaw_agent_consult` run on the
|
||||
Gateway host, so model credentials stay there.
|
||||
VM. In both cases the model providers and `openclaw_agent_consult` run on the
|
||||
Gateway host, so model credentials stay there. With the default `mode: "agent"`,
|
||||
the realtime transcription provider handles listening, the configured OpenClaw
|
||||
agent produces the answer, and regular OpenClaw TTS speaks it into Meet. Use
|
||||
`mode: "bidi"` when you want the realtime voice model to answer directly.
|
||||
Raw `mode: "realtime"` remains accepted as a legacy compatibility alias for
|
||||
`mode: "agent"`, but it is no longer advertised in the agent tool schema.
|
||||
Agent-mode logs include the resolved transcription provider/model at bridge
|
||||
startup and the TTS provider, model, voice, output format, and sample rate after
|
||||
each synthesized reply.
|
||||
|
||||
Use `action: "status"` to list active sessions or inspect a session ID. Use
|
||||
`action: "speak"` with `sessionId` and `message` to make the realtime agent
|
||||
speak immediately. Use `action: "test_speech"` to create or reuse the session,
|
||||
trigger a known phrase, and return `inCall` health when the Chrome host can
|
||||
report it. `test_speech` always forces `mode: "realtime"` and fails if asked to
|
||||
report it. `test_speech` always forces `mode: "agent"` and fails if asked to
|
||||
run in `mode: "transcribe"` because observe-only sessions intentionally cannot
|
||||
emit speech. Its `speechOutputVerified` result is based on realtime audio output
|
||||
bytes increasing during this test call, so a reused session with older audio
|
||||
@@ -1149,6 +1246,8 @@ a session ended.
|
||||
not send the intro/test phrase into the audio bridge.
|
||||
- `providerConnected` / `realtimeReady`: realtime voice bridge state
|
||||
- `lastInputAt` / `lastOutputAt`: last audio seen from or sent to the bridge
|
||||
- `audioOutputRouted` / `audioOutputDeviceLabel`: whether the Meet tab's media
|
||||
output was actively routed to the BlackHole device used by the bridge
|
||||
- `lastSuppressedInputAt` / `suppressedInputBytes`: loopback input ignored while
|
||||
assistant playback is active
|
||||
|
||||
@@ -1160,22 +1259,41 @@ a session ended.
|
||||
}
|
||||
```
|
||||
|
||||
## Realtime agent consult
|
||||
## Agent And Bidi Modes
|
||||
|
||||
Chrome realtime mode is optimized for a live voice loop. The realtime voice
|
||||
provider hears the meeting audio and speaks through the configured audio bridge.
|
||||
When the realtime model needs deeper reasoning, current information, or normal
|
||||
OpenClaw tools, it can call `openclaw_agent_consult`.
|
||||
Chrome `agent` mode is optimized for "my agent is in the meeting" behavior. The
|
||||
realtime transcription provider hears the meeting audio, final participant
|
||||
transcripts are routed through the configured OpenClaw agent, and the answer is
|
||||
spoken through the normal OpenClaw TTS runtime. Set `mode: "bidi"` when you want
|
||||
the realtime voice model to answer directly.
|
||||
Nearby final transcript fragments are coalesced before the consult so one spoken
|
||||
turn does not produce several stale partial answers. Realtime input is also
|
||||
suppressed while queued assistant audio is still playing,
|
||||
and recent assistant-like transcript echoes are ignored before the agent consult
|
||||
so BlackHole loopback does not make the agent answer its own speech.
|
||||
|
||||
| Mode | Who decides the answer | Speech output path | Use when |
|
||||
| ------- | ----------------------------- | -------------------------------------- | ----------------------------------------------------- |
|
||||
| `agent` | The configured OpenClaw agent | Normal OpenClaw TTS runtime | You want "my agent is in the meeting" behavior |
|
||||
| `bidi` | The realtime voice model | Realtime voice provider audio response | You want the lowest-latency conversational voice loop |
|
||||
|
||||
In `bidi` mode, when the realtime model needs deeper reasoning, current
|
||||
information, or normal OpenClaw tools, it can call `openclaw_agent_consult`.
|
||||
|
||||
The consult tool runs the regular OpenClaw agent behind the scenes with recent
|
||||
meeting transcript context and returns a concise spoken answer to the realtime
|
||||
voice session. The voice model can then speak that answer back into the meeting.
|
||||
It uses the same shared realtime consult tool as Voice Call.
|
||||
meeting transcript context and returns a concise spoken answer. In `agent` mode,
|
||||
OpenClaw sends that answer directly to the TTS runtime; in `bidi` mode, the
|
||||
realtime voice model can speak the consult result back into the meeting. It uses
|
||||
the same shared consult machinery as Voice Call.
|
||||
|
||||
By default, consults run against the `main` agent. Set `realtime.agentId` when a
|
||||
Meet lane should consult a dedicated OpenClaw agent workspace, model defaults,
|
||||
tool policy, memory, and session history.
|
||||
|
||||
Agent-mode consults use a per-meeting `agent:<id>:subagent:google-meet:<session>`
|
||||
session key so follow-up questions keep meeting context while inheriting normal
|
||||
agent policy from the configured agent.
|
||||
|
||||
`realtime.toolPolicy` controls the consult run:
|
||||
|
||||
- `safe-read-only`: expose the consult tool and limit the regular agent to
|
||||
@@ -1276,10 +1394,10 @@ The running agent only sees plugin tools registered by the current Gateway
|
||||
process.
|
||||
|
||||
On non-macOS Gateway hosts, the agent-facing `google_meet` tool stays visible,
|
||||
but local Chrome realtime actions are blocked before they hit the audio bridge.
|
||||
Local Chrome realtime audio currently depends on macOS `BlackHole 2ch`, so
|
||||
but local Chrome talk-back actions are blocked before they hit the audio bridge.
|
||||
Local Chrome talk-back audio currently depends on macOS `BlackHole 2ch`, so
|
||||
Linux agents should use `mode: "transcribe"`, Twilio dial-in, or a macOS
|
||||
`chrome-node` host instead of the default local Chrome realtime path.
|
||||
`chrome-node` host instead of the default local Chrome agent path.
|
||||
|
||||
### No connected Google Meet-capable node
|
||||
|
||||
@@ -1393,8 +1511,9 @@ openclaw googlemeet setup
|
||||
openclaw googlemeet doctor
|
||||
```
|
||||
|
||||
Use `mode: "realtime"` for listen/talk-back. `mode: "transcribe"` intentionally
|
||||
does not start the duplex realtime voice bridge. For observe-only debugging,
|
||||
Use `mode: "agent"` for the normal STT -> OpenClaw agent -> TTS talk-back path,
|
||||
or `mode: "bidi"` for the direct realtime voice fallback. `mode: "transcribe"`
|
||||
intentionally does not start the talk-back bridge. For observe-only debugging,
|
||||
run `openclaw googlemeet status --json <session-id>` after participants speak
|
||||
and check `captioning`, `transcriptLines`, and `lastCaptionText`. If `inCall` is
|
||||
true but `transcriptLines` stays at `0`, Meet captions may be disabled, no one
|
||||
@@ -1414,7 +1533,8 @@ Also verify:
|
||||
- `BlackHole 2ch` is visible on the Chrome host.
|
||||
- `sox` exists on the Chrome host.
|
||||
- Meet microphone and speaker are routed through the virtual audio path used by
|
||||
OpenClaw.
|
||||
OpenClaw. `doctor` should show `meet output routed: yes` for local Chrome
|
||||
realtime joins.
|
||||
|
||||
`googlemeet doctor [session-id]` prints the session, node, in-call state,
|
||||
manual action reason, realtime provider connection, `realtimeReady`, audio
|
||||
@@ -1575,14 +1695,22 @@ call still needs a participant path. This plugin keeps that boundary visible:
|
||||
Chrome handles browser participation and local audio routing; Twilio handles
|
||||
phone dial-in participation.
|
||||
|
||||
Chrome realtime mode needs `BlackHole 2ch` plus either:
|
||||
Chrome talk-back modes need `BlackHole 2ch` plus either:
|
||||
|
||||
- `chrome.audioInputCommand` plus `chrome.audioOutputCommand`: OpenClaw owns the
|
||||
realtime model bridge and pipes audio in `chrome.audioFormat` between those
|
||||
commands and the selected realtime voice provider. The default Chrome path is
|
||||
24 kHz PCM16; 8 kHz G.711 mu-law remains available for legacy command pairs.
|
||||
bridge and pipes audio in `chrome.audioFormat` between those commands and the
|
||||
selected provider. Agent mode uses realtime transcription plus regular TTS;
|
||||
bidi mode uses the realtime voice provider. The default Chrome path is 24 kHz
|
||||
PCM16 with `chrome.audioBufferBytes: 4096`; 8 kHz G.711 mu-law remains
|
||||
available for legacy command pairs.
|
||||
- `chrome.audioBridgeCommand`: an external bridge command owns the whole local
|
||||
audio path and must exit after starting or validating its daemon.
|
||||
audio path and must exit after starting or validating its daemon. This is only
|
||||
valid for `bidi` because `agent` mode needs direct command-pair access for TTS.
|
||||
|
||||
When an agent calls the `google_meet` tool in agent mode, the meeting consultant
|
||||
session forks the caller's current transcript before answering participant
|
||||
speech. The Meet session still stays separate (`agent:<agentId>:subagent:google-meet:<sessionId>`)
|
||||
so meeting follow-ups do not mutate the caller transcript directly.
|
||||
|
||||
For clean duplex audio, route Meet output and Meet microphone through separate
|
||||
virtual devices or a Loopback-style virtual device graph. A single shared
|
||||
@@ -1596,7 +1724,7 @@ Like `chrome.audioInputCommand` and `chrome.audioOutputCommand`, it is an
|
||||
operator-configured local command. Use an explicit trusted command path or
|
||||
argument list, and do not point it at scripts from untrusted locations.
|
||||
|
||||
`googlemeet speak` triggers the active realtime audio bridge for a Chrome
|
||||
`googlemeet speak` triggers the active talk-back audio bridge for a Chrome
|
||||
session. `googlemeet leave` stops that bridge. For Twilio sessions delegated
|
||||
through the Voice Call plugin, `leave` also hangs up the underlying voice call.
|
||||
Use `googlemeet end-active-conference` when you also want to close the active
|
||||
|
||||
@@ -264,6 +264,22 @@ the harness for one more model pass before finalization, `{ action:
|
||||
Codex native `Stop` hooks are relayed into this hook as OpenClaw
|
||||
`before_agent_finalize` decisions.
|
||||
|
||||
When returning `action: "revise"`, plugins can include `retry` metadata to make
|
||||
the extra model pass bounded and replay-safe:
|
||||
|
||||
```typescript
|
||||
type BeforeAgentFinalizeRetry = {
|
||||
instruction: string;
|
||||
idempotencyKey?: string;
|
||||
maxAttempts?: number;
|
||||
};
|
||||
```
|
||||
|
||||
`instruction` is appended to the revision reason sent to the harness.
|
||||
`idempotencyKey` lets the host count retries for the same plugin request across
|
||||
equivalent finalize decisions, and `maxAttempts` caps how many extra passes the
|
||||
host will allow before continuing with the natural final answer.
|
||||
|
||||
Non-bundled plugins that need `llm_input`, `llm_output`,
|
||||
`before_agent_finalize`, or `agent_end` must set:
|
||||
|
||||
|
||||
@@ -92,7 +92,9 @@ when it was previously pinned to an exact version or tag.
|
||||
When `openclaw update` runs on the beta channel, default-line npm and ClawHub
|
||||
plugin records try the matching plugin `@beta` release first. If that beta
|
||||
release does not exist, OpenClaw falls back to the recorded default/latest spec.
|
||||
Exact versions and explicit tags such as `@rc` or `@beta` are preserved.
|
||||
For npm plugins, OpenClaw also falls back when the beta package exists but fails
|
||||
install validation. Exact versions and explicit tags such as `@rc` or `@beta`
|
||||
are preserved.
|
||||
|
||||
## Uninstall plugins
|
||||
|
||||
|
||||
@@ -26,6 +26,26 @@ Source checkouts are different from npm installs: after `pnpm install`, bundled
|
||||
plugins load from `extensions/<id>` so local edits and package-local workspace
|
||||
dependencies are available.
|
||||
|
||||
## Install a plugin
|
||||
|
||||
Use the **Distribution** column to decide whether install is needed. Plugins that
|
||||
say `included in OpenClaw` are already present in the core package. Official
|
||||
external packages need one install, then a Gateway restart.
|
||||
|
||||
For example, Discord is an official external package:
|
||||
|
||||
```bash
|
||||
openclaw plugins install @openclaw/discord
|
||||
openclaw gateway restart
|
||||
openclaw plugins inspect discord --runtime --json
|
||||
```
|
||||
|
||||
Bare package specs try ClawHub first, then npm fallback. To force a source, use
|
||||
`clawhub:@openclaw/discord` or `npm:@openclaw/discord`. After install, follow
|
||||
the plugin's setup doc, such as [Discord](/channels/discord), to add credentials
|
||||
and channel config. See [Manage plugins](/plugins/manage-plugins) for update,
|
||||
uninstall, and publishing commands.
|
||||
|
||||
## Core npm package
|
||||
|
||||
| Plugin | Description | Distribution | Surface |
|
||||
|
||||
@@ -257,6 +257,9 @@ AI CLI backend such as `codex-cli`.
|
||||
plugin default before running the CLI.
|
||||
- Use `normalizeConfig` when a backend needs compatibility rewrites after merge
|
||||
(for example normalizing old flag shapes).
|
||||
- Use `resolveExecutionArgs` for request-scoped argv rewrites that belong to
|
||||
the CLI dialect, such as mapping OpenClaw thinking levels to a native effort
|
||||
flag.
|
||||
|
||||
### Exclusive slots
|
||||
|
||||
|
||||
@@ -417,12 +417,13 @@ Provider and channel execution paths must use the active runtime config snapshot
|
||||
});
|
||||
|
||||
await store.register("key-1", { value: "hello" });
|
||||
const claimed = await store.registerIfAbsent("dedupe-key", { value: "first" });
|
||||
const value = await store.lookup("key-1");
|
||||
await store.consume("key-1");
|
||||
await store.clear();
|
||||
```
|
||||
|
||||
Keyed stores survive restarts and are isolated by the runtime-bound plugin id. Limits: `maxEntries` per namespace, 1,000 live rows per plugin, JSON values under 64KB, and optional TTL expiry.
|
||||
Keyed stores survive restarts and are isolated by the runtime-bound plugin id. Use `registerIfAbsent(...)` for atomic dedupe claims: it returns `true` when the key was missing or expired and registered, or `false` when a live value already exists without overwriting its value, creation time, or TTL. Limits: `maxEntries` per namespace, 1,000 live rows per plugin, JSON values under 64KB, and optional TTL expiry.
|
||||
|
||||
<Warning>
|
||||
Bundled plugins only in this release.
|
||||
|
||||
@@ -250,6 +250,9 @@ Current runtime behaviour:
|
||||
Defaults: API key from `realtime.providers.google.apiKey`,
|
||||
`GEMINI_API_KEY`, or `GOOGLE_GENERATIVE_AI_API_KEY`; model
|
||||
`gemini-2.5-flash-native-audio-preview-12-2025`; voice `Kore`.
|
||||
`sessionResumption` and `contextWindowCompression` default on for longer,
|
||||
reconnectable calls. Use `silenceDurationMs`, `startSensitivity`, and
|
||||
`endSensitivity` to tune faster turn-taking on telephony audio.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -270,6 +273,8 @@ Current runtime behaviour:
|
||||
apiKey: "${GEMINI_API_KEY}",
|
||||
model: "gemini-2.5-flash-native-audio-preview-12-2025",
|
||||
voice: "Kore",
|
||||
silenceDurationMs: 500,
|
||||
startSensitivity: "high",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,18 +3,18 @@ summary: "Use ElevenLabs speech, Scribe STT, and realtime transcription with Ope
|
||||
read_when:
|
||||
- You want ElevenLabs text-to-speech in OpenClaw
|
||||
- You want ElevenLabs Scribe speech-to-text for audio attachments
|
||||
- You want ElevenLabs realtime transcription for Voice Call
|
||||
- You want ElevenLabs realtime transcription for Voice Call or Google Meet
|
||||
title: "ElevenLabs"
|
||||
---
|
||||
|
||||
OpenClaw uses ElevenLabs for text-to-speech, batch speech-to-text with Scribe
|
||||
v2, and Voice Call streaming STT with Scribe v2 Realtime.
|
||||
v2, and streaming STT with Scribe v2 Realtime.
|
||||
|
||||
| Capability | OpenClaw surface | Default |
|
||||
| ------------------------ | --------------------------------------------- | ------------------------ |
|
||||
| Text-to-speech | `messages.tts` / `talk` | `eleven_multilingual_v2` |
|
||||
| Batch speech-to-text | `tools.media.audio` | `scribe_v2` |
|
||||
| Streaming speech-to-text | Voice Call `streaming.provider: "elevenlabs"` | `scribe_v2_realtime` |
|
||||
| Capability | OpenClaw surface | Default |
|
||||
| ------------------------ | -------------------------------------------------------------------- | ------------------------ |
|
||||
| Text-to-speech | `messages.tts` / `talk` | `eleven_multilingual_v2` |
|
||||
| Batch speech-to-text | `tools.media.audio` | `scribe_v2` |
|
||||
| Streaming speech-to-text | Voice Call streaming or Google Meet `realtime.transcriptionProvider` | `scribe_v2_realtime` |
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -66,10 +66,10 @@ Use Scribe v2 for inbound audio attachments and short recorded voice segments:
|
||||
OpenClaw sends multipart audio to ElevenLabs `/v1/speech-to-text` with
|
||||
`model_id: "scribe_v2"`. Language hints map to `language_code` when present.
|
||||
|
||||
## Voice Call streaming STT
|
||||
## Streaming STT
|
||||
|
||||
The bundled `elevenlabs` plugin registers Scribe v2 Realtime for Voice Call
|
||||
streaming transcription.
|
||||
The bundled `elevenlabs` plugin registers Scribe v2 Realtime for Voice Call and
|
||||
Google Meet agent-mode streaming transcription.
|
||||
|
||||
| Setting | Config path | Default |
|
||||
| --------------- | ------------------------------------------------------------------------- | ------------------------------------------------- |
|
||||
@@ -111,7 +111,13 @@ provider defaults to `ulaw_8000`, so telephony frames can be forwarded without
|
||||
transcoding.
|
||||
</Note>
|
||||
|
||||
For Google Meet agent mode, set
|
||||
`plugins.entries.google-meet.config.realtime.transcriptionProvider` to
|
||||
`"elevenlabs"` and configure the same provider block under
|
||||
`plugins.entries.google-meet.config.realtime.providers.elevenlabs`.
|
||||
|
||||
## Related
|
||||
|
||||
- [Text-to-speech](/tools/tts)
|
||||
- [Google Meet](/plugins/google-meet)
|
||||
- [Model selection](/concepts/model-providers)
|
||||
|
||||
@@ -343,6 +343,8 @@ Gemini Live API for backend audio bridges such as Voice Call and Google Meet.
|
||||
| Activity handling | `...google.activityHandling` | Google default, `start-of-activity-interrupts` |
|
||||
| Turn coverage | `...google.turnCoverage` | Google default, `only-activity` |
|
||||
| Disable auto VAD | `...google.automaticActivityDetectionDisabled` | `false` |
|
||||
| Session resumption | `...google.sessionResumption` | `true` |
|
||||
| Context compression | `...google.contextWindowCompression` | `true` |
|
||||
| API key | `...google.apiKey` | Falls back to `models.providers.google.apiKey`, `GEMINI_API_KEY`, or `GOOGLE_API_KEY` |
|
||||
|
||||
Example Voice Call realtime config:
|
||||
|
||||
@@ -139,11 +139,11 @@ OpenRouter uses a Bearer token with your API key under the hood.
|
||||
On real OpenRouter requests (`https://openrouter.ai/api/v1`), OpenClaw also adds
|
||||
OpenRouter's documented app-attribution headers:
|
||||
|
||||
| Header | Value |
|
||||
| ------------------------- | --------------------- |
|
||||
| `HTTP-Referer` | `https://openclaw.ai` |
|
||||
| `X-OpenRouter-Title` | `OpenClaw` |
|
||||
| `X-OpenRouter-Categories` | `cli-agent` |
|
||||
| Header | Value |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| `HTTP-Referer` | `https://openclaw.ai` |
|
||||
| `X-OpenRouter-Title` | `OpenClaw` |
|
||||
| `X-OpenRouter-Categories` | `cli-agent,cloud-agent,programming-app,creative-writing,writing-assistant,general-chat,personal-agent` |
|
||||
|
||||
<Warning>
|
||||
If you repoint the OpenRouter provider at some other proxy or base URL, OpenClaw
|
||||
@@ -153,6 +153,39 @@ does **not** inject those OpenRouter-specific headers or Anthropic cache markers
|
||||
## Advanced configuration
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Response caching">
|
||||
OpenRouter response caching is opt-in. Enable it per OpenRouter model with
|
||||
model params:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openrouter/auto": {
|
||||
params: {
|
||||
responseCache: true,
|
||||
responseCacheTtlSeconds: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
OpenClaw sends `X-OpenRouter-Cache: true` and, when configured,
|
||||
`X-OpenRouter-Cache-TTL`. `responseCacheClear: true` forces a refresh for
|
||||
the current request and stores the replacement response. Snake_case aliases
|
||||
(`response_cache`, `response_cache_ttl_seconds`, and
|
||||
`response_cache_clear`) are also accepted.
|
||||
|
||||
This is separate from provider prompt caching and from OpenRouter's
|
||||
Anthropic `cache_control` markers. It is only applied on verified
|
||||
`openrouter.ai` routes, not custom proxy base URLs.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Anthropic cache markers">
|
||||
On verified OpenRouter routes, Anthropic model refs keep the
|
||||
OpenRouter-specific Anthropic `cache_control` markers that OpenClaw uses for
|
||||
@@ -178,7 +211,9 @@ does **not** inject those OpenRouter-specific headers or Anthropic cache markers
|
||||
On verified OpenRouter routes, `openrouter/deepseek/deepseek-v4-flash` and
|
||||
`openrouter/deepseek/deepseek-v4-pro` fill missing `reasoning_content` on
|
||||
replayed assistant turns so thinking/tool conversations keep DeepSeek V4's
|
||||
required follow-up shape.
|
||||
required follow-up shape. OpenClaw sends OpenRouter-supported
|
||||
`reasoning_effort` values for these routes; `xhigh` is the highest advertised
|
||||
level, and stale `max` overrides are mapped to `xhigh`.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="OpenAI-only request shaping">
|
||||
|
||||
@@ -211,6 +211,7 @@ Validation` or from the `main`/release workflow ref so workflow logic and
|
||||
against the published npm package using the shared leased Telegram credential
|
||||
pool. Local maintainer one-offs may omit the Convex vars and pass the three
|
||||
`OPENCLAW_QA_TELEGRAM_*` env credentials directly.
|
||||
- To run the full post-publish beta smoke from a maintainer machine, use `pnpm release:beta-smoke -- --beta betaN`. The helper runs Parallels npm update/fresh-target validation, dispatches `NPM Telegram Beta E2E`, polls the exact workflow run, downloads the artifact, and prints the Telegram report.
|
||||
- Maintainers can run the same post-publish check from GitHub Actions via the
|
||||
manual `NPM Telegram Beta E2E` workflow. It is intentionally manual-only and
|
||||
does not run on every merge.
|
||||
|
||||
@@ -44,7 +44,7 @@ title: "Tests"
|
||||
- `pnpm test:docker:openwebui`: Starts Dockerized OpenClaw + Open WebUI, signs in through Open WebUI, checks `/api/models`, then runs a real proxied chat through `/api/chat/completions`. Requires a usable live model key (for example OpenAI in `~/.profile`), pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites.
|
||||
- `pnpm test:docker:mcp-channels`: Starts a seeded Gateway container and a second client container that spawns `openclaw mcp serve`, then verifies routed conversation discovery, transcript reads, attachment metadata, live event queue behavior, outbound send routing, and Claude-style channel + permission notifications over the real stdio bridge. The Claude notification assertion reads the raw stdio MCP frames directly so the smoke reflects what the bridge actually emits.
|
||||
- `pnpm test:docker:upgrade-survivor`: Installs the packed OpenClaw tarball over a dirty old-user fixture, runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks that agents, channel config, plugin allowlists, workspace/session files, stale legacy plugin dependency state, startup, and RPC status survive.
|
||||
- `pnpm test:docker:published-upgrade-survivor`: Installs `openclaw@latest` by default, seeds realistic existing-user files without live provider or channel keys, configures that baseline with a baked `openclaw config set` command recipe, updates that published install to the packed OpenClaw tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks that configured intents, workspace/session files, stale plugin config and legacy dependency state, startup, `/healthz`, `/readyz`, and RPC status survive or repair cleanly. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, expand an exact matrix with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS` such as `all-since-2026.4.23`, or add scenario fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues`; the reported-issues set includes `configured-plugin-installs` to verify configured external OpenClaw plugins install automatically during upgrade. Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`.
|
||||
- `pnpm test:docker:published-upgrade-survivor`: Installs `openclaw@latest` by default, seeds realistic existing-user files without live provider or channel keys, configures that baseline with a baked `openclaw config set` command recipe, updates that published install to the packed OpenClaw tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks that configured intents, workspace/session files, stale plugin config and legacy dependency state, startup, `/healthz`, `/readyz`, and RPC status survive or repair cleanly. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, expand an exact matrix with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS` such as `all-since-2026.4.23`, or add scenario fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues`; the reported-issues set includes `configured-plugin-installs` to verify configured external OpenClaw plugins install automatically during upgrade and `stale-source-plugin-shadow` to keep source-only plugin shadows from breaking startup. Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`.
|
||||
- `pnpm test:docker:update-migration`: Runs the published-upgrade survivor harness in the cleanup-heavy `plugin-deps-cleanup` scenario, starting at `openclaw@2026.4.23` by default. The separate `Update Migration` workflow expands this lane with `baselines=all-since-2026.4.23` so every stable published package from `.23` onward updates to the candidate and proves configured-plugin dependency cleanup outside Full Release CI.
|
||||
- `pnpm test:docker:plugins`: Runs install/update smoke for local path, `file:`, npm registry packages with hoisted dependencies, git moving refs, ClawHub fixtures, marketplace updates, and Claude-bundle enable/inspect.
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ Validate the proxy from the same host, container, or service account that runs O
|
||||
openclaw proxy validate --proxy-url http://127.0.0.1:3128
|
||||
```
|
||||
|
||||
By default, when no custom destinations are provided, the command checks that `https://example.com/` succeeds and starts a temporary loopback canary that the proxy must not reach. The default denied check passes when the proxy returns a non-2xx denial response or blocks the canary with a transport failure; it fails if a successful response reaches the canary. If no proxy is enabled and configured, validation reports a config problem; use `--proxy-url` for a one-off preflight before changing config. Use `--allowed-url` and `--denied-url` to test deployment-specific expectations. Custom denied destinations are fail-closed: any HTTP response means the destination was reachable through the proxy, and any transport error is reported as inconclusive because OpenClaw cannot prove the proxy blocked a reachable origin. On validation failure, the command exits with code 1.
|
||||
By default, when no custom destinations are provided, the command checks that `https://example.com/` succeeds and starts a temporary loopback canary that the proxy must not reach. The default denied check passes when the proxy returns a non-2xx denial response or blocks the canary with a transport failure; it fails if a successful response reaches the canary. If no proxy is enabled and configured, validation reports a config problem; use `--proxy-url` for a one-off preflight before changing config. Use `--allowed-url` and `--denied-url` to test deployment-specific expectations. Add `--apns-reachable` to also verify direct APNs HTTP/2 delivery can open a CONNECT tunnel through the proxy and receive a sandbox APNs response; the probe uses an intentionally invalid provider token, so `403 InvalidProviderToken` is expected and counts as reachable. Custom denied destinations are fail-closed: any HTTP response means the destination was reachable through the proxy, and any transport error is reported as inconclusive because OpenClaw cannot prove the proxy blocked a reachable origin. On validation failure, the command exits with code 1.
|
||||
|
||||
Use `--json` for automation. The JSON output contains the overall result, the effective proxy config source, any config errors, and each destination check. Proxy URL credentials are redacted in text and JSON output:
|
||||
|
||||
@@ -158,6 +158,12 @@ Use `--json` for automation. The JSON output contains the overall result, the ef
|
||||
"url": "https://example.com/",
|
||||
"ok": true,
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"kind": "apns",
|
||||
"url": "https://api.sandbox.push.apple.com",
|
||||
"ok": true,
|
||||
"status": 403
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -193,6 +199,8 @@ proxy:
|
||||
|
||||
- The proxy improves coverage for process-local JavaScript HTTP and WebSocket clients, but it is not an OS-level network sandbox.
|
||||
- Raw `net`, `tls`, and `http2` sockets, native addons, and child processes may bypass Node-level proxy routing unless they inherit and respect proxy environment variables.
|
||||
- IRC is a raw TCP/TLS channel outside operator-managed forward proxy routing. In deployments that require all egress through that forward proxy, set `channels.irc.enabled=false` unless direct IRC egress is explicitly approved.
|
||||
- The local debug proxy is diagnostic tooling and its direct upstream forwarding for proxy requests and CONNECT tunnels is disabled by default while managed proxy mode is active; enable direct forwarding only for approved local diagnostics.
|
||||
- User local WebUIs and local model servers should be allowlisted in the operator proxy policy when needed; OpenClaw does not expose a general local-network bypass for them.
|
||||
- Gateway control-plane proxy bypass is intentionally limited to `localhost` and literal loopback IP URLs. Use `ws://127.0.0.1:18789`, `ws://[::1]:18789`, or `ws://localhost:18789` for local direct Gateway control-plane connections; other hostnames route like ordinary hostname-based traffic.
|
||||
- OpenClaw does not inspect, test, or certify your proxy policy.
|
||||
|
||||
@@ -83,7 +83,8 @@ requester chat when the run finishes.
|
||||
</Accordion>
|
||||
<Accordion title="Manual-spawn delivery resilience">
|
||||
- OpenClaw tries direct `agent` delivery first with a stable idempotency key.
|
||||
- If direct delivery fails, it falls back to queue routing.
|
||||
- If the requester-agent completion turn fails, produces no visible output, or returns an obviously incomplete prefix of the captured child result, OpenClaw falls back to direct completion delivery from the captured child result.
|
||||
- If direct delivery cannot be used, it falls back to queue routing.
|
||||
- If queue routing is still not available, the announce is retried with a short exponential backoff before final give-up.
|
||||
- Completion delivery keeps the resolved requester route: thread-bound or conversation-bound completion routes win when available; if the completion origin only provides a channel, OpenClaw fills the missing target/account from the requester session's resolved route (`lastChannel` / `lastTo` / `lastAccountId`) so direct delivery still works.
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ title: "Thinking levels"
|
||||
- Anthropic Claude Opus 4.7 does not default to adaptive thinking. Its API effort default remains provider-owned unless you explicitly set a thinking level.
|
||||
- Anthropic Claude Opus 4.7 maps `/think xhigh` to adaptive thinking plus `output_config.effort: "xhigh"`, because `/think` is a thinking directive and `xhigh` is the Opus 4.7 effort setting.
|
||||
- Anthropic Claude Opus 4.7 also exposes `/think max`; it maps to the same provider-owned max effort path.
|
||||
- DeepSeek V4 models expose `/think xhigh|max`; both map to DeepSeek `reasoning_effort: "max"` while lower non-off levels map to `high`.
|
||||
- Direct DeepSeek V4 models expose `/think xhigh|max`; both map to DeepSeek `reasoning_effort: "max"` while lower non-off levels map to `high`.
|
||||
- OpenRouter-routed DeepSeek V4 models expose `/think xhigh` and send OpenRouter-supported `reasoning_effort` values. Stored `max` overrides fall back to `xhigh`.
|
||||
- Ollama thinking-capable models expose `/think low|medium|high|max`; `max` maps to native `think: "high"` because Ollama's native API accepts `low`, `medium`, and `high` effort strings.
|
||||
- OpenAI GPT models map `/think` through model-specific Responses API effort support. `/think off` sends `reasoning.effort: "none"` only when the target model supports it; otherwise OpenClaw omits the disabled reasoning payload instead of sending an unsupported value.
|
||||
- Custom OpenAI-compatible catalog entries can opt into `/think xhigh` by setting `models.providers.<provider>.models[].compat.supportedReasoningEfforts` to include `"xhigh"`. This uses the same compat metadata that maps outbound OpenAI reasoning effort payloads, so menus, session validation, agent CLI, and `llm-task` agree with transport behavior.
|
||||
@@ -54,6 +55,7 @@ title: "Thinking levels"
|
||||
## Application by agent
|
||||
|
||||
- **Embedded Pi**: the resolved level is passed to the in-process Pi agent runtime.
|
||||
- **Claude CLI backend**: non-off levels are passed to Claude Code as `--effort` when using `claude-cli`; see [CLI backends](/gateway/cli-backends).
|
||||
|
||||
## Fast mode (/fast)
|
||||
|
||||
@@ -80,9 +82,12 @@ title: "Thinking levels"
|
||||
- `/verbose off` stores an explicit session override; clear it via the Sessions UI by choosing `inherit`.
|
||||
- Inline directive affects only that message; session/global defaults apply otherwise.
|
||||
- Send `/verbose` (or `/verbose:`) with no argument to see the current verbose level.
|
||||
- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool call back as its own metadata-only message, prefixed with `<emoji> <tool-name>: <arg>` when available (path/command). These tool summaries are sent as soon as each tool starts (separate bubbles), not as streaming deltas.
|
||||
- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool call back as its own metadata-only message, prefixed with `<emoji> <tool-name>: <arg>` when available. These tool summaries are sent as soon as each tool starts (separate bubbles), not as streaming deltas.
|
||||
- Tool failure summaries remain visible in normal mode, but raw error detail suffixes are hidden unless verbose is `on` or `full`.
|
||||
- When verbose is `full`, tool outputs are also forwarded after completion (separate bubble, truncated to a safe length). If you toggle `/verbose on|full|off` while a run is in-flight, subsequent tool bubbles honor the new setting.
|
||||
- `agents.defaults.toolProgressDetail` controls the shape of `/verbose` tool summaries and progress-draft tool lines. Use `"explain"` (default) for compact human labels such as `🛠️ Exec: checking JS syntax`; use `"raw"` when you also want the raw command/detail appended for debugging. Per-agent `agents.list[].toolProgressDetail` overrides the default.
|
||||
- `explain`: `🛠️ Exec: check JS syntax for /tmp/app.js`
|
||||
- `raw`: `🛠️ Exec: check JS syntax for /tmp/app.js, node --check /tmp/app.js`
|
||||
|
||||
## Plugin trace directives (/trace)
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ OpenClaw redacts sensitive values before writing export files:
|
||||
|
||||
The exporter also bounds input size:
|
||||
|
||||
- runtime sidecar files: 50 MiB
|
||||
- runtime sidecar files: live capture stops at 10 MiB and records a truncation event when space remains; export accepts existing runtime sidecars up to 50 MiB
|
||||
- session files: 50 MiB
|
||||
- runtime events: 200,000
|
||||
- total exported events: 250,000
|
||||
|
||||
@@ -87,7 +87,7 @@ Docs translations are generated for the same non-English locale set, but the doc
|
||||
|
||||
## Appearance themes
|
||||
|
||||
The Appearance panel keeps the built-in Claw, Knot, and Dash themes, plus one browser-local tweakcn import slot. To import a theme, open [tweakcn themes](https://tweakcn.com/themes), choose or create a theme, click **Share**, and paste the copied theme link into Appearance. The importer also accepts `https://tweakcn.com/r/themes/<id>` registry URLs, editor URLs like `https://tweakcn.com/editor/theme?theme=amethyst-haze`, relative `/themes/<id>` paths, raw theme IDs, and default theme names such as `amethyst-haze`.
|
||||
The Appearance panel keeps the built-in Claw, Knot, and Dash themes, plus one browser-local tweakcn import slot. To import a theme, open [tweakcn editor](https://tweakcn.com/editor/theme), choose or create a theme, click **Share**, and paste the copied theme link into Appearance. The importer also accepts `https://tweakcn.com/r/themes/<id>` registry URLs, editor URLs like `https://tweakcn.com/editor/theme?theme=amethyst-haze`, relative `/themes/<id>` paths, raw theme IDs, and default theme names such as `amethyst-haze`.
|
||||
|
||||
Imported themes are stored only in the current browser profile. They are not written to gateway config and do not sync across devices. Replacing the imported theme updates the one local slot; clearing it switches the active theme back to Claw if the imported theme was selected.
|
||||
|
||||
@@ -127,6 +127,7 @@ Imported themes are stored only in the current browser profile. They are not wri
|
||||
</Accordion>
|
||||
<Accordion title="Debug, logs, update">
|
||||
- Debug: status/health/models snapshots + event log + manual RPC calls (`status`, `health`, `models.list`).
|
||||
- The event log includes Control UI refresh/RPC timings plus browser responsiveness entries for long animation frames or long tasks when the browser exposes those PerformanceObserver entry types.
|
||||
- Logs: live tail of gateway file logs with filter/export (`logs.tail`).
|
||||
- Update: run a package/git update + restart (`update.run`) with a restart report, then poll `update.status` after reconnect to verify the running gateway version.
|
||||
|
||||
@@ -155,7 +156,11 @@ Imported themes are stored only in the current browser profile. They are not wri
|
||||
- Assistant/generated images are persisted as managed media references and served back through authenticated Gateway media URLs, so reloads do not depend on raw base64 image payloads staying in the chat history response.
|
||||
- `chat.history` also strips display-only inline directive tags from visible assistant text (for example `[[reply_to_*]]` and `[[audio_as_voice]]`), plain-text tool-call XML payloads (including `<tool_call>...</tool_call>`, `<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and truncated tool-call blocks), and leaked ASCII/full-width model control tokens, and omits assistant entries whose whole visible text is only the exact silent token `NO_REPLY` / `no_reply`.
|
||||
- During an active send and the final history refresh, the chat view keeps local optimistic user/assistant messages visible if `chat.history` briefly returns an older snapshot; the canonical transcript replaces those local messages once the Gateway history catches up.
|
||||
- Live `chat` events are delivery state, while `chat.history` is rebuilt from the durable session transcript. After tool-final events the Control UI reloads history and merges only a small optimistic tail; the transcript boundary is documented in [WebChat](/web/webchat).
|
||||
- `chat.inject` appends an assistant note to the session transcript and broadcasts a `chat` event for UI-only updates (no agent run, no channel delivery).
|
||||
- The chat header shows the agent filter before the session picker, and the session picker is scoped by the selected agent. Switching agents shows only sessions tied to that agent and falls back to that agent's main session when it has no saved dashboard sessions yet.
|
||||
- On desktop widths, chat controls stay on one compact row and collapse while scrolling down the transcript; scrolling up, returning to the top, or reaching the bottom restores the controls.
|
||||
- Consecutive duplicate text-only messages render as one bubble with a count badge. Messages that carry images, attachments, tool output, or canvas previews are left uncollapsed.
|
||||
- The chat header model and thinking pickers patch the active session immediately through `sessions.patch`; they are persistent session overrides, not one-turn-only send options.
|
||||
- Typing `/new` in the Control UI creates and switches to the same fresh dashboard session as New Chat. Typing `/reset` keeps the Gateway's explicit in-place reset for the current session.
|
||||
- The chat model picker requests the Gateway's configured model view. If `agents.defaults.models` is present, that allowlist drives the picker. Otherwise the picker shows explicit `models.providers.*.models` entries plus providers with usable auth. The full catalog stays available through the debug `models.list` RPC with `view: "all"`.
|
||||
@@ -383,6 +388,16 @@ When gateway auth is configured, the Control UI avatar endpoint requires the sam
|
||||
|
||||
If you disable gateway auth (not recommended on shared hosts), the avatar route also becomes unauthenticated, in line with the rest of the gateway.
|
||||
|
||||
## Assistant media route auth
|
||||
|
||||
When gateway auth is configured, assistant local-media previews use a two-step route:
|
||||
|
||||
- `GET /__openclaw__/assistant-media?meta=1&source=<path>` requires the normal Control UI operator auth. The browser sends the gateway token as a bearer header when checking availability.
|
||||
- Successful metadata responses include a short-lived `mediaTicket` scoped to that exact source path.
|
||||
- Browser-rendered image, audio, video, and document URLs use `mediaTicket=<ticket>` instead of the active gateway token or password. The ticket expires quickly and cannot authorize a different source.
|
||||
|
||||
This keeps normal media rendering compatible with browser-native media elements without putting reusable gateway credentials in visible media URLs.
|
||||
|
||||
## Building the UI
|
||||
|
||||
The Gateway serves static files from `dist/control-ui`. Build them with:
|
||||
|
||||
@@ -28,6 +28,7 @@ Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket.
|
||||
- Compaction entries render as an explicit compacted-history divider. The divider explains that earlier turns are preserved in a checkpoint and links to the Sessions checkpoint controls, where operators can branch or restore the pre-compaction view when their permissions allow it.
|
||||
- Control UI remembers the backing Gateway `sessionId` returned by `chat.history` and includes it on follow-up `chat.send` calls, so reconnects and page refreshes continue the same stored conversation unless the user starts or resets a session.
|
||||
- Control UI coalesces duplicate in-flight submits for the same session, message, and attachments before generating a new `chat.send` run id; the Gateway still dedupes repeated requests that reuse the same idempotency key.
|
||||
- Workspace startup files and pending `BOOTSTRAP.md` instructions are supplied through the agent system prompt's Project Context, not copied into the WebChat user message. Bootstrap truncation only adds a concise system-prompt recovery notice; detailed counts and config knobs stay on diagnostic surfaces.
|
||||
- `chat.history` is also display-normalized: runtime-only OpenClaw context,
|
||||
inbound envelope wrappers, inline delivery directive tags
|
||||
such as `[[reply_to_*]]` and `[[audio_as_voice]]`, plain-text tool-call XML
|
||||
@@ -44,6 +45,17 @@ Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket.
|
||||
- History is always fetched from the gateway (no local file watching).
|
||||
- If the gateway is unreachable, WebChat is read-only.
|
||||
|
||||
### Transcript and delivery model
|
||||
|
||||
WebChat has two separate data paths:
|
||||
|
||||
- The session JSONL file is the durable model/runtime transcript. For normal agent runs, Pi persists model-visible `user`, `assistant`, and `toolResult` messages through its session manager. WebChat does not write arbitrary delivery, status, or helper text into that transcript.
|
||||
- Gateway `ReplyPayload` events are the live delivery projection. They can be normalized for WebChat/channel display, block streaming, directive tags, media embedding, TTS/audio flags, and UI fallback behavior. They are not themselves the canonical session log.
|
||||
- WebChat injects assistant transcript entries only when the Gateway owns a displayed message outside a normal Pi assistant turn: `chat.inject`, non-agent command replies, aborted partial output, and WebChat-managed media transcript supplements.
|
||||
- `chat.history` reads the stored session transcript and applies WebChat display projection. If live assistant text appears during a run but disappears after history reload, first check whether the raw JSONL contains the assistant text, then whether `chat.history` projection stripped it, then whether the Control UI optimistic-tail merge replaced local delivery state with the persisted snapshot.
|
||||
|
||||
Normal agent-run final answers should be durable because Pi writes the assistant `message_end`. Any fallback that mirrors a delivered final payload into the transcript must first avoid duplicating an assistant turn that Pi already wrote.
|
||||
|
||||
## Control UI agents tools panel
|
||||
|
||||
- The Control UI `/agents` Tools panel has two separate views:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.5.3",
|
||||
"version": "2026.5.4",
|
||||
"description": "OpenClaw ACP runtime backend",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,10 +25,10 @@
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.3"
|
||||
"pluginApi": ">=2026.5.4"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.3",
|
||||
"openclawVersion": "2026.5.4",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./src/runtime-internals/mcp-proxy.mjs",
|
||||
|
||||
@@ -56,13 +56,20 @@ function generatedClaudePaths(stateDir: string): {
|
||||
}
|
||||
|
||||
function expectCodexWrapperCommand(command: string | undefined, wrapperPath: string): void {
|
||||
expect(command).toContain(process.execPath);
|
||||
expect(command).toContain(wrapperPath);
|
||||
expect(command).toContain(quoteArg(process.execPath));
|
||||
expect(command).toContain(quoteArg(wrapperPath));
|
||||
}
|
||||
|
||||
function expectClaudeWrapperCommand(command: string | undefined, wrapperPath: string): void {
|
||||
expect(command).toContain(process.execPath);
|
||||
expect(command).toContain(wrapperPath);
|
||||
expect(command).toContain(quoteArg(process.execPath));
|
||||
expect(command).toContain(quoteArg(wrapperPath));
|
||||
}
|
||||
|
||||
function expectWrapperToContainPathSuffix(wrapper: string, pathSuffix: string[]): void {
|
||||
const nativeSuffix = pathSuffix.join(path.sep);
|
||||
const escapedNativeSuffix = JSON.stringify(nativeSuffix).slice(1, -1);
|
||||
const posixSuffix = pathSuffix.join("/");
|
||||
expect(wrapper.includes(escapedNativeSuffix) || wrapper.includes(posixSuffix)).toBe(true);
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -199,7 +206,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
|
||||
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain("@zed-industries/codex-acp");
|
||||
expect(wrapper).toContain("bin/codex-acp.js");
|
||||
expectWrapperToContainPathSuffix(wrapper, ["bin", "codex-acp.js"]);
|
||||
expect(wrapper).toContain("defaultArgs = [installedBinPath]");
|
||||
});
|
||||
|
||||
@@ -219,7 +226,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
|
||||
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain("@agentclientprotocol/claude-agent-acp");
|
||||
expect(wrapper).toContain("dist/index.js");
|
||||
expectWrapperToContainPathSuffix(wrapper, ["dist", "index.js"]);
|
||||
expect(wrapper).toContain("defaultArgs = [installedBinPath]");
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { resolveAcpxPluginConfig, resolveAcpxPluginRoot } from "./config.js";
|
||||
|
||||
describe("embedded acpx plugin config", () => {
|
||||
it("resolves workspace stateDir and cwd by default", () => {
|
||||
const workspaceDir = "/tmp/openclaw-acpx";
|
||||
const workspaceDir = path.resolve("/tmp/openclaw-acpx");
|
||||
const resolved = resolveAcpxPluginConfig({
|
||||
rawConfig: undefined,
|
||||
workspaceDir,
|
||||
|
||||
@@ -125,6 +125,23 @@ describe("active-memory plugin", () => {
|
||||
"utf8",
|
||||
);
|
||||
};
|
||||
const makeMemoryToolAllowlistError = (
|
||||
reason: string,
|
||||
sources = "runtime toolsAllow: memory_recall, memory_search, memory_get",
|
||||
) =>
|
||||
new Error(
|
||||
`No callable tools remain after resolving explicit tool allowlist ` +
|
||||
`(${sources}); ${reason}. ` +
|
||||
`Fix the allowlist or enable the plugin that registers the requested tool.`,
|
||||
);
|
||||
const hasDebugLine = (needle: string) =>
|
||||
vi
|
||||
.mocked(api.logger.debug)
|
||||
.mock.calls.some((call: unknown[]) => String(call[0]).includes(needle));
|
||||
const hasWarnLine = (needle: string) =>
|
||||
vi
|
||||
.mocked(api.logger.warn)
|
||||
.mock.calls.some((call: unknown[]) => String(call[0]).includes(needle));
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
@@ -1074,9 +1091,12 @@ describe("active-memory plugin", () => {
|
||||
"Your job is to search memory and return only the most relevant memory context for that model.",
|
||||
);
|
||||
expect(runParams?.prompt).toContain(
|
||||
"You receive conversation context, including the user's latest message.",
|
||||
"You receive a bounded search query plus conversation context, including the user's latest message.",
|
||||
);
|
||||
expect(runParams?.prompt).toContain("Use only the available memory tools.");
|
||||
expect(runParams?.prompt).toContain(
|
||||
"Use the bounded search query as the memory_search or memory_recall query.",
|
||||
);
|
||||
expect(runParams?.prompt).toContain("Prefer memory_recall when available.");
|
||||
expect(runParams?.prompt).toContain(
|
||||
"If memory_recall is unavailable, use memory_search and memory_get.",
|
||||
@@ -1643,6 +1663,133 @@ describe("active-memory plugin", () => {
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("skips the recall subagent when no registered memory tools match", async () => {
|
||||
const sessionKey = "agent:main:missing-memory-tools";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-missing-memory-tools",
|
||||
updatedAt: 0,
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError("no registered tools matched");
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(true);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? missing memory tools", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(true);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(false);
|
||||
const lines = getActiveMemoryLines(sessionKey);
|
||||
expect(lines).toEqual([expect.stringContaining("🧩 Active Memory: status=empty")]);
|
||||
expect(lines.join("\n")).not.toContain("status=unavailable");
|
||||
});
|
||||
|
||||
it("skips missing memory tools when the allowlist error includes inherited sources", async () => {
|
||||
const sessionKey = "agent:main:missing-memory-tools-with-policy-source";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-missing-memory-tools-with-policy-source",
|
||||
updatedAt: 0,
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError(
|
||||
"no registered tools matched",
|
||||
"tools.allow: *, lobster; runtime toolsAllow: memory_recall, memory_search, memory_get",
|
||||
);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(true);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? missing memory tools with policy", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(true);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(false);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=empty"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps memory-tool allowlist errors visible when upstream policy can filter memory tools", async () => {
|
||||
const sessionKey = "agent:main:memory-tools-filtered-by-policy";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-memory-tools-filtered-by-policy",
|
||||
updatedAt: 0,
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError(
|
||||
"no registered tools matched",
|
||||
"tools.allow: read, exec; runtime toolsAllow: memory_recall, memory_search, memory_get",
|
||||
);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(false);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? memory tools filtered by policy", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(false);
|
||||
expect(hasWarnLine("No callable tools remain")).toBe(true);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["disabled tools", "tools are disabled for this run"],
|
||||
["models without tool support", "the selected model does not support tools"],
|
||||
])("keeps allowlist errors for %s visible", async (_label, reason) => {
|
||||
const sessionKey = `agent:main:${reason.replace(/\W+/g, "-")}`;
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: `s-${reason.replace(/\W+/g, "-")}`,
|
||||
updatedAt: 0,
|
||||
};
|
||||
const error = makeMemoryToolAllowlistError(reason);
|
||||
expect(__testing.isMissingRegisteredMemoryToolsError(error)).toBe(false);
|
||||
runEmbeddedPiAgent.mockRejectedValueOnce(error);
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: `what wings should i order? ${reason}`, messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(false);
|
||||
expect(hasWarnLine(reason)).toBe(true);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=unavailable"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not skip missing memory-tool allowlist errors after abort", async () => {
|
||||
const sessionKey = "agent:main:missing-memory-tools-after-abort";
|
||||
hoisted.sessionStore[sessionKey] = {
|
||||
sessionId: "s-missing-memory-tools-after-abort",
|
||||
updatedAt: 0,
|
||||
};
|
||||
runEmbeddedPiAgent.mockImplementationOnce(async (params: { abortSignal?: AbortSignal }) => {
|
||||
Object.defineProperty(params.abortSignal as AbortSignal, "aborted", {
|
||||
configurable: true,
|
||||
value: true,
|
||||
});
|
||||
throw makeMemoryToolAllowlistError("no registered tools matched");
|
||||
});
|
||||
|
||||
const result = await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? missing memory tools after abort", messages: [] },
|
||||
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(hasDebugLine("no memory tools registered")).toBe(false);
|
||||
expect(getActiveMemoryLines(sessionKey)).toEqual([
|
||||
expect.stringContaining("🧩 Active Memory: status=timeout"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns partial transcript text on timeout when the subagent has already written assistant output", async () => {
|
||||
__testing.setMinimumTimeoutMsForTests(1);
|
||||
__testing.setSetupGraceTimeoutMsForTests(0);
|
||||
@@ -2753,6 +2900,33 @@ describe("active-memory plugin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("skips colon-containing session-store channels for embedded recall (#77396)", async () => {
|
||||
hoisted.sessionStore["agent:main:qqbot:direct:12345"] = {
|
||||
sessionId: "session-a",
|
||||
updatedAt: 25,
|
||||
channel: "c2c:10D4F7C2",
|
||||
origin: {
|
||||
provider: "qqbot",
|
||||
},
|
||||
};
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{ prompt: "what wings should i order? scoped stored channel", messages: [] },
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:qqbot:direct:12345",
|
||||
messageProvider: "qqbot",
|
||||
channelId: "qqbot",
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent.mock.calls.at(-1)?.[0]).toMatchObject({
|
||||
messageChannel: "qqbot",
|
||||
messageProvider: "qqbot",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves an explicit real channel hint over a stale stored wrapper channel", async () => {
|
||||
hoisted.sessionStore["agent:main:telegram:direct:12345"] = {
|
||||
sessionId: "session-a",
|
||||
@@ -2867,10 +3041,54 @@ describe("active-memory plugin", () => {
|
||||
);
|
||||
|
||||
const prompt = runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.prompt;
|
||||
expect(prompt).toContain("Bounded memory search query:\nwhat should i grab on the way?");
|
||||
expect(prompt).toContain("Conversation context:\nwhat should i grab on the way?");
|
||||
expect(prompt).not.toContain("Recent conversation tail:");
|
||||
});
|
||||
|
||||
it("sends a bounded latest-message query instead of channel metadata to memory search", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
queryMode: "recent",
|
||||
};
|
||||
plugin.register(api as unknown as OpenClawPluginApi);
|
||||
|
||||
await hooks.before_prompt_build(
|
||||
{
|
||||
prompt: [
|
||||
"Conversation info:",
|
||||
"Sender: discord:user-123",
|
||||
"Untrusted Discord message body",
|
||||
"---",
|
||||
"do you remember my flight preferences?",
|
||||
].join("\n"),
|
||||
messages: [
|
||||
{ role: "user", content: "i have a flight tomorrow" },
|
||||
{ role: "assistant", content: "got it" },
|
||||
],
|
||||
},
|
||||
{
|
||||
agentId: "main",
|
||||
trigger: "user",
|
||||
sessionKey: "agent:main:main",
|
||||
messageProvider: "webchat",
|
||||
},
|
||||
);
|
||||
|
||||
const prompt = runEmbeddedPiAgent.mock.calls.at(-1)?.[0]?.prompt;
|
||||
expect(prompt).toContain(
|
||||
"Bounded memory search query:\ndo you remember my flight preferences?",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"Do not use channel metadata, provider metadata, debug output, or the full conversation context as the memory tool query.",
|
||||
);
|
||||
expect(prompt).toContain("Conversation context:");
|
||||
expect(prompt).toContain("Conversation info:");
|
||||
expect(prompt).not.toContain("Bounded memory search query:\nConversation info:");
|
||||
expect(prompt).not.toContain("Bounded memory search query:\nSender:");
|
||||
expect(prompt).not.toContain("Bounded memory search query:\nUntrusted Discord message body");
|
||||
});
|
||||
|
||||
it("supports full mode by sending the whole conversation", async () => {
|
||||
api.pluginConfig = {
|
||||
agents: ["main"],
|
||||
@@ -3209,7 +3427,6 @@ describe("active-memory plugin", () => {
|
||||
`^${escapeRegExp(expectedDir)}${escapeRegExp(path.sep)}active-memory-[a-z0-9]+-[a-f0-9]{8}\\.jsonl$`,
|
||||
),
|
||||
);
|
||||
expect(rmSpy).not.toHaveBeenCalled();
|
||||
expect(
|
||||
vi
|
||||
.mocked(api.logger.info)
|
||||
@@ -3217,6 +3434,7 @@ describe("active-memory plugin", () => {
|
||||
String(call[0]).includes(`transcript=${expectedDir}${path.sep}`),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(rmSpy.mock.calls.some(([target]) => String(target).startsWith(expectedDir))).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to the default transcript directory when transcriptDir is unsafe", async () => {
|
||||
|
||||
@@ -41,11 +41,13 @@ const DEFAULT_QMD_SEARCH_MODE = "search" as const;
|
||||
const DEFAULT_TRANSCRIPT_DIR = "active-memory";
|
||||
const DEFAULT_CIRCUIT_BREAKER_MAX_TIMEOUTS = 3;
|
||||
const DEFAULT_CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
|
||||
const ACTIVE_MEMORY_TOOL_ALLOWLIST = ["memory_recall", "memory_search", "memory_get"] as const;
|
||||
const TOGGLE_STATE_FILE = "session-toggles.json";
|
||||
const DEFAULT_PARTIAL_TRANSCRIPT_MAX_CHARS = 32_000;
|
||||
const DEFAULT_TRANSCRIPT_READ_MAX_LINES = 2_000;
|
||||
const DEFAULT_TRANSCRIPT_READ_MAX_BYTES = 50 * 1024 * 1024;
|
||||
const TIMEOUT_PARTIAL_DATA_GRACE_MS = 50;
|
||||
const TIMEOUT_PARTIAL_DATA_GRACE_MS = 500;
|
||||
const MAX_ACTIVE_MEMORY_SEARCH_QUERY_CHARS = 480;
|
||||
const TERMINAL_MEMORY_SEARCH_POLL_INTERVAL_MS = 25;
|
||||
|
||||
const NO_RECALL_VALUES = new Set([
|
||||
@@ -493,6 +495,38 @@ function normalizeOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function isMissingRegisteredMemoryToolsError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
const message = error.message.trim();
|
||||
const prefix = "No callable tools remain after resolving explicit tool allowlist (";
|
||||
const suffix =
|
||||
"); no registered tools matched. Fix the allowlist or enable the plugin that registers the requested tool.";
|
||||
if (!message.startsWith(prefix) || !message.endsWith(suffix)) {
|
||||
return false;
|
||||
}
|
||||
const sources = message.slice(prefix.length, -suffix.length);
|
||||
const runtimeSource = `runtime toolsAllow: ${ACTIVE_MEMORY_TOOL_ALLOWLIST.join(", ")}`;
|
||||
const sourceParts = sources
|
||||
.split(";")
|
||||
.map((source) => source.trim())
|
||||
.filter(Boolean);
|
||||
if (!sourceParts.includes(runtimeSource)) {
|
||||
return false;
|
||||
}
|
||||
return sourceParts.every((source) => {
|
||||
if (source === runtimeSource) {
|
||||
return true;
|
||||
}
|
||||
const entries = source
|
||||
.slice(source.indexOf(":") + 1)
|
||||
.split(",")
|
||||
.map((entry) => entry.trim());
|
||||
return entries.includes("*");
|
||||
});
|
||||
}
|
||||
|
||||
function resolveRecallRunChannelContext(params: {
|
||||
api: OpenClawPluginApi;
|
||||
agentId: string;
|
||||
@@ -560,9 +594,17 @@ function resolveRecallRunChannelContext(params: {
|
||||
store,
|
||||
sessionKey: resolvedSessionKey,
|
||||
}).existing;
|
||||
const strongEntryChannel =
|
||||
const rawStrongEntryChannel =
|
||||
normalizeOptionalString(sessionEntry?.lastChannel) ??
|
||||
normalizeOptionalString(sessionEntry?.channel);
|
||||
// Channel IDs containing ":" are scoped conversation IDs (e.g. QQ c2c
|
||||
// "c2c:10D4F7C2..."), not runnable channel names. The same guard that
|
||||
// applies to explicit channelId (#76704) must also apply to channels
|
||||
// read from the session store (#77396).
|
||||
const strongEntryChannel =
|
||||
rawStrongEntryChannel && !rawStrongEntryChannel.includes(":")
|
||||
? rawStrongEntryChannel
|
||||
: undefined;
|
||||
const weakEntryChannel = normalizeOptionalString(sessionEntry?.origin?.provider);
|
||||
return resolveReturnValue({
|
||||
resolvedChannel: strongEntryChannel ?? weakEntryChannel,
|
||||
@@ -932,13 +974,16 @@ function buildPromptStyleLines(style: ActiveMemoryPromptStyle): string[] {
|
||||
function buildRecallPrompt(params: {
|
||||
config: ResolvedActiveRecallPluginConfig;
|
||||
query: string;
|
||||
searchQuery: string;
|
||||
}): string {
|
||||
const defaultInstructions = [
|
||||
"You are a memory search agent.",
|
||||
"Another model is preparing the final user-facing answer.",
|
||||
"Your job is to search memory and return only the most relevant memory context for that model.",
|
||||
"You receive conversation context, including the user's latest message.",
|
||||
"You receive a bounded search query plus conversation context, including the user's latest message.",
|
||||
"Use only the available memory tools.",
|
||||
"Use the bounded search query as the memory_search or memory_recall query.",
|
||||
"Do not use channel metadata, provider metadata, debug output, or the full conversation context as the memory tool query.",
|
||||
"Prefer memory_recall when available.",
|
||||
"If memory_recall is unavailable, use memory_search and memory_get.",
|
||||
"When searching for preference or habit recall, use a permissive recall limit or memory_search threshold before deciding that no useful memory exists.",
|
||||
@@ -990,7 +1035,11 @@ function buildRecallPrompt(params: {
|
||||
]
|
||||
.filter((section) => section.length > 0)
|
||||
.join("\n\n");
|
||||
return `${instructionBlock}\n\nConversation context:\n${params.query}`;
|
||||
return [
|
||||
instructionBlock,
|
||||
`Bounded memory search query:\n${params.searchQuery}`,
|
||||
`Conversation context:\n${params.query}`,
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
function isEnabledForAgent(
|
||||
@@ -2048,6 +2097,83 @@ function buildQuery(params: {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function stripExternalUntrustedBlocks(text: string): string {
|
||||
return text.replace(
|
||||
/<<<EXTERNAL_UNTRUSTED_CONTENT\b[^>]*>>>[\s\S]*?<<<END_EXTERNAL_UNTRUSTED_CONTENT\b[^>]*>>>/g,
|
||||
" ",
|
||||
);
|
||||
}
|
||||
|
||||
function stripJsonFences(text: string): string {
|
||||
return text.replace(/```(?:json)?\s*[\s\S]*?```/gi, " ");
|
||||
}
|
||||
|
||||
function stripActiveMemoryXmlBlocks(text: string): string {
|
||||
return text.replace(/<active_memory_plugin>[\s\S]*?<\/active_memory_plugin>/gi, " ");
|
||||
}
|
||||
|
||||
function normalizeSearchQueryText(text: string): string {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => {
|
||||
if (!line) {
|
||||
return false;
|
||||
}
|
||||
if (/^(conversation info|sender|untrusted context)\b/i.test(line)) {
|
||||
return false;
|
||||
}
|
||||
if (/^(source: external|---|untrusted discord message body)$/i.test(line)) {
|
||||
return false;
|
||||
}
|
||||
if (/^⚠️?\s*Agent couldn't generate a response/i.test(line)) {
|
||||
return false;
|
||||
}
|
||||
if (/^Please try again\.?$/i.test(line)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.join(" ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function clampSearchQuery(text: string): string {
|
||||
const normalized = text.replace(/\s+/g, " ").trim();
|
||||
return normalized.length > MAX_ACTIVE_MEMORY_SEARCH_QUERY_CHARS
|
||||
? normalized.slice(0, MAX_ACTIVE_MEMORY_SEARCH_QUERY_CHARS).trim()
|
||||
: normalized;
|
||||
}
|
||||
|
||||
function buildSearchQuery(params: {
|
||||
latestUserMessage: string;
|
||||
recentTurns?: ActiveRecallRecentTurn[];
|
||||
}): string {
|
||||
const latest = clampSearchQuery(
|
||||
normalizeSearchQueryText(
|
||||
stripActiveMemoryXmlBlocks(
|
||||
stripJsonFences(stripExternalUntrustedBlocks(params.latestUserMessage)),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (latest.length >= 12 || !params.recentTurns?.length) {
|
||||
return latest || clampSearchQuery(params.latestUserMessage);
|
||||
}
|
||||
const previousUser = [...params.recentTurns]
|
||||
.toReversed()
|
||||
.find((turn) => turn.role === "user" && turn.text.trim() !== params.latestUserMessage.trim());
|
||||
if (!previousUser) {
|
||||
return latest || clampSearchQuery(params.latestUserMessage);
|
||||
}
|
||||
const context = clampSearchQuery(
|
||||
normalizeSearchQueryText(stripRecalledContextNoise(previousUser.text)),
|
||||
)
|
||||
.slice(0, 120)
|
||||
.trim();
|
||||
return clampSearchQuery(context ? `${context} ${latest}` : latest);
|
||||
}
|
||||
|
||||
function extractTextContent(content: unknown): string {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
@@ -2216,6 +2342,7 @@ async function runRecallSubagent(params: {
|
||||
messageProvider?: string;
|
||||
channelId?: string;
|
||||
query: string;
|
||||
searchQuery: string;
|
||||
currentModelProviderId?: string;
|
||||
currentModelId?: string;
|
||||
modelRef?: { provider: string; model: string };
|
||||
@@ -2270,6 +2397,7 @@ async function runRecallSubagent(params: {
|
||||
const prompt = buildRecallPrompt({
|
||||
config: params.config,
|
||||
query: params.query,
|
||||
searchQuery: params.searchQuery,
|
||||
});
|
||||
const { messageChannel, messageProvider } = resolveRecallRunChannelContext({
|
||||
api: params.api,
|
||||
@@ -2299,7 +2427,7 @@ async function runRecallSubagent(params: {
|
||||
timeoutMs: embeddedTimeoutMs,
|
||||
runId: subagentSessionId,
|
||||
trigger: "manual",
|
||||
toolsAllow: ["memory_recall", "memory_search", "memory_get"],
|
||||
toolsAllow: [...ACTIVE_MEMORY_TOOL_ALLOWLIST],
|
||||
disableMessageTool: true,
|
||||
allowGatewaySubagentBinding: true,
|
||||
bootstrapContextMode: "lightweight",
|
||||
@@ -2342,6 +2470,12 @@ async function runRecallSubagent(params: {
|
||||
const searchDebug = partialReply ? await readActiveMemorySearchDebug(sessionFile) : undefined;
|
||||
attachPartialTimeoutData(error, partialReply, searchDebug);
|
||||
}
|
||||
if (!params.abortSignal?.aborted && isMissingRegisteredMemoryToolsError(error)) {
|
||||
params.api.logger.debug?.(
|
||||
`active-memory: no memory tools registered (memory-core or memory-lancedb required); skipping sub-agent`,
|
||||
);
|
||||
return { rawReply: "NONE" };
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
@@ -2359,6 +2493,7 @@ async function maybeResolveActiveRecall(params: {
|
||||
messageProvider?: string;
|
||||
channelId?: string;
|
||||
query: string;
|
||||
searchQuery: string;
|
||||
currentModelProviderId?: string;
|
||||
currentModelId?: string;
|
||||
}): Promise<ActiveRecallResult> {
|
||||
@@ -2436,7 +2571,9 @@ async function maybeResolveActiveRecall(params: {
|
||||
|
||||
if (params.config.logging) {
|
||||
params.api.logger.info?.(
|
||||
`${logPrefix} start timeoutMs=${String(params.config.timeoutMs)} queryChars=${String(params.query.length)}`,
|
||||
`${logPrefix} start timeoutMs=${String(params.config.timeoutMs)} queryChars=${String(
|
||||
params.query.length,
|
||||
)} searchQueryChars=${String(params.searchQuery.length)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2805,11 +2942,16 @@ export default definePluginEntry({
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
const recentTurns = extractRecentTurns(event.messages);
|
||||
const query = buildQuery({
|
||||
latestUserMessage: event.prompt,
|
||||
recentTurns: extractRecentTurns(event.messages),
|
||||
recentTurns,
|
||||
config,
|
||||
});
|
||||
const searchQuery = buildSearchQuery({
|
||||
latestUserMessage: event.prompt,
|
||||
recentTurns,
|
||||
});
|
||||
const result = await maybeResolveActiveRecall({
|
||||
api,
|
||||
config,
|
||||
@@ -2819,6 +2961,7 @@ export default definePluginEntry({
|
||||
messageProvider: ctx.messageProvider,
|
||||
channelId: ctx.channelId,
|
||||
query,
|
||||
searchQuery,
|
||||
currentModelProviderId: ctx.modelProviderId,
|
||||
currentModelId: ctx.modelId,
|
||||
});
|
||||
@@ -2855,6 +2998,7 @@ const testing = {
|
||||
buildPromptPrefix,
|
||||
getCachedResult,
|
||||
isCircuitBreakerOpen,
|
||||
isMissingRegisteredMemoryToolsError,
|
||||
normalizePluginConfig,
|
||||
readActiveMemorySearchDebug,
|
||||
readPartialAssistantText,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/alibaba-provider",
|
||||
"version": "2026.5.3",
|
||||
"version": "2026.5.4",
|
||||
"private": true,
|
||||
"description": "OpenClaw Alibaba Model Studio video provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.5.3",
|
||||
"version": "2026.5.4",
|
||||
"private": true,
|
||||
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.5.3",
|
||||
"version": "2026.5.4",
|
||||
"private": true,
|
||||
"description": "OpenClaw Amazon Bedrock provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.5.3",
|
||||
"version": "2026.5.4",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic Vertex provider plugin",
|
||||
"type": "module",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user