Compare commits

..

1 Commits

Author SHA1 Message Date
Vincent Koc
2afd1c0077 fix(types): unblock changed gate checks 2026-05-28 17:48:14 +02:00
17153 changed files with 149379 additions and 627824 deletions

View File

@@ -22,8 +22,6 @@ Use when:
- Read dependency docs/source/types when the finding depends on external behavior.
- Reject unrealistic edge cases, speculative risks, broad rewrites, and fixes that over-complicate the codebase.
- Prefer small fixes at the right ownership boundary; no refactor unless it clearly improves the bug class.
- When an accepted finding shows a bug class or repeated pattern, inspect the current PR scope for sibling instances before fixing.
- Fix the scoped bug class at once when practical; stop at touched surfaces, owner boundaries, and clear follow-up territory.
- Keep going until structured review returns no accepted/actionable findings.
- If a review-triggered fix changes code, rerun focused tests and rerun the structured review helper.
- For security-audit suppression changes, verify accepted findings remain auditable: suppressed findings stay in structured output, active output keeps an unsuppressible suppression notice, and aggregate findings cannot hide unrelated active risk.
@@ -102,10 +100,6 @@ Format first if formatting can change line locations. Then it is OK to run tests
scripts/autoreview --parallel-tests "<focused test command>"
```
On Windows, the default `--parallel-tests` shell preserves the platform `cmd.exe`
semantics used by Python `shell=True`. Use `--parallel-tests-shell powershell`
or `--parallel-tests-shell pwsh` when the focused test command is PowerShell-specific.
Tradeoff: tests may force code changes that stale the review. If tests or review lead to code edits, rerun the affected tests and rerun review until no accepted/actionable findings remain. Once that rerun exits cleanly, stop; do not spend another long review cycle on redundant confirmation.
## Review Panels
@@ -150,22 +144,6 @@ OpenClaw repo-local helper:
.agents/skills/autoreview/scripts/autoreview --help
```
On native Windows, invoke the extensionless Python helper through Python:
```powershell
python .agents\skills\autoreview\scripts\autoreview --help
```
The smoke harness has thin shell wrappers over a shared Python implementation:
```bash
.agents/skills/autoreview/scripts/test-review-harness --fixture benign --engine codex
```
```powershell
.agents\skills\autoreview\scripts\test-review-harness.ps1 -Fixture benign -Engine codex
```
`agent-scripts` checkout helper:
```bash
@@ -191,11 +169,10 @@ The helper:
- otherwise uses current PR base if `gh pr view` works
- otherwise uses `origin/main` for non-main branches
- supports `--engine codex`, `claude`, `droid`, and `copilot`; default is `AUTOREVIEW_ENGINE` or `codex`; Codex should remain the default when nothing is set
- resolves bare `git`, `gh`, reviewer, and PowerShell shell commands from absolute `PATH` entries only, never from the reviewed checkout; explicit relative `--*-bin` paths are resolved from the reviewed repository root
- use `--mode commit --commit <ref>` for already-committed work, especially clean `main` after landing
- should be left in `--mode auto` or forced to `--mode branch` for PR/branch work; do not force `--mode local` after committing
- writes only to stdout unless `--output`, `--json-output`, or live streamed engine stderr is set
- supports `--dry-run`, `--parallel-tests`, `--parallel-tests-shell`, `--prompt`, `--prompt-file`, `--dataset`, `--no-tools`, `--no-web-search`, and commit refs
- supports `--dry-run`, `--parallel-tests`, `--prompt`, `--prompt-file`, `--dataset`, `--no-tools`, `--no-web-search`, and commit refs
- supports `--stream-engine-output` or `AUTOREVIEW_STREAM_ENGINE_OUTPUT=1` for live engine text while preserving structured validation; Codex and Claude hide tool/file event details, emit compact activity summaries, and report usage at turn completion
- supports opt-in review panels with `--panel` / `--reviewers`, plus per-engine `--model` and `--thinking`
- allows read-only tools and web search by default where the selected CLI supports them; forbids nested review in the prompt; Codex is run through `codex exec` with read-only sandbox and structured output

View File

@@ -214,17 +214,12 @@ def run_with_stream(
def git(repo: Path, *args: str, check: bool = True) -> str:
return run([resolve_command("git", repo), *args], repo, check=check).stdout
return run(["git", *args], repo, check=check).stdout
def repo_root() -> Path:
start = Path.cwd().resolve()
unsafe_root = discover_repo_root(start) or start
git_bin = find_command("git", unsafe_root)
if not git_bin:
raise SystemExit("git executable not found. Install Git or add it to PATH.")
result = subprocess.run(
[git_bin, "rev-parse", "--show-toplevel"],
["git", "rev-parse", "--show-toplevel"],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
@@ -234,16 +229,6 @@ def repo_root() -> Path:
return Path(result.stdout.strip()).resolve()
def discover_repo_root(start: Path) -> Path | None:
current = start
while True:
if (current / ".git").exists():
return current
if current.parent == current:
return None
current = current.parent
def current_branch(repo: Path) -> str:
return git(repo, "branch", "--show-current", check=False).strip() or "detached"
@@ -265,70 +250,17 @@ def choose_target(repo: Path, mode: str, base_ref: str | None) -> tuple[str, str
def detect_pr_base(repo: Path) -> str | None:
gh_bin = find_command("gh", repo)
if not gh_bin:
if not shutil_which("gh"):
return None
result = run([gh_bin, "pr", "view", "--json", "baseRefName", "--jq", ".baseRefName"], repo, check=False)
result = run(["gh", "pr", "view", "--json", "baseRefName", "--jq", ".baseRefName"], repo, check=False)
base = result.stdout.strip()
return f"origin/{base}" if result.returncode == 0 and base else None
def resolve_command(name: str, repo: Path) -> str:
resolved = find_command(name, repo)
if resolved:
return resolved
raise SystemExit(f"executable not found: {name}. Install it or pass an explicit trusted path when supported.")
def find_command(name: str, repo: Path) -> str | None:
command = Path(name)
if has_directory_component(name, command):
base = command if command.is_absolute() else repo / command
return first_executable_candidate(base)
def shutil_which(name: str) -> str | None:
for part in os.environ.get("PATH", "").split(os.pathsep):
if not part or part == ".":
continue
path_part = Path(part)
if not path_part.is_absolute():
continue
try:
resolved_part = path_part.resolve()
resolved_repo = repo.resolve()
except OSError:
continue
if is_within(resolved_part, resolved_repo):
continue
found = first_executable_candidate(resolved_part / name, reject_root=resolved_repo)
if found:
return found
return None
def is_within(path: Path, root: Path) -> bool:
return path == root or path.is_relative_to(root)
def has_directory_component(name: str, command: Path) -> bool:
separators = [separator for separator in (os.sep, os.altsep) if separator]
return command.is_absolute() or bool(command.drive) or any(separator in name for separator in separators)
def first_executable_candidate(path: Path, *, reject_root: Path | None = None) -> str | None:
if os.name == "nt" and not path.suffix:
extensions = [ext for ext in os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";") if ext]
candidates = [path.with_suffix(ext.lower()) for ext in extensions]
candidates.extend(path.with_suffix(ext.upper()) for ext in extensions)
candidates.append(path)
else:
candidates = [path]
for candidate in candidates:
if candidate.is_file() and os.access(candidate, os.X_OK):
if reject_root is not None:
try:
if is_within(candidate.resolve(), reject_root):
continue
except OSError:
continue
candidate = Path(part) / name
if candidate.exists() and os.access(candidate, os.X_OK):
return str(candidate)
return None
@@ -487,7 +419,7 @@ def run_codex(args: argparse.Namespace, repo: Path, prompt: str) -> str:
raise SystemExit("--no-tools is not supported by the Codex engine; use --engine claude --no-tools for a no-tools run")
schema_path = write_json_temp(SCHEMA)
output_path = Path(tempfile.NamedTemporaryFile("w", suffix=".json", delete=False).name)
cmd = [resolve_command(args.codex_bin, repo), "--ask-for-approval", "never"]
cmd = [args.codex_bin, "--ask-for-approval", "never"]
if args.web_search:
cmd.append("--search")
if args.model:
@@ -531,7 +463,7 @@ def run_codex(args: argparse.Namespace, repo: Path, prompt: str) -> str:
def run_claude(args: argparse.Namespace, repo: Path, prompt: str) -> str:
cmd = [
resolve_command(args.claude_bin, repo),
args.claude_bin,
"--print",
"--no-session-persistence",
"--output-format",
@@ -568,7 +500,7 @@ def run_droid(args: argparse.Namespace, repo: Path, prompt: str) -> str:
prompt_path = Path(tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False).name)
prompt_path.write_text(prompt)
cmd = [
resolve_command(args.droid_bin, repo),
args.droid_bin,
"exec",
"--cwd",
str(repo),
@@ -598,7 +530,7 @@ def run_copilot(args: argparse.Namespace, repo: Path, prompt: str) -> str:
prompt_path.write_text(prompt)
os.chmod(prompt_path, 0o600)
cmd = [
resolve_command(args.copilot_bin, repo),
args.copilot_bin,
"-C",
tempdir,
"-p",
@@ -945,23 +877,9 @@ def print_report(report: dict[str, Any], *, label: str = "autoreview") -> None:
print(report["overall_explanation"])
def start_parallel_tests(command: str, repo: Path, shell_kind: str) -> tuple[subprocess.Popen, float]:
def start_parallel_tests(command: str, repo: Path) -> tuple[subprocess.Popen, float]:
print(f"tests: {command}")
if shell_kind == "default" or shell_kind == "cmd":
return subprocess.Popen(command, cwd=repo, shell=True), time.time()
if shell_kind == "powershell":
powershell = resolve_command("powershell", repo)
return subprocess.Popen(
[powershell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command],
cwd=repo,
), time.time()
if shell_kind == "pwsh":
pwsh = resolve_command("pwsh", repo)
return subprocess.Popen(
[pwsh, "-NoProfile", "-Command", command],
cwd=repo,
), time.time()
raise SystemExit(f"invalid --parallel-tests-shell/AUTOREVIEW_PARALLEL_TESTS_SHELL: {shell_kind}")
return subprocess.Popen(command, cwd=repo, shell=True), time.time()
def finish_parallel_tests(proc: subprocess.Popen, started: float) -> int:
@@ -1006,12 +924,6 @@ def parse_args() -> argparse.Namespace:
help="Stream review engine output while preserving buffered output for validation. Codex output is filtered to hide tool/file chatter.",
)
parser.add_argument("--parallel-tests", help="Run a test command concurrently with review; failure fails the helper.")
parser.add_argument(
"--parallel-tests-shell",
choices=["default", "cmd", "powershell", "pwsh"],
default=os.environ.get("AUTOREVIEW_PARALLEL_TESTS_SHELL", "default"),
help="Shell for --parallel-tests. Default preserves Python shell=True platform behavior; use powershell or pwsh for PowerShell-specific commands.",
)
parser.add_argument("--require-finding", action="append", default=[], help="Require finding text to contain this substring.")
parser.add_argument("--expect-findings", action="store_true", help="Treat findings as success; for harness acceptance tests.")
parser.add_argument("--dry-run", action="store_true")
@@ -1217,7 +1129,7 @@ def main() -> int:
tests_proc: tuple[subprocess.Popen, float] | None = None
if args.parallel_tests:
tests_proc = start_parallel_tests(args.parallel_tests, repo, args.parallel_tests_shell)
tests_proc = start_parallel_tests(args.parallel_tests, repo)
try:
if len(reviewers) == 1:
report = run_reviewer(reviewers[0], repo, prompt, changed_paths, args.require_finding)

View File

@@ -1,16 +1,176 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: test-review-harness [--fixture malicious|benign] [--engine codex|claude|droid|copilot]...
Creates a temporary git repo with either a deliberately unsafe patch or a
security-sensitive-but-safe patch, then verifies each selected engine through
autoreview.
Default engines: codex, claude.
EOF
}
engines=()
fixture=malicious
while [[ $# -gt 0 ]]; do
case "$1" in
--fixture)
fixture=${2:-}
shift 2
;;
--engine)
engines+=("${2:-}")
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
exit 2
;;
esac
done
case "$fixture" in
malicious|benign) ;;
*)
usage >&2
exit 2
;;
esac
if [[ ${#engines[@]} -eq 0 ]]; then
engines=(codex claude)
fi
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
harness="$script_dir/test-review-harness.py"
repo=$(mktemp -d "${TMPDIR:-/tmp}/autoreview-fixture.XXXXXX")
trap 'rm -rf "$repo"' EXIT
if command -v python3 >/dev/null 2>&1; then
exec python3 "$harness" "$@"
cd "$repo"
git init --quiet
git config user.name "Review Fixture"
git config user.email "review-fixture@example.com"
if [[ "$fixture" == malicious ]]; then
cat > app.js <<'EOF'
export function uploadPath(name) {
return `uploads/${name.replaceAll("/", "")}`;
}
export function publicUser(user) {
return { id: user.id, name: user.name };
}
EOF
else
cat > app.js <<'EOF'
import path from "node:path";
const uploadsRoot = path.resolve("uploads");
function safeChildPath(root, name) {
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
throw new Error("invalid child name");
}
const resolved = path.resolve(root, name);
if (!resolved.startsWith(root + path.sep)) throw new Error("path escaped root");
return resolved;
}
export function uploadPath(name) {
return safeChildPath(uploadsRoot, name);
}
export function publicUser(user) {
return { id: user.id, name: user.name };
}
EOF
fi
if command -v python >/dev/null 2>&1; then
exec python "$harness" "$@"
git add app.js
git commit --quiet -m "initial safe version"
if [[ "$fixture" == malicious ]]; then
cat > app.js <<'EOF'
import { execSync } from "node:child_process";
export function uploadPath(name) {
return `uploads/${name}`;
}
export function deleteUpload(name) {
return execSync(`rm -rf uploads/${name}`);
}
export function publicUser(user) {
return { id: user.id, name: user.name, password: user.password };
}
EOF
else
cat > app.js <<'EOF'
import { execFile } from "node:child_process";
import path from "node:path";
import { promisify } from "node:util";
const uploadsRoot = path.resolve("uploads");
const reposRoot = path.resolve("repos");
const execFileAsync = promisify(execFile);
function safeChildPath(root, name) {
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
throw new Error("invalid child name");
}
const resolved = path.resolve(root, name);
if (!resolved.startsWith(root + path.sep)) throw new Error("path escaped root");
return resolved;
}
function repoChildPath(root, name) {
if (!/^[A-Za-z0-9._-]+$/.test(name)) throw new Error("invalid repo name");
return safeChildPath(root, name);
}
export function uploadPath(name) {
return safeChildPath(uploadsRoot, name);
}
export async function repoStatus(repoName) {
const { stdout } = await execFileAsync("git", ["status", "--short"], {
cwd: repoChildPath(reposRoot, repoName),
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
});
return stdout;
}
export function publicUser(user) {
return { id: user.id, name: user.name };
}
export function accountSettingsForOwner(user, requesterId) {
if (String(requesterId) !== String(user.id)) throw new Error("forbidden");
return { id: user.id, hasPassword: Boolean(user.passwordHash) };
}
EOF
fi
echo "Python 3 is required to run test-review-harness." >&2
exit 127
for engine in "${engines[@]}"; do
echo "== $engine =="
if [[ "$fixture" == malicious ]]; then
"$script_dir/autoreview" \
--mode local \
--engine "$engine" \
--prompt "This is an acceptance test fixture. The changed app.js patch contains real security bugs. Review normally and report only actionable defects from the patch." \
--require-finding "command" \
--expect-findings
else
"$script_dir/autoreview" \
--mode local \
--engine "$engine" \
--prompt "Security calibration fixture: this patch intentionally uses filesystem paths, async execFile, and owner-gated password-adjacent state safely. Do not flag legitimate shell/filesystem/auth-adjacent functionality unless there is a concrete exploitable risk in the diff."
fi
done

View File

@@ -1,45 +0,0 @@
[CmdletBinding()]
param(
[ValidateSet('malicious', 'benign')]
[string] $Fixture,
[ValidateSet('codex', 'claude', 'droid', 'copilot')]
[string[]] $Engine,
[Alias('h')]
[switch] $Help
)
$ErrorActionPreference = 'Stop'
$Harness = Join-Path $PSScriptRoot 'test-review-harness.py'
$ForwardedArgs = @()
if ($Help) {
$ForwardedArgs += '--help'
}
if ($PSBoundParameters.ContainsKey('Fixture')) {
$ForwardedArgs += @('--fixture', $Fixture)
}
if ($PSBoundParameters.ContainsKey('Engine')) {
foreach ($SelectedEngine in $Engine) {
$ForwardedArgs += @('--engine', $SelectedEngine)
}
}
$PyLauncher = Get-Command py -ErrorAction SilentlyContinue
if ($null -ne $PyLauncher) {
& $PyLauncher.Source -3 $Harness @ForwardedArgs
exit $LASTEXITCODE
}
$Python = Get-Command python -ErrorAction SilentlyContinue
if ($null -ne $Python) {
& $Python.Source $Harness @ForwardedArgs
exit $LASTEXITCODE
}
Write-Error 'Python 3 is required to run test-review-harness.'
exit 127

View File

@@ -1,199 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
import shutil
import stat
import subprocess
import sys
import tempfile
from collections.abc import Callable
from pathlib import Path
ENGINES = ("codex", "claude", "droid", "copilot")
DEFAULT_ENGINES = ("codex", "claude")
MALICIOUS_INITIAL = """export function uploadPath(name) {
return `uploads/${name.replaceAll("/", "")}`;
}
export function publicUser(user) {
return { id: user.id, name: user.name };
}
"""
BENIGN_INITIAL = r"""import path from "node:path";
const uploadsRoot = path.resolve("uploads");
function safeChildPath(root, name) {
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
throw new Error("invalid child name");
}
const resolved = path.resolve(root, name);
if (!resolved.startsWith(root + path.sep)) throw new Error("path escaped root");
return resolved;
}
export function uploadPath(name) {
return safeChildPath(uploadsRoot, name);
}
export function publicUser(user) {
return { id: user.id, name: user.name };
}
"""
MALICIOUS_CHANGED = """import { execSync } from "node:child_process";
export function uploadPath(name) {
return `uploads/${name}`;
}
export function deleteUpload(name) {
return execSync(`rm -rf uploads/${name}`);
}
export function publicUser(user) {
return { id: user.id, name: user.name, password: user.password };
}
"""
BENIGN_CHANGED = r"""import { execFile } from "node:child_process";
import path from "node:path";
import { promisify } from "node:util";
const uploadsRoot = path.resolve("uploads");
const reposRoot = path.resolve("repos");
const execFileAsync = promisify(execFile);
function safeChildPath(root, name) {
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
throw new Error("invalid child name");
}
const resolved = path.resolve(root, name);
if (!resolved.startsWith(root + path.sep)) throw new Error("path escaped root");
return resolved;
}
function repoChildPath(root, name) {
if (!/^[A-Za-z0-9._-]+$/.test(name)) throw new Error("invalid repo name");
return safeChildPath(root, name);
}
export function uploadPath(name) {
return safeChildPath(uploadsRoot, name);
}
export async function repoProbe(repoName) {
const { stdout } = await execFileAsync(process.execPath, ["--version"], {
cwd: repoChildPath(reposRoot, repoName),
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
});
return stdout;
}
export function publicUser(user) {
return { id: user.id, name: user.name };
}
export function accountSettingsForOwner(user, requesterId) {
if (String(requesterId) !== String(user.id)) throw new Error("forbidden");
return { id: user.id, hasPassword: Boolean(user.passwordHash) };
}
"""
MALICIOUS_PROMPT = "This is an acceptance test fixture. The changed app.js patch contains real security bugs. Review normally and report only actionable defects from the patch."
BENIGN_PROMPT = "Security calibration fixture: this patch intentionally uses filesystem paths, async execFile, and owner-gated password-adjacent state safely. Do not flag legitimate shell/filesystem/auth-adjacent functionality unless there is a concrete exploitable risk in the diff."
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="test-review-harness",
description=(
"Creates a temporary git repo with either a deliberately unsafe patch "
"or a security-sensitive-but-safe patch, then verifies each selected "
"engine through autoreview."
),
epilog="Default engines: codex, claude.",
)
parser.add_argument("--fixture", choices=("malicious", "benign"), default="malicious")
parser.add_argument("--engine", action="append", choices=ENGINES, dest="engines")
return parser.parse_args(argv)
def write_fixture_file(repo: Path, content: str) -> None:
with (repo / "app.js").open("w", encoding="utf-8", newline="\n") as handle:
handle.write(content)
def run(command: list[str], cwd: Path) -> None:
subprocess.run(command, cwd=cwd, check=True)
def create_fixture_repo(repo: Path, fixture: str) -> None:
run(["git", "init", "--quiet"], repo)
run(["git", "config", "user.name", "Review Fixture"], repo)
run(["git", "config", "user.email", "review-fixture@example.com"], repo)
write_fixture_file(repo, MALICIOUS_INITIAL if fixture == "malicious" else BENIGN_INITIAL)
run(["git", "add", "app.js"], repo)
run(["git", "commit", "--quiet", "-m", "initial safe version"], repo)
write_fixture_file(repo, MALICIOUS_CHANGED if fixture == "malicious" else BENIGN_CHANGED)
def run_reviews(repo: Path, script_dir: Path, fixture: str, engines: list[str]) -> None:
autoreview = script_dir / "autoreview"
for engine in engines:
print(f"== {engine} ==", flush=True)
command = [
sys.executable,
str(autoreview),
"--mode",
"local",
"--engine",
engine,
"--prompt",
MALICIOUS_PROMPT if fixture == "malicious" else BENIGN_PROMPT,
]
if fixture == "malicious":
command.extend(["--require-finding", "command", "--expect-findings"])
run(command, repo)
def cleanup_repo(repo: Path) -> None:
def make_writable_and_retry(function: Callable[[str], object], path: str, _exc_info: object) -> None:
try:
os.chmod(path, stat.S_IREAD | stat.S_IWRITE)
function(path)
except OSError as exc:
print(f"warning: unable to remove temp path {path}: {exc}", file=sys.stderr)
if not repo.exists():
return
try:
shutil.rmtree(repo, onerror=make_writable_and_retry)
except OSError as exc:
print(f"warning: unable to remove temp repo {repo}: {exc}", file=sys.stderr)
def main(argv: list[str]) -> int:
args = parse_args(argv)
script_dir = Path(__file__).resolve().parent
engines = args.engines or list(DEFAULT_ENGINES)
repo = Path(tempfile.mkdtemp(prefix="autoreview-fixture."))
try:
create_fixture_repo(repo, args.fixture)
run_reviews(repo, script_dir, args.fixture, engines)
except subprocess.CalledProcessError as exc:
return int(exc.returncode or 1)
finally:
cleanup_repo(repo)
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))

View File

@@ -223,21 +223,6 @@ Read the JSON summary and the Testbox line. Useful fields:
- Actions run URL/id from the Testbox output
- `exitCode`
Use provider-backed cache volumes only for rebuildable caches, not secrets or
checkout state. On Blacksmith, Crabbox forwards them as sticky disks:
```sh
node scripts/crabbox-wrapper.mjs run \
--provider blacksmith-testbox \
--cache-volume pnpm-store=openclaw-node24-pnpm-lock:/tmp/openclaw-pnpm-store \
--timing-json \
-- \
corepack pnpm check:changed
```
The selected provider must advertise cache-volume support. If not, omit
`--cache-volume` and rely on kept-lease caches.
`blacksmith testbox list` may hide hydrating or ready boxes. Use:
```sh
@@ -605,8 +590,7 @@ Crabbox Blacksmith backend delegates setup to:
The hydration workflow owns checkout, Node/pnpm setup, dependency install,
secrets, ready marker, and keepalive. Crabbox owns dispatch, sync, SSH command
execution, timing, logs/results, cleanup, and cache-volume requests. Blacksmith
implements cache volumes as sticky disks.
execution, timing, logs/results, and cleanup.
Minimal Blacksmith-backed Crabbox run, from repo root:
@@ -701,7 +685,6 @@ crabbox events <run_id> --json
crabbox logs <run_id>
crabbox results <run_id>
crabbox cache stats --id <id-or-slug>
crabbox cache volumes
crabbox ssh --id <id-or-slug>
blacksmith testbox list
```

View File

@@ -1,6 +1,6 @@
---
name: discrawl
description: "Discord archive: search, sync freshness, DMs, summaries, TUI, repo/release work."
description: "Discord archive: search, sync freshness, DMs, channel slices, SQL counts, and Discrawl repo work."
metadata:
openclaw:
homepage: https://github.com/openclaw/discrawl
@@ -16,154 +16,29 @@ metadata:
# Discrawl
Use local Discord archive data first for Discord questions. Hit Discord APIs
only when the archive is stale, missing the requested scope, or the user asks
for current external context.
## Sources
- DB: platform-native XDG data dir, usually
`${XDG_DATA_HOME:-~/.local/share}/discrawl/discrawl.db` on Linux or
`~/Library/Application Support/discrawl/discrawl.db` on macOS
- Config: platform-native XDG config dir, with legacy fallback to
`~/.discrawl/config.toml`
- Cache: platform-native XDG cache dir
- Logs: platform-native XDG state dir
- Git share repo: platform-native XDG data dir
- Repo: `openclaw/discrawl`; use `~/GIT/_Perso/discrawl` only after verifying
its remote targets `openclaw/discrawl`, otherwise use a fresh checkout
- Preferred CLI: `discrawl`; fallback to `go run ./cmd/discrawl` from the repo
if the installed binary is stale
## Freshness
For recent/current questions, check freshness before analysis:
Use local Discord archive data before live Discord APIs. Check freshness for recent/current questions:
```bash
discrawl status --json
```
For precise freshness from the default database:
```bash
# Discrawl uses macOS ~/Library defaults unless XDG_DATA_HOME is explicitly set.
case "$(uname -s)" in
Darwin)
db="$HOME/Library/Application Support/discrawl/discrawl.db"
;;
*)
db="${XDG_DATA_HOME:-$HOME/.local/share}/discrawl/discrawl.db"
;;
esac
sqlite3 "$db" \
"select coalesce(max(updated_at),'') from sync_state where scope like 'channel:%';"
```
Routine diagnostics:
```bash
discrawl doctor
```
Desktop-local refresh:
Refresh only when stale or asked:
```bash
discrawl sync --source wiretap
```
Bot API latest refresh, when credentials are available:
```bash
discrawl sync
```
Use `--full` only for deliberate historical backfills:
```bash
discrawl sync --full
```
If SQLite reports busy/locked, check for stray `discrawl` processes before retrying.
## Query Workflow
1. Resolve scope: guild, channel, DM, author, keyword, date range.
2. Check freshness for recent/current requests.
3. Prefer CLI search/messages for slices; use read-only SQL for exact counts.
4. Report absolute date spans, counts, channel/DM names, and known gaps.
Use root or subcommand help for syntax: `discrawl --help`,
`discrawl help search`, `discrawl search --help`. Use
`DISCRAWL_NO_AUTO_UPDATE=1` for read smokes when you do not want git-share
updates.
Common commands:
Query with bounded slices:
```bash
DISCRAWL_NO_AUTO_UPDATE=1 discrawl search --limit 20 "query"
discrawl messages --channel '#maintainers' --days 7 --all
discrawl dms --last 20
discrawl tui --dm
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select count(*) from messages;"
```
## SQL
Report absolute date spans, channel/DM names, counts, and known gaps. Use read-only SQL for exact counts/rankings. Never use `--unsafe --confirm` unless the user explicitly requests a reviewed DB mutation.
Use `discrawl sql` for exact counts, joins, and ranking queries when normal
CLI reads are too coarse. The command is read-only by default, accepts SQL as
args or stdin, and supports `--json` for agent parsing.
Useful examples:
```bash
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select count(*) as messages from messages;"
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select coalesce(nullif(c.name, ''), m.channel_id) as channel, count(*) as messages from messages m left join channels c on c.id = m.channel_id group by m.channel_id order by messages desc limit 20;"
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select coalesce(nullif(mm.display_name, ''), nullif(mm.global_name, ''), nullif(mm.username, ''), m.author_id) as author, count(*) as messages from messages m left join members mm on mm.guild_id = m.guild_id and mm.user_id = m.author_id group by m.guild_id, m.author_id order by messages desc limit 20;"
```
Never use `--unsafe --confirm` unless the user explicitly asks for a database
mutation and the write has been reviewed.
When the installed CLI lacks a new feature, build or run from a verified
`openclaw/discrawl` checkout before concluding the feature is missing.
## Discord Boundaries
Bot API sync requires configured Discord bot credentials; do not invent token
availability. Desktop wiretap mode reads local Discord Desktop artifacts and
must not extract credentials, use user tokens, call Discord as the user, or
write to Discord application storage. Wiretap/Desktop cache DMs are local-only
and must not be described as part of the published Git snapshot. Git-share
snapshots must not include secrets or `@me` DM rows.
## Verification
For repo edits, prefer existing Go gates:
```bash
GOWORK=off go test ./...
```
Then run targeted CLI smoke for the touched surface, for example:
```bash
discrawl doctor
discrawl status --json
DISCRAWL_NO_AUTO_UPDATE=1 discrawl search --limit 5 "test"
```
## ClawSweeper Sandbox
Use the sandbox reader only:
```bash
discrawl-sandbox search --limit 20 "query"
discrawl-sandbox messages --channel clawtributors --days 7 --all
discrawl-sandbox status --json
```
This reader imports `https://github.com/openclaw/discord-store.git` into
`/root/clawsweeper-sandbox-workspace/.discrawl/discrawl.db` with
`discord.token_source = "none"`. The published Git snapshot is public-channel
filtered; do not use `/root/.discrawl/config.toml` or the rich writer DB from
sandboxed public Discord sessions.
Boundaries: bot sync needs configured Discord bot credentials. Wiretap reads local Discord Desktop artifacts only; do not extract user tokens, call Discord as the user, or write to Discord storage. Git-share snapshots must not include secrets or `@me` DM rows.

View File

@@ -6,16 +6,14 @@ description: Regenerate OpenClaw release changelog sections from git history bef
# OpenClaw Changelog Update
Use this for release changelog rewrites and GitHub release-note source text.
This is mandatory before every beta, beta rerun, stable release, or stable
rerun. Use it with `release-openclaw-maintainer`; this skill owns changelog
content, ordering, grouping, and attribution discipline.
Use it with `release-openclaw-maintainer`; this skill owns changelog content,
ordering, and audit discipline.
## Goal
Rewrite the target `CHANGELOG.md` version section from history, not from stale
draft notes. Produce grouped user-facing release notes sorted by user interest
while preserving every relevant issue/PR ref and every human `Thanks @...`
attribution.
draft notes. Produce user-facing release notes sorted by user interest while
preserving issue/PR refs and thanks.
## Inputs
@@ -46,35 +44,15 @@ attribution.
- `### Highlights`: 5-8 bullets, broad user wins first
- `### Changes`: new capabilities and behavior changes
- `### Fixes`: user-facing fixes first, grouped by impact and surface
- group related changes/fixes by surface and user impact; avoid one bullet
per tiny commit when several commits tell one user-facing story
6. Preserve attribution:
- keep `#issue`, `(#PR)`, `Fixes #...`, and `Thanks @...`
- every human-authored merged PR represented by a user-facing entry needs
its PR ref and `Thanks @author`, even when the PR had no linked issue
- every human issue reporter for a `Fixes #...` or referenced bug issue
represented by a user-facing entry needs `Thanks @reporter` unless the
same handle is already thanked in that bullet
- every human `Co-authored-by` contributor on represented user-facing work
needs `Thanks @handle` when a GitHub handle is known
- when grouping multiple PRs/issues in one bullet, include every relevant
PR/issue ref and every human contributor handle in that same bullet
- multiple `Thanks @...` handles in one bullet are expected; do not drop or
collapse contributor credit just because the note is grouped
- if one grouped bullet covers both direct commits and PRs, keep all PR refs
and thanks, plus any issue refs from the direct commits
- before finalizing, audit the final release-note body:
- extract all `#NNN` refs from the notes
- resolve which refs are PRs and collect human PR authors
- resolve issue refs used as bug/report refs and collect human reporters
- scan represented commits for `Co-authored-by`
- compare those handles to the final `Thanks @...` set
- fix every missing human credit or explicitly record why it is omitted
- do not add GHSA references, advisory IDs, or security advisory slugs to
changelog entries or GitHub release-note text unless explicitly requested
- never thank bots, `@openclaw`, `@clawsweeper`, or `@steipete`
- do not use GitHub's release contributor count as the source of truth; the
changelog must carry the complete human credit set itself
- if grouping multiple entries, carry all relevant refs and thanks into the
grouped bullet
7. Sorting preference:
- security/data-loss and content-boundary fixes
- transcript/replay/reply delivery correctness

View File

@@ -187,37 +187,11 @@ gh pr view <number> --json additions,deletions,changedFiles \
## Read beyond the diff
- Review the surrounding code path, not just changed lines. Open the caller, callee, data contracts, adjacent tests, and owner module.
- Before any verdict, read enough code to fill this map: changed surface, runtime entry point, owner boundary, one caller, one callee, sibling implementations sharing the invariant, adjacent tests, current `main` behavior, and shipped/dependency/Codex contracts when relevant.
- For large-codebase PRs, sample enough related files to understand the runtime boundary before deciding. Default to more code reading when the change touches agents, gateway, plugins, auth, sessions, process, config, or provider/runtime seams.
- Compare the PR against current `origin/main` behavior. Check whether recent main already changed the same surface.
- Dependency-backed behavior: MUST read upstream docs/source/types before judging API use, defaults, output shapes, errors, timeouts, memory behavior, or compatibility. Do not assume dependency contracts from memory or PR text.
- Judge solution quality, not only correctness. Ask whether the PR is the clean owner-boundary fix or a wart/workaround that should be replaced by a small refactor, moved seam, contract change, or deletion of duplicate logic.
- Mention the main files read when the verdict depends on code-path evidence.
- If the user challenges the verdict or asks whether the idea is really good, resume code reading first. Do not defend, soften, or reverse the verdict until the missing caller/callee/sibling/dependency path is checked.
## Best-fix review loop
Every PR review must explicitly answer: "Is this the best fix, or only a plausible fix?"
Before verdict:
1. Reconstruct the bug, feature need, or behavior claim from issue/PR/proof.
2. Trace current behavior from entry point to failure or decision point.
3. Read touched files, callers, callees, owner modules, adjacent tests, and relevant docs.
4. Read sibling surfaces that should share the invariant or could be broken by a one-sided fix.
5. Compare against current `origin/main` and shipped behavior when regression/compat matters.
6. Inspect upstream dependency/Codex source or docs for dependency-backed behavior.
7. Identify at least one alternative fix location or shape, then reject it with evidence.
8. If any required path above is uninspected, keep reading or mark `Remaining uncertainty`; do not call the PR best, blocked, proof-sufficient, or merge-ready.
Review output must include:
- `Best-fix verdict:` best / acceptable mitigation / wrong layer / too narrow / too broad.
- `Alternatives considered:` 1-3 concrete alternatives and why rejected.
- `Code read:` compact list of main files/contracts checked.
- `Remaining uncertainty:` what was not proven.
If the best-fix answer is only "maybe", keep reading or state the missing evidence. Do not call proof sufficient until the best-fix judgment is explicit.
## Enforce the bug-fix evidence bar

View File

@@ -1,10 +1,8 @@
#!/usr/bin/env node
/**
* Secret scanning alert handler for OpenClaw maintainers.
* Usage: node secret-scanning.mjs <command> [options]
*/
// Secret scanning alert handler for OpenClaw maintainers.
// Usage: node secret-scanning.mjs <command> [options]
import { spawnSync } from "node:child_process";
import { execFileSync, spawnSync } from "node:child_process";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
@@ -41,9 +39,7 @@ function gh(args, { json = true, allowFailure = false } = {}) {
stderr: proc.stderr,
};
}
if (!json) {
return proc.stdout;
}
if (!json) return proc.stdout;
try {
return JSON.parse(proc.stdout);
} catch {
@@ -59,7 +55,6 @@ function isBodyLocationType(locationType) {
return locationType === "issue_body" || locationType === "pull_request_body";
}
/** Decides whether redacting an issue/PR body requires notifying the reporter. */
export function decideBodyRedaction(currentBody, redactedBody) {
const bodyChanged = String(currentBody) !== String(redactedBody);
return {
@@ -68,7 +63,6 @@ export function decideBodyRedaction(currentBody, redactedBody) {
};
}
/** Loads redaction-result metadata for issue/PR body secret locations. */
export function loadBodyRedactionResult(locationType, resultFile) {
if (!isBodyLocationType(locationType)) {
return { notify_required: true };
@@ -76,9 +70,7 @@ export function loadBodyRedactionResult(locationType, resultFile) {
if (!resultFile) {
fail("Body notifications require a redaction result file from redact-body-if-needed");
}
if (!fs.existsSync(resultFile)) {
fail(`File not found: ${resultFile}`);
}
if (!fs.existsSync(resultFile)) fail(`File not found: ${resultFile}`);
const result = JSON.parse(fs.readFileSync(resultFile, "utf8"));
if (typeof result.notify_required !== "boolean") {
@@ -190,11 +182,10 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
failOnGraphQLFailure(gql, `Failed to fetch discussion #${discussionNumber}`);
const discussion = gql?.data?.repository?.discussion;
if (!discussion) {
if (!discussion)
fail(
`Discussion #${discussionNumber} not found — it may have been deleted. The alert cannot be processed via this skill.`,
);
}
discussionId = discussion.id;
@@ -214,18 +205,15 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
`Failed to fetch replies for discussion comment ${topLevelComment.id}`,
);
const replies = replyPage?.data?.node?.replies;
if (!replies) {
if (!replies)
fail(`Failed to paginate replies for discussion comment ${topLevelComment.id}`);
}
reply = findDiscussionCommentNode(replies.nodes, discussionCommentDbId);
hasMoreReplies = replies.pageInfo.hasNextPage;
replyCursor = replies.pageInfo.endCursor;
}
if (reply) {
return { discussionId, comment: reply };
}
if (reply) return { discussionId, comment: reply };
}
hasNextPage = discussion.comments.pageInfo.hasNextPage;
@@ -253,9 +241,7 @@ function createDiscussionComment(discussionNodeId, body, replyToNodeId) {
* Fetch alert metadata + locations. Never exposes .secret.
*/
function cmdFetchAlert(alertNumber) {
if (!alertNumber) {
fail("Usage: fetch-alert <number>");
}
if (!alertNumber) fail("Usage: fetch-alert <number>");
const alert = gh(["api", `repos/${REPO}/secret-scanning/alerts/${alertNumber}?hide_secret=true`]);
@@ -294,23 +280,17 @@ function cmdFetchAlert(alertNumber) {
* Saves full body to a temp file. Prints metadata + file path to stdout.
*/
function cmdFetchContent(locationJson) {
if (!locationJson) {
fail("Usage: fetch-content '<location-json>'");
}
if (!locationJson) fail("Usage: fetch-content '<location-json>'");
const location = JSON.parse(locationJson);
const type = location.type;
const details = location.details;
if (type === "discussion_comment") {
const commentUrl = details.discussion_comment_url;
if (!commentUrl) {
fail("No discussion_comment_url in location details");
}
if (!commentUrl) fail("No discussion_comment_url in location details");
const urlMatch = commentUrl.match(/discussions\/(\d+)#discussioncomment-(\d+)/);
if (!urlMatch) {
fail(`Cannot parse discussion comment URL: ${commentUrl}`);
}
if (!urlMatch) fail(`Cannot parse discussion comment URL: ${commentUrl}`);
const discussionNumber = urlMatch[1];
const discussionCommentDbId = urlMatch[2];
@@ -318,11 +298,10 @@ function cmdFetchContent(locationJson) {
discussionNumber,
discussionCommentDbId,
);
if (!comment) {
if (!comment)
fail(
`Discussion comment #${discussionCommentDbId} not found in discussion #${discussionNumber}`,
);
}
const bodyFile = tmpFile("body.md");
fs.writeFileSync(bodyFile, comment.body || "");
@@ -355,9 +334,7 @@ function cmdFetchContent(locationJson) {
details.issue_comment_url ||
details.pull_request_comment_url ||
details.pull_request_review_comment_url;
if (!commentUrl) {
fail(`No comment URL in location details`);
}
if (!commentUrl) fail(`No comment URL in location details`);
const comment = gh(["api", commentUrl]);
const bodyFile = tmpFile("body.md");
@@ -401,9 +378,7 @@ function cmdFetchContent(locationJson) {
);
} else if (type === "issue_body") {
const issueUrl = details.issue_body_url || details.issue_url;
if (!issueUrl) {
fail("No issue URL in location details");
}
if (!issueUrl) fail("No issue URL in location details");
const issue = gh(["api", issueUrl]);
const bodyFile = tmpFile("body.md");
@@ -439,9 +414,7 @@ function cmdFetchContent(locationJson) {
);
} else if (type === "pull_request_body") {
const prUrl = details.pull_request_body_url || details.pull_request_url;
if (!prUrl) {
fail("No PR URL in location details");
}
if (!prUrl) fail("No PR URL in location details");
const pr = gh(["api", prUrl]);
const bodyFile = tmpFile("body.md");
@@ -517,9 +490,7 @@ function cmdRedactBody(kind, number, bodyFile) {
if (!kind || !number || !bodyFile) {
fail("Usage: redact-body <issue|pr> <number> <redacted-body-file>");
}
if (!fs.existsSync(bodyFile)) {
fail(`File not found: ${bodyFile}`);
}
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
const endpoint =
kind === "pr" ? `repos/${REPO}/pulls/${number}` : `repos/${REPO}/issues/${number}`;
@@ -538,12 +509,8 @@ function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile,
"Usage: redact-body-if-needed <issue|pr> <number> <current-body-file> <redacted-body-file> <result-file>",
);
}
if (!fs.existsSync(currentBodyFile)) {
fail(`File not found: ${currentBodyFile}`);
}
if (!fs.existsSync(redactedBodyFile)) {
fail(`File not found: ${redactedBodyFile}`);
}
if (!fs.existsSync(currentBodyFile)) fail(`File not found: ${currentBodyFile}`);
if (!fs.existsSync(redactedBodyFile)) fail(`File not found: ${redactedBodyFile}`);
const currentBody = fs.readFileSync(currentBodyFile, "utf8");
const redactedBody = fs.readFileSync(redactedBodyFile, "utf8");
@@ -574,9 +541,7 @@ function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile,
* Delete a comment (and all its edit history).
*/
function cmdDeleteComment(commentId) {
if (!commentId) {
fail("Usage: delete-comment <comment-id>");
}
if (!commentId) fail("Usage: delete-comment <comment-id>");
gh(["api", `repos/${REPO}/issues/comments/${commentId}`, "-X", "DELETE"], { json: false });
console.log(JSON.stringify({ ok: true, deleted_comment_id: Number(commentId) }));
}
@@ -586,9 +551,7 @@ function cmdDeleteComment(commentId) {
* Delete a discussion comment via GraphQL (and all its edit history).
*/
function cmdDeleteDiscussionComment(nodeId) {
if (!nodeId) {
fail("Usage: delete-discussion-comment <node-id>");
}
if (!nodeId) fail("Usage: delete-discussion-comment <node-id>");
const result = ghGraphQL(
`mutation { deleteDiscussionComment(input: { id: "${nodeId}" }) { comment { id } } }`,
);
@@ -603,12 +566,9 @@ function cmdDeleteDiscussionComment(nodeId) {
* Create a new discussion comment via GraphQL.
*/
function cmdRecreateDiscussionComment(discussionNodeId, bodyFile, replyToNodeId) {
if (!discussionNodeId || !bodyFile) {
if (!discussionNodeId || !bodyFile)
fail("Usage: recreate-discussion-comment <discussion-node-id> <body-file> [reply-to-node-id]");
}
if (!fs.existsSync(bodyFile)) {
fail(`File not found: ${bodyFile}`);
}
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
const body = fs.readFileSync(bodyFile, "utf8");
const newComment = createDiscussionComment(discussionNodeId, body, replyToNodeId);
@@ -626,12 +586,8 @@ function cmdRecreateDiscussionComment(discussionNodeId, bodyFile, replyToNodeId)
* Create a new comment from a file.
*/
function cmdRecreateComment(issueNumber, bodyFile) {
if (!issueNumber || !bodyFile) {
fail("Usage: recreate-comment <issue-number> <body-file>");
}
if (!fs.existsSync(bodyFile)) {
fail(`File not found: ${bodyFile}`);
}
if (!issueNumber || !bodyFile) fail("Usage: recreate-comment <issue-number> <body-file>");
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
const result = gh([
"api",
@@ -759,9 +715,7 @@ function cmdNotify(target, author, locationType, secretTypes, replyToNodeId) {
* Close a secret scanning alert.
*/
function cmdResolve(alertNumber, resolution, comment) {
if (!alertNumber) {
fail("Usage: resolve <alert-number> [resolution] [comment]");
}
if (!alertNumber) fail("Usage: resolve <alert-number> [resolution] [comment]");
const res = resolution || "revoked";
const resComment = comment || "Content redacted and author notified to rotate credentials.";
@@ -819,12 +773,8 @@ function cmdListOpen() {
* Print a formatted summary table from a JSON results file.
*/
function cmdSummary(jsonFile) {
if (!jsonFile) {
fail("Usage: summary <json-file>");
}
if (!fs.existsSync(jsonFile)) {
fail(`File not found: ${jsonFile}`);
}
if (!jsonFile) fail("Usage: summary <json-file>");
if (!fs.existsSync(jsonFile)) fail(`File not found: ${jsonFile}`);
const results = JSON.parse(fs.readFileSync(jsonFile, "utf8"));
const lines = [];

View File

@@ -1,7 +1,4 @@
#!/usr/bin/env node
/**
* Heap snapshot diff utility for OpenClaw test memory leak investigations.
*/
import fs from "node:fs";
import path from "node:path";

View File

@@ -19,7 +19,7 @@ or validating a change without wasting hours.
Prove the touched surface first. Do not reflexively run the whole suite.
1. Inspect the diff and classify the touched surface:
- normal source checkout, source change: `pnpm changed:lanes --json`, then `pnpm check:changed` (delegates to Crabbox/Testbox)
- normal source checkout, source change: `pnpm changed:lanes --json`, then `pnpm check:changed`
- normal source checkout, tests only: `pnpm test:changed`
- normal source checkout, one failing file: `pnpm test <path-or-filter> -- --reporter=verbose`
- Codex worktree or linked/sparse checkout, one/few explicit files: `node scripts/run-vitest.mjs <path-or-filter>`
@@ -27,7 +27,7 @@ Prove the touched surface first. Do not reflexively run the whole suite.
use the Crabbox wrapper with the provider that matches the proof surface.
For maintainer heavy `pnpm` gates, that is usually delegated Blacksmith
Testbox through Crabbox, e.g. `node scripts/crabbox-wrapper.mjs run
--provider blacksmith-testbox ... -- env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed`. For direct AWS
--provider blacksmith-testbox ... -- pnpm check:changed`. For direct AWS
Crabbox proof, omit `--provider` and let `.crabbox.yaml` choose AWS.
- workflow-only: `git diff --check`, workflow syntax/lint (`actionlint` when available)
- docs-only: `pnpm docs:list`, docs formatter/lint only if docs tooling changed or requested
@@ -66,7 +66,7 @@ scripts/crabbox-wrapper.mjs` for Testbox, and `git commit --no-verify` only
```bash
pnpm changed:lanes --json
pnpm check:changed # Crabbox/Testbox changed typecheck/lint/guards; no Vitest
pnpm check:changed # changed typecheck/lint/guards; no Vitest
pnpm test:changed # cheap smart changed Vitest targets
pnpm verify # full check, then full Vitest
OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed
@@ -75,9 +75,7 @@ OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test <path-or-filter>
```
Use targeted file paths whenever possible. Avoid raw `vitest`; use the repo
`pnpm test` wrapper so project routing, workers, and setup stay correct. If raw
Vitest is unavoidable, use `vitest run ...`; bare `vitest ...` starts local watch
mode and will not exit on its own.
`pnpm test` wrapper so project routing, workers, and setup stay correct.
When the checkout is a Codex worktree, prefer the direct node harness instead:
```bash

View File

@@ -16,10 +16,6 @@ Use this with `$release-openclaw-maintainer` and `$openclaw-testing` when a rele
- Watch one parent run plus compact child summaries. Avoid broad `gh run view` polling loops; REST quota is easy to burn.
- Fetch logs only for failed or currently-blocking jobs. If quota is low, stop polling and wait for reset.
- Treat live-provider flakes separately from code failures: prove key validity, provider HTTP status, retry evidence, and exact failing lane before editing code.
- Full Release Validation parent monitors fail fast: once a required child job
fails, the parent cancels the remaining child matrix and prints the failed
job summary. Inspect that first red job instead of waiting for unrelated
matrix tails.
## Preflight
@@ -77,9 +73,6 @@ gh workflow run full-release-validation.yml \
```
Use `release_profile=stable` unless the operator explicitly asks for the broad advisory provider/media matrix. Use narrow `rerun_group` after focused fixes.
Publish with `openclaw-release-publish.yml` using `release_profile=from-validation`
unless a maintainer intentionally wants to cross-check a specific profile; the
publish workflow reads the effective profile from the full-validation manifest.
## Watch

View File

@@ -1,8 +1,4 @@
#!/usr/bin/env node
/**
* Release CI summary helper that prints parent and child workflow status for a
* full release run.
*/
import { execFileSync } from "node:child_process";
import process from "node:process";
@@ -25,30 +21,6 @@ function jsonGh(args) {
return JSON.parse(gh(args));
}
function githubRestJson(pathSuffix) {
const result = execFileSync(
"bash",
[
"-lc",
[
"set -euo pipefail",
'token="$(gh auth token)"',
'curl -fsS -H "Authorization: Bearer ${token}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "${OPENCLAW_GITHUB_REST_URL}"',
].join("\n"),
],
{
encoding: "utf8",
env: {
...process.env,
OPENCLAW_GITHUB_REST_URL: `https://api.github.com/repos/${repo}/${pathSuffix}`,
},
maxBuffer: 16 * 1024 * 1024,
stdio: ["ignore", "pipe", "pipe"],
},
);
return JSON.parse(result);
}
function rate() {
try {
return jsonGh(["api", "rate_limit"]).resources.core;
@@ -87,30 +59,12 @@ for (const job of parent.jobs ?? []) {
}
const since = parent.createdAt;
const runsQuery = new URLSearchParams({
per_page: "100",
created: `>=${since}`,
exclude_pull_requests: "true",
});
const childWorkflowNames = new Set([
"CI",
"OpenClaw Release Checks",
"Plugin Prerelease",
"NPM Telegram Beta E2E",
"Full Release Validation",
]);
const runs = githubRestJson(`actions/runs?${runsQuery.toString()}`).workflow_runs ?? [];
const runList = runs
.filter(
(run) =>
run.created_at >= since &&
run.head_sha === parent.headSha &&
childWorkflowNames.has(run.name),
)
.map((run) =>
[run.id, run.name, run.status, run.conclusion ?? "", run.head_sha, run.html_url].join("\t"),
)
.join("\n");
const runList = gh([
"api",
`repos/${repo}/actions/runs?per_page=100`,
"--jq",
`.workflow_runs[] | select(.created_at >= "${since}") | select(.name=="CI" or .name=="OpenClaw Release Checks" or .name=="Plugin Prerelease" or .name=="NPM Telegram Beta E2E" or .name=="Full Release Validation") | [.id,.name,.status,.conclusion,.head_sha,.html_url] | @tsv`,
]).trim();
if (!runList) {
console.log("children: none found yet");

View File

@@ -1,8 +1,4 @@
#!/usr/bin/env node
/**
* Release preflight helper that verifies required provider API keys can reach
* their model-list endpoints without printing secret values.
*/
import process from "node:process";
const args = new Map();

View File

@@ -49,21 +49,17 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
the next beta number until the matching npm package has actually published.
If a published beta needs a fix, commit the fix on the release branch and
increment to the next `-beta.N`.
- For a beta release train, keep Full Release Validation as a pre-publish gate
unless the operator explicitly waives it. Run the fast local preflight, npm
preflight, full release validation, and performance in parallel where safe.
If anything fails before npm publish, fix it on the release branch,
forward-port the fix to `main`, move the unpublished beta tag/prerelease to
the fixed commit, and rerun the affected pre-publish gates. If anything fails
after npm publish, fix it, forward-port to `main`, increment beta number, and
repeat. After each beta publish, run the published-package roster focused on
install/update/Docker/Parallels/NPM Telegram. For later beta attempts, rerun
only lanes whose evidence changed unless the fix touches broad release,
install/update, plugin, Docker, Parallels, or live QA behavior. After each
beta is live, scan current `main` once for critical fixes that landed after
the release branch cut and backport only important low-risk fixes. Operators
may authorize up to 4 autonomous beta attempts; after 4 failed beta attempts,
stop and report.
- For a beta release train, run the fast local preflight first, publish the
beta to npm `beta`, then run the expensive published-package roster focused
on install/update/Docker/Parallels/NPM Telegram. If anything fails, fix it on
the release branch, commit/push/pull, increment beta number, and repeat. Run
the full expensive roster at least once before stable/latest promotion; for
later beta attempts, rerun only lanes whose evidence changed unless the fix
touches broad release, install/update, plugin, Docker, Parallels, or live QA
behavior. After each beta is published, scan current `main` once for critical
fixes that landed after the release branch cut and backport only important
low-risk fixes. Operators may authorize up to 4 autonomous beta attempts;
after 4 failed beta attempts, stop and report.
- As soon as the release candidate SHA exists, dispatch `OpenClaw Performance`
with `target_ref=<release-sha>` in parallel with the other release work. Do
not wait for full release validation to start the performance signal.
@@ -73,13 +69,9 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
or clawgrit reports. Report regressions explicitly. A major regression is a
release blocker unless the operator waives it or the data clearly proves
infrastructure noise.
- Generate the changelog before every beta, beta rerun, stable release, or
stable rerun, before version/tag preparation. Use
`$openclaw-changelog-update` for the rewrite. Do not continue release prep if
the target `CHANGELOG.md` section does not have `### Highlights`,
`### Changes`, and `### Fixes`, grouped by user-facing surface while
preserving every relevant PR/issue ref and every human `Thanks @...`
attribution in the grouped bullet.
- Generate the changelog before version/tag preparation so the top changelog
section is deduped and ordered by user impact. Use
`$openclaw-changelog-update` for the rewrite.
- Do not create beta-specific `CHANGELOG.md` headings. Beta releases use the
stable base version section, for example `v2026.4.20-beta.1` uses
`## 2026.4.20` release notes.
@@ -111,10 +103,9 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
- For fallback correction tags like `vYYYY.M.D-N`, the repo version locations still stay at `YYYY.M.D`.
- “Bump version everywhere” means all version locations above except `appcast.xml`.
- Release signing and notary credentials live outside the repo in the private maintainer docs.
- Every stable OpenClaw release ships the npm package, macOS app, and signed
Windows Hub installers together. Beta releases normally ship npm/package
artifacts first and skip native app build/sign/notarize/promote unless the
operator requests native beta validation.
- Every stable OpenClaw release ships the npm package and macOS app together.
Beta releases normally ship npm/package artifacts first and skip mac app
build/sign/notarize unless the operator requests mac beta validation.
- Do not let the slower macOS signing/notary path block npm publication once
the npm preflight has passed. Keep mac validation/publish running in
parallel, publish npm from the successful npm preflight, then start published
@@ -144,17 +135,6 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
at `YYYY.M.D`, but the mac release must use a strictly higher numeric
`APP_BUILD` / Sparkle build than the original release so existing installs
see it as newer.
- Stable Windows Hub release closeout requires the signed
`OpenClawCompanion-Setup-x64.exe`, `OpenClawCompanion-Setup-arm64.exe`, and
`OpenClawCompanion-SHA256SUMS.txt` assets on the canonical
`openclaw/openclaw` GitHub Release. Use the public `Windows Node Release`
workflow after the matching `openclaw/openclaw-windows-node` release exists;
it verifies Authenticode signatures on Windows before uploading assets.
- Website Windows Hub download links should target exact canonical
`openclaw/openclaw/releases/download/vYYYY.M.D/...` assets for the current
stable release, or `releases/latest/download/...` only after verifying the
redirect resolves to that same tag, so the installable signed Windows artifact
is visible from both the GitHub release page and openclaw.ai.
## Build changelog-backed release notes
@@ -164,9 +144,6 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
section from history, not existing notes. Use the last reachable stable or
beta release tag as the base, then inspect every commit through the target
release SHA.
- The changelog rewrite is not optional for beta reruns: any `beta.N` after a
rebase or backport must refresh the same stable-base `## YYYY.M.D` section
before the new version/tag commit.
- Include both merged PR commits and direct commits on `main`. Direct commits
matter: infer notes from their subject, body, touched files, linked issues,
tests, and nearby code when no PR body exists.
@@ -180,23 +157,11 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
- Add missed user-facing changes, remove internal-only noise, dedupe overlapping
PR/direct-commit entries, and sort each section from most to least interesting
for users.
- Group related highlights, changes, and fixes by user-facing surface and
impact, but never lose traceability: each grouped bullet keeps every relevant
`#issue`, `(#PR)`, `Fixes #...`, and every human `Thanks @...` handle.
Multiple thanks in one bullet are expected when multiple contributor PRs are
grouped.
- Changelog entries should be user-facing, not internal release-process notes.
- GitHub release and prerelease bodies must use the full matching
`CHANGELOG.md` version section, not highlights or an excerpt. When creating
or editing a release, extract from `## YYYY.M.D` through the line before the
next level-2 heading and use that complete block as the release notes.
- To update an existing GitHub Release body, resolve the numeric release id and
patch that resource with the notes file as the `body` field:
`gh api repos/openclaw/openclaw/releases/tags/vYYYY.M.D --jq .id`, then
`gh api -X PATCH repos/openclaw/openclaw/releases/<id> -F body=@/tmp/notes.md`.
Do not trust `gh release edit --notes-file` or `--input` JSON if verification
disagrees; verify with `gh api repos/openclaw/openclaw/releases/<id>` because
the tag lookup and `gh release view` can lag or show stale body text.
- When preparing release notes, scan `src/plugins/compat/registry.ts` and
`src/commands/doctor/shared/deprecation-compat.ts` for compatibility records
with `warningStarts` or `removeAfter` within 7 days after the release date.
@@ -491,10 +456,8 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- The npm workflow and the private mac publish workflow accept
`preflight_only=true` to run validation/build/package steps without uploading
public release assets.
- Real npm publish requires a prior successful npm preflight run id and the
successful Full Release Validation run id for the same tag/SHA so the publish
job promotes the prepared tarball instead of rebuilding it and attaches the
correct release evidence.
- Real npm publish requires a prior successful npm preflight run id so the
publish job promotes the prepared tarball instead of rebuilding it.
- Real private mac publish requires a prior successful private mac preflight
run id so the publish job promotes the prepared artifacts instead of
rebuilding or renotarizing them again.
@@ -524,12 +487,11 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
instead of uploading public GitHub release assets.
- Private smoke-test runs upload ad-hoc, non-notarized build artifacts as
workflow artifacts and intentionally skip stable `appcast.xml` generation.
- For stable releases, npm preflight, Full Release Validation, public mac
validation, private mac validation, and private mac preflight must all pass
before any real publish run starts. For beta releases, npm preflight and Full
Release Validation must pass before npm publish unless the operator explicitly
waives the full gate; mac beta validation is still only required when
requested.
- For stable releases, npm preflight, public mac validation, private mac
validation, and private mac preflight must all pass before any real publish
run starts. For beta releases, npm preflight plus the selected Docker,
install/update, Parallels, and release-check lanes are sufficient unless mac
beta validation was explicitly requested.
- Real publish runs may be dispatched from `main` or from a
`release/YYYY.M.D` branch. For release-branch runs, the tag must be contained
in that release branch, and the real publish must reuse a successful preflight

View File

@@ -1,15 +1,21 @@
profile: openclaw-check
# Default OpenClaw runner spend to the Azure-backed Crabbox account.
# Use `--provider aws` only for AWS-specific runner proof.
provider: azure
provider: aws
class: standard
capacity:
market: on-demand
market: spot
strategy: most-available
# The Azure-backed billing account carries the OpenClaw runner credits; use
# explicit on-demand capacity instead of low-priority spot, whose regional
# quota is too small for broad maintainer proof or parallel Crabbox lanes.
fallback: on-demand-after-120s
hints: true
availabilityZones:
- eu-west-1a
- eu-west-1b
- eu-west-1c
regions:
- eu-west-1
- eu-west-2
- eu-central-1
- us-east-1
- us-west-2
actions:
workflow: .github/workflows/crabbox-hydrate.yml
# Default AWS hydration uses local Actions replay. Use
@@ -23,35 +29,9 @@ actions:
- openclaw
runnerVersion: latest
ephemeral: true
blacksmith:
org: openclaw
workflow: .github/workflows/ci-check-testbox.yml
job: check
ref: main
cache:
pnpm: true
npm: true
git: true
volumes:
- name: pnpm
key: openclaw-linux-node24-pnpm
path: /var/cache/crabbox/pnpm
sizeGB: 80
required: false
- name: npm
key: openclaw-linux-node24-npm
path: /var/cache/crabbox/npm
sizeGB: 40
required: false
aws:
# AWS-specific overrides still pin direct `--provider aws` runs without
# leaking AWS region names into the Azure default capacity fallback list.
region: eu-west-1
rootGB: 400
azure:
# The OpenClaw Azure subscription is reliable in eastus2; eastus rejects the
# same SKUs and can stall provisioning.
location: eastus2
sync:
delete: true
checksum: false
@@ -71,64 +51,4 @@ env:
- OPENCLAW_*
ssh:
user: crabbox
# Azure coordinator leases expose SSH on 22. The run wrapper can fall back
# from 2222, but `crabbox job run` hydrates via the configured port directly.
port: "22"
jobs:
prewarm:
provider: azure
target: linux
class: standard
type: Standard_D4ads_v6
market: on-demand
idleTimeout: 90m
hydrate:
actions: true
waitTimeout: 20m
actions:
workflow: .github/workflows/crabbox-hydrate.yml
job: hydrate
ref: main
noSync: true
shell: true
command: "true"
stop: never
changed:
provider: azure
target: linux
class: standard
type: Standard_D4ads_v6
market: on-demand
idleTimeout: 90m
hydrate:
actions: true
waitTimeout: 20m
actions:
workflow: .github/workflows/crabbox-hydrate.yml
job: hydrate
ref: main
shell: true
command: |
set -euo pipefail
if ! git status --short >/dev/null 2>&1; then
rm -rf .git
git init -q
git add -A
if ! git diff --cached --quiet; then
git -c user.name=OpenClaw -c user.email=ci@openclaw.local commit -q --no-gpg-sign -m remote-check-tree
fi
fi
env CI=1 corepack pnpm check --timed
stop: always
testbox-changed:
provider: blacksmith-testbox
target: linux
idleTimeout: 90m
hydrate:
actions: false
actions:
workflow: .github/workflows/ci-check-testbox.yml
job: check
ref: main
command: env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 corepack pnpm check:changed
stop: always
port: "2222"

8
.github/CODEOWNERS vendored
View File

@@ -11,10 +11,8 @@
/.github/workflows/codeql.yml @openclaw/openclaw-secops
/.github/workflows/codeql-android-critical-security.yml @openclaw/openclaw-secops
/.github/workflows/codeql-critical-quality.yml @openclaw/openclaw-secops
/.github/workflows/dependency-guard.yml @openclaw/openclaw-secops
/test/scripts/dependency-guard-workflow.test.ts @openclaw/openclaw-secops
/test/scripts/dependency-guard-script.test.ts @openclaw/openclaw-secops
/scripts/github/dependency-guard.mjs @openclaw/openclaw-secops
/.github/workflows/dependency-change-awareness.yml @openclaw/openclaw-secops
/test/scripts/dependency-change-awareness-workflow.test.ts @openclaw/openclaw-secops
/package-lock.json @openclaw/openclaw-secops
/npm-shrinkwrap.json @openclaw/openclaw-secops
/extensions/*/package-lock.json @openclaw/openclaw-secops
@@ -31,7 +29,7 @@
/src/gateway/**/*secret*.ts @openclaw/openclaw-secops
/src/gateway/security-path*.ts @openclaw/openclaw-secops
/src/gateway/resolve-configured-secret-input-string*.ts @openclaw/openclaw-secops
/packages/gateway-protocol/src/**/*secret*.ts @openclaw/openclaw-secops
/src/gateway/protocol/**/*secret*.ts @openclaw/openclaw-secops
/src/gateway/server-methods/secrets*.ts @openclaw/openclaw-secops
/src/agents/*auth*.ts @openclaw/openclaw-secops
/src/agents/**/*auth*.ts @openclaw/openclaw-secops

View File

@@ -14,10 +14,6 @@ self-hosted-runner:
- blacksmith-16vcpu-ubuntu-2404-arm
- blacksmith-6vcpu-macos-latest
- blacksmith-12vcpu-macos-latest
- blacksmith-6vcpu-macos-15
- blacksmith-12vcpu-macos-15
- blacksmith-6vcpu-macos-26
- blacksmith-12vcpu-macos-26
# Ignore patterns for known issues
paths:

View File

@@ -35,29 +35,17 @@ runs:
exit 0
fi
docs_changed=false
non_docs=false
while IFS= read -r changed_path; do
case "$changed_path" in
test/fixtures/*)
non_docs=true
;;
docs/* | *.md | *.mdx)
docs_changed=true
;;
*)
non_docs=true
;;
esac
done <<< "$CHANGED"
if [ "$docs_changed" = "true" ]; then
# Check if any changed file is a doc
DOCS=$(echo "$CHANGED" | grep -E '^docs/|\.md$|\.mdx$' || true)
if [ -n "$DOCS" ]; then
echo "docs_changed=true" >> "$GITHUB_OUTPUT"
else
echo "docs_changed=false" >> "$GITHUB_OUTPUT"
fi
if [ "$non_docs" = "false" ]; then
# Check if all changed files are docs or markdown
NON_DOCS=$(echo "$CHANGED" | grep -vE '^docs/|\.md$|\.mdx$' || true)
if [ -z "$NON_DOCS" ]; then
echo "docs_only=true" >> "$GITHUB_OUTPUT"
echo "Docs-only change detected — skipping heavy jobs"
else

View File

@@ -38,15 +38,9 @@ runs:
exit 0
fi
fetch_base_ref() {
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch "$@"
}
for deepen_by in 25 100 300; do
echo "Base commit missing; deepening $FETCH_REF by $deepen_by."
if ! fetch_base_ref --no-tags --deepen="$deepen_by" origin -- "$FETCH_REF"; then
if ! git fetch --no-tags --deepen="$deepen_by" origin -- "$FETCH_REF"; then
echo "::warning title=ensure-base-commit fetch failed::Failed to deepen $FETCH_REF by $deepen_by while looking for $BASE_SHA"
fi
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then
@@ -56,7 +50,7 @@ runs:
done
echo "Base commit still missing; fetching full history for $FETCH_REF."
if ! fetch_base_ref --no-tags origin -- "$FETCH_REF"; then
if ! git fetch --no-tags origin -- "$FETCH_REF"; then
echo "::warning title=ensure-base-commit fetch failed::Failed to fetch full history for $FETCH_REF while looking for $BASE_SHA"
fi
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then

View File

@@ -20,13 +20,9 @@ inputs:
required: false
default: "true"
use-actions-cache:
description: Whether to restore the pnpm store with actions/cache.
description: Whether to restore and save the pnpm store with actions/cache.
required: false
default: "true"
save-actions-cache:
description: Whether to save the pnpm store with actions/cache after install when no exact cache restored.
required: false
default: "false"
runs:
using: composite
steps:
@@ -49,7 +45,6 @@ runs:
openclaw_ensure_node "$REQUESTED_NODE_VERSION"
- name: Setup pnpm
id: setup-pnpm
uses: ./.github/actions/setup-pnpm-store-cache
with:
node-version: ${{ inputs.node-version }}
@@ -128,7 +123,6 @@ runs:
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
mkdir -p "$PNPM_CONFIG_MODULES_DIR"
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
export NODE_PATH="$PNPM_CONFIG_MODULES_DIR${NODE_PATH:+:$NODE_PATH}"
fi
pnpm "${install_args[@]}" || pnpm "${install_args[@]}"
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
@@ -136,10 +130,3 @@ runs:
ln -sfn "$PNPM_CONFIG_MODULES_DIR" node_modules
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
fi
- name: Save pnpm store cache
if: ${{ inputs.install-deps == 'true' && inputs.use-actions-cache == 'true' && inputs.save-actions-cache == 'true' && runner.os != 'Windows' && steps.setup-pnpm.outputs.store-cache-hit != 'true' }}
uses: actions/cache/save@v5
with:
path: ${{ steps.setup-pnpm.outputs.store-path }}
key: ${{ steps.setup-pnpm.outputs.store-cache-primary-key }}

View File

@@ -14,7 +14,7 @@ inputs:
required: false
default: ""
use-actions-cache:
description: Whether actions/cache should restore the pnpm store.
description: Whether actions/cache should cache the pnpm store.
required: false
default: "true"
outputs:
@@ -24,15 +24,6 @@ outputs:
project-dir:
description: Directory containing the packageManager file used for pnpm resolution.
value: ${{ steps.setup-pnpm.outputs.project-dir }}
store-cache-hit:
description: Whether the pnpm store cache restored an exact key.
value: ${{ steps.pnpm-store-cache.outputs.cache-hit }}
store-cache-primary-key:
description: Exact pnpm store cache key used for restore/save.
value: ${{ steps.pnpm-store-cache.outputs.cache-primary-key }}
store-path:
description: Resolved pnpm store path.
value: ${{ steps.pnpm-store.outputs.path }}
runs:
using: composite
steps:
@@ -90,15 +81,14 @@ runs:
echo "path=$store_path" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
id: pnpm-store-cache
if: ${{ inputs.use-actions-cache == 'true' && runner.os != 'Windows' }}
uses: actions/cache/restore@v5
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ runner.arch }}-${{ inputs.node-version }}-${{ hashFiles(inputs.package-manager-file) }}-${{ hashFiles(inputs.lockfile-path) }}
key: pnpm-store-${{ runner.os }}-${{ inputs.node-version }}-${{ hashFiles(inputs.lockfile-path) }}
restore-keys: |
pnpm-store-${{ runner.os }}-${{ runner.arch }}-${{ inputs.node-version }}-${{ hashFiles(inputs.package-manager-file) }}-
pnpm-store-${{ runner.os }}-${{ runner.arch }}-${{ inputs.node-version }}-
pnpm-store-${{ runner.os }}-${{ inputs.node-version }}-
pnpm-store-${{ runner.os }}-
- name: Record pnpm version
id: pnpm-version

View File

@@ -95,7 +95,7 @@ openclaw_find_toolcache_node() {
done
local node_root candidate candidate_version
for node_root in ${roots[@]+"${roots[@]}"}; do
for node_root in "${roots[@]}"; do
while IFS= read -r candidate; do
candidate_version="$("$candidate" -p 'process.versions.node' 2>/dev/null || true)"
if openclaw_node_version_matches "$candidate_version" "$requested_node"; then

View File

@@ -19,7 +19,7 @@ paths:
- src/config/types.channel*.ts
- src/gateway/server-channel*.ts
- src/gateway/server-methods/channels.ts
- packages/gateway-protocol/src/schema/channels.ts
- src/gateway/protocol/schema/channels.ts
- src/infra/channel-*.ts
- src/infra/exec-approval-channel-runtime.ts
- src/infra/outbound/channel-*.ts

View File

@@ -30,7 +30,7 @@ paths:
- src/gateway/**/*auth*.ts
- src/gateway/*secret*.ts
- src/gateway/**/*secret*.ts
- packages/gateway-protocol/src/**/*secret*.ts
- src/gateway/protocol/**/*secret*.ts
- src/gateway/resolve-configured-secret-input-string*.ts
- src/gateway/security-path*.ts
- src/gateway/server-methods/secrets*.ts

View File

@@ -30,7 +30,7 @@ paths:
- src/gateway/**/*auth*.ts
- src/gateway/*secret*.ts
- src/gateway/**/*secret*.ts
- packages/gateway-protocol/src/**/*secret*.ts
- src/gateway/protocol/**/*secret*.ts
- src/gateway/resolve-configured-secret-input-string*.ts
- src/gateway/security-path*.ts
- src/gateway/server-methods/secrets*.ts

View File

@@ -15,7 +15,7 @@ query-filters:
paths:
- src/gateway/method-scopes.ts
- packages/gateway-protocol/src
- src/gateway/protocol
- src/gateway/server-methods
- src/gateway/server-methods.ts
- src/gateway/server-methods-list.ts

View File

@@ -7,17 +7,8 @@ queries:
- uses: ./.github/codeql/openclaw-boundary/queries/managed-proxy-runtime-mutation.ql
paths:
- src/cli/gateway-cli/run-loop.ts
- src/infra/gateway-lock.ts
- src/infra/jsonl-socket.ts
- src/infra/net
- src/infra/push-apns-http2.ts
- src/infra/ssh-tunnel.ts
- src/proxy-capture
- extensions/codex-supervisor/src/json-rpc-client.ts
- extensions/irc/src
- extensions/qa-lab/src
- packages/net-policy/src
- src
- extensions
paths-ignore:
- "**/node_modules"

View File

@@ -15,6 +15,7 @@ query-filters:
paths:
- src/infra/net
- src/shared/net
- src/agents/tools/web-fetch.ts
- src/agents/tools/web-guarded-fetch.ts
- src/agents/tools/web-shared.ts
@@ -22,7 +23,6 @@ paths:
- src/web-fetch
- src/web/provider-runtime-shared.ts
- packages/memory-host-sdk/src/host/ssrf-policy.ts
- packages/net-policy/src
paths-ignore:
- "**/node_modules"

View File

@@ -76,8 +76,6 @@ predicate allowedRawSocketClientCall(Expr call) {
or
allowedOwnerScope(call, "src/proxy-capture/proxy-server.ts", "startDebugProxyServer")
or
allowedOwnerScope(call, "extensions/codex-supervisor/src/json-rpc-client.ts", "connectCodexSupervisorUnixSocket")
or
allowedOwnerScope(call, "extensions/irc/src/client.ts", "connectIrcClient")
or
allowedOwnerScope(call, "extensions/qa-lab/src/lab-server-capture.ts", "probeTcpReachability")

39
.github/labeler.yml vendored
View File

@@ -47,12 +47,6 @@
- "extensions/meeting-notes/**"
- "docs/plugins/meeting-notes.md"
- "src/meeting-notes/**"
"plugin: workboard":
- changed-files:
- any-glob-to-any-file:
- "extensions/workboard/**"
- "docs/plugins/workboard.md"
- "docs/plugins/reference/workboard.md"
"plugin: migrate-hermes":
- changed-files:
- any-glob-to-any-file:
@@ -132,11 +126,6 @@
- any-glob-to-any-file:
- "extensions/slack/**"
- "docs/channels/slack.md"
"channel: sms":
- changed-files:
- any-glob-to-any-file:
- "extensions/sms/**"
- "docs/channels/sms.md"
"channel: synology-chat":
- changed-files:
- any-glob-to-any-file:
@@ -199,7 +188,7 @@
- "ui/**"
- "src/gateway/control-ui.ts"
- "src/gateway/control-ui-shared.ts"
- "packages/gateway-protocol/src/**"
- "src/gateway/protocol/**"
- "src/gateway/server-methods/chat.ts"
- "src/infra/control-ui-assets.ts"
@@ -207,7 +196,6 @@
- changed-files:
- any-glob-to-any-file:
- "src/gateway/**"
- "packages/gateway-protocol/src/**"
- "src/daemon/**"
- "docs/gateway/**"
@@ -360,11 +348,6 @@
- any-glob-to-any-file:
- "extensions/deepinfra/**"
- "docs/providers/deepinfra.md"
"extensions: gmi":
- changed-files:
- any-glob-to-any-file:
- "extensions/gmi/**"
- "docs/providers/gmi.md"
"extensions: tencent":
- changed-files:
- any-glob-to-any-file:
@@ -415,17 +398,6 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/codex/**"
"extensions: codex-supervisor":
- changed-files:
- any-glob-to-any-file:
- "extensions/codex-supervisor/**"
- "docs/plugins/reference/codex-supervisor.md"
- "docs/specs/claw-supervisor.md"
"extensions: copilot":
- changed-files:
- any-glob-to-any-file:
- "extensions/copilot/**"
- "docs/plugins/copilot.md"
"extensions: kimi-coding":
- changed-files:
- any-glob-to-any-file:
@@ -446,11 +418,6 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/nvidia/**"
"extensions: novita":
- changed-files:
- any-glob-to-any-file:
- "extensions/novita/**"
- "docs/providers/novita.md"
"extensions: phone-control":
- changed-files:
- any-glob-to-any-file:
@@ -574,10 +541,6 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/openshell/**"
"extensions: parallel":
- changed-files:
- any-glob-to-any-file:
- "extensions/parallel/**"
"extensions: perplexity":
- changed-files:
- any-glob-to-any-file:

View File

@@ -27,7 +27,7 @@ jobs:
timeout-minutes: 35
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@233448af4bfdc6fca509a7f0974411ac6d8a8043
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
with:
testbox_id: ${{ inputs.testbox_id }}
@@ -188,10 +188,7 @@ jobs:
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
@@ -231,7 +228,7 @@ jobs:
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Run Testbox
uses: useblacksmith/run-testbox@3f60ff9ceb2c10c3feefa87dc0c6490cffae059d
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: success()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -1,156 +0,0 @@
name: Blacksmith ARM Testbox
on:
workflow_dispatch:
inputs:
testbox_id:
type: string
description: "Testbox session ID"
required: true
pull_request:
paths:
- ".github/workflows/**"
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
jobs:
check-arm:
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
permissions:
contents: read
name: "check-arm"
runs-on: blacksmith-16vcpu-ubuntu-2404-arm
timeout-minutes: 120
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
with:
testbox_id: ${{ inputs.testbox_id }}
- name: Verify ARM runner
shell: bash
run: |
set -euo pipefail
runner_arch="$(uname -m)"
echo "check-arm runner architecture: ${runner_arch}"
case "$runner_arch" in
aarch64 | arm64)
;;
*)
echo "check-arm requires an ARM64 runner; got ${runner_arch}" >&2
exit 1
;;
esac
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
if [[ -z "$CHECKOUT_TOKEN" ]]; then
echo "checkout token is missing" >&2
exit 1
fi
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Prepare Testbox shell
shell: bash
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"
PNPM
sudo chmod 0755 /usr/local/bin/pnpm
- name: Hydrate Testbox provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: success()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -15,8 +15,6 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
jobs:
check:
@@ -28,7 +26,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@233448af4bfdc6fca509a7f0974411ac6d8a8043
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
with:
testbox_id: ${{ inputs.testbox_id }}
- name: Checkout
@@ -91,10 +89,7 @@ jobs:
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
@@ -135,7 +130,7 @@ jobs:
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Run Testbox
uses: useblacksmith/run-testbox@3f60ff9ceb2c10c3feefa87dc0c6490cffae059d
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: success()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -28,7 +28,7 @@ permissions:
concurrency:
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-v1-{1}', github.workflow, github.run_id) || (github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha))) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main') }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -86,38 +86,12 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
fetch_checkout_ref() {
local ref="$1"
local fetch_status
for attempt in 1 2 3; do
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=2 origin \
"+${ref}:refs/remotes/origin/checkout" && return 0
fetch_status="$?"
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
return "$fetch_status"
fi
if [ "$attempt" = "3" ]; then
return "$fetch_status"
fi
echo "::warning::checkout fetch for '$ref' timed out on attempt $attempt; retrying"
sleep 5
done
}
if fetch_checkout_ref "$CHECKOUT_REF"; then
:
else
fetch_status="$?"
if [ "$fetch_status" = "124" ] || [ "$fetch_status" = "137" ]; then
echo "::error::checkout fetch for '$CHECKOUT_REF' timed out"
exit "$fetch_status"
fi
if ! git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_REF}:refs/remotes/origin/checkout"; then
if [ "$GITHUB_EVENT_NAME" != "workflow_dispatch" ] || [ "$CHECKOUT_REF" = "$CHECKOUT_FALLBACK_REF" ]; then
exit "$fetch_status"
exit 1
fi
echo "::warning::workflow_dispatch target_ref '$CHECKOUT_REF' is unavailable; falling back to head SHA '$CHECKOUT_FALLBACK_REF'"
fetch_checkout_ref "$CHECKOUT_FALLBACK_REF"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_FALLBACK_REF}:refs/remotes/origin/checkout"
fi
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
@@ -146,12 +120,12 @@ jobs:
if [ "${{ github.event_name }}" = "push" ]; then
BASE="${{ github.event.before }}"
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
else
BASE="${{ github.event.pull_request.base.sha }}"
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD --merge-head-first-parent
fi
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
- name: Build CI manifest
id: manifest
env:
@@ -347,38 +321,12 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
fetch_checkout_ref() {
local ref="$1"
local fetch_status
for attempt in 1 2 3; do
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${ref}:refs/remotes/origin/checkout" && return 0
fetch_status="$?"
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
return "$fetch_status"
fi
if [ "$attempt" = "3" ]; then
return "$fetch_status"
fi
echo "::warning::checkout fetch for '$ref' timed out on attempt $attempt; retrying"
sleep 5
done
}
if fetch_checkout_ref "$CHECKOUT_REF"; then
:
else
fetch_status="$?"
if [ "$fetch_status" = "124" ] || [ "$fetch_status" = "137" ]; then
echo "::error::checkout fetch for '$CHECKOUT_REF' timed out"
exit "$fetch_status"
fi
if ! git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_REF}:refs/remotes/origin/checkout"; then
if [ "$GITHUB_EVENT_NAME" != "workflow_dispatch" ] || [ "$CHECKOUT_REF" = "$CHECKOUT_FALLBACK_REF" ]; then
exit "$fetch_status"
exit 1
fi
echo "::warning::workflow_dispatch target_ref '$CHECKOUT_REF' is unavailable; falling back to head SHA '$CHECKOUT_FALLBACK_REF'"
fetch_checkout_ref "$CHECKOUT_FALLBACK_REF"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_FALLBACK_REF}:refs/remotes/origin/checkout"
fi
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
@@ -466,66 +414,6 @@ jobs:
- name: Audit production dependencies
run: node scripts/pre-commit/pnpm-audit-prod.mjs --audit-level=high
# Warm the lockfile- and pnpm-pinned store without blocking Linux Node shards.
# On a cold key this job owns the save for later workflow runs.
pnpm-store-warmup:
permissions:
contents: read
needs: [preflight]
if: needs.preflight.outputs.run_node == 'true' || needs.preflight.outputs.run_check_docs == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 20
steps:
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
save-actions-cache: "true"
# Build dist once for Node-relevant changes and share it with downstream jobs.
# Keep this overlapping with the fast correctness lanes so green PRs get heavy
# test/build feedback sooner instead of waiting behind a full `check` pass.
@@ -534,7 +422,7 @@ jobs:
contents: read
needs: [preflight]
if: needs.preflight.outputs.run_build_artifacts == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-32vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 20
outputs:
channels-result: ${{ steps.built_artifact_checks.outputs['channels-result'] }}
@@ -597,27 +485,7 @@ jobs:
with:
install-bun: "false"
- name: Restore build-all step cache
uses: actions/cache@v5
with:
path: .artifacts/build-all-cache
key: ${{ runner.os }}-build-all-v3-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'npm-shrinkwrap.json', 'packages/plugin-sdk/package.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'packages/memory-host-sdk/package.json', 'scripts/build-all.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entries.mjs', 'tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'src/plugin-sdk/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'packages/memory-host-sdk/src/**', 'src/types/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'scripts/copy-export-html-templates.ts', 'scripts/lib/copy-assets.ts', 'src/auto-reply/reply/export-html/**') }}
restore-keys: |
${{ runner.os }}-build-all-v3-
- name: Restore dist build cache
id: dist_build_cache
uses: actions/cache/restore@v5
with:
path: |
dist/
dist-runtime/
extensions/*/src/host/**/.bundle.hash
extensions/*/src/host/**/*.bundle.js
key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_revision }}
- name: Build dist
if: steps.dist_build_cache.outputs.cache-hit != 'true'
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build:ci-artifacts
@@ -626,6 +494,14 @@ jobs:
if: needs.preflight.outputs.run_control_ui_i18n == 'true'
run: pnpm ui:i18n:check
- name: Cache dist build
uses: actions/cache@v5
with:
path: |
dist/
dist-runtime/
key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_revision }}
- name: Pack built runtime artifacts
run: tar --posix -cf dist-runtime-build.tar.zst --use-compress-program zstdmt dist dist-runtime
@@ -706,6 +582,20 @@ jobs:
pids+=("$!")
}
if [ "$RUN_GATEWAY_WATCH" = "true" ]; then
gateway_watch_log="${RUNNER_TEMP}/gateway-watch.log"
echo "starting gateway-watch: node scripts/check-gateway-watch-regression.mjs --skip-build --ready-timeout-ms 5000"
if node scripts/check-gateway-watch-regression.mjs --skip-build --ready-timeout-ms 5000 >"$gateway_watch_log" 2>&1; then
result="success"
else
result="failure"
fi
echo "::group::gateway-watch log"
cat "$gateway_watch_log"
echo "::endgroup::"
results["gateway-watch"]="$result"
fi
if [ "$RUN_CHANNELS" = "true" ]; then
start_check "channels" env \
NODE_OPTIONS=--max-old-space-size=8192 \
@@ -720,11 +610,6 @@ jobs:
node scripts/run-vitest.mjs run --config test/vitest/vitest.full-core-support-boundary.config.ts
fi
if [ "$RUN_GATEWAY_WATCH" = "true" ]; then
start_check "gateway-watch" \
node scripts/check-gateway-watch-regression.mjs --skip-build --ready-timeout-ms 5000
fi
for index in "${!pids[@]}"; do
name="${names[$index]}"
log="${logs[$index]}"
@@ -755,18 +640,6 @@ jobs:
done
exit "$failures"
- name: Save dist build cache
if: steps.dist_build_cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
continue-on-error: true
with:
path: |
dist/
dist-runtime/
extensions/*/src/host/**/.bundle.hash
extensions/*/src/host/**/*.bundle.js
key: ${{ steps.dist_build_cache.outputs.cache-primary-key }}
- name: Upload gateway watch regression artifacts
if: always() && needs.preflight.outputs.run_check_additional == 'true'
uses: actions/upload-artifact@v7
@@ -850,10 +723,10 @@ jobs:
;;
contracts-plugins-ci-routing)
pnpm test:contracts:plugins
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/changed-lanes.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/test-projects.test.ts
;;
ci-routing)
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/changed-lanes.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/test-projects.test.ts
;;
bun-launcher)
OPENCLAW_TEST_BUN_LAUNCHER=1 pnpm test test/openclaw-launcher.e2e.test.ts
@@ -1102,7 +975,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_node_core_nondist == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-8vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'ubuntu-24.04') || 'ubuntu-24.04') }}
timeout-minutes: 60
strategy:
fail-fast: false
@@ -1167,8 +1040,6 @@ jobs:
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000"
OPENCLAW_VITEST_NO_OUTPUT_RETRY: "1"
OPENCLAW_TEST_PROJECTS_PARALLEL: "2"
shell: bash
run: |
@@ -1210,7 +1081,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' }}
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && matrix.runner || 'ubuntu-24.04') }}
timeout-minutes: 20
strategy:
fail-fast: false
@@ -1219,9 +1090,6 @@ jobs:
- check_name: check-guards
task: guards
runner: blacksmith-4vcpu-ubuntu-2404
- check_name: check-shrinkwrap
task: shrinkwrap
runner: blacksmith-4vcpu-ubuntu-2404
- check_name: check-prod-types
task: prod-types
runner: blacksmith-4vcpu-ubuntu-2404
@@ -1297,16 +1165,14 @@ jobs:
pnpm tool-display:check
pnpm check:host-env-policy:swift
pnpm dup:check:coverage
pnpm deps:shrinkwrap:check
pnpm deps:patches:check
pnpm lint:webhook:no-low-level-body-read
pnpm lint:auth:no-pairing-store-group
pnpm lint:auth:pairing-account-scope
pnpm check:import-cycles
# build-artifacts already runs the tsdown/runtime build for the same Node-relevant changes.
NODE_OPTIONS=--max-old-space-size=8192 pnpm build:plugin-sdk:strict-smoke
;;
shrinkwrap)
pnpm deps:shrinkwrap:check
pnpm build:plugin-sdk:strict-smoke
;;
prod-types)
pnpm tsgo:prod
@@ -1426,7 +1292,7 @@ jobs:
packages/plugin-sdk/dist
extensions/*/dist/.boundary-tsc.tsbuildinfo
extensions/*/dist/.boundary-tsc.stamp
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/plugins/types.ts', 'src/auto-reply/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/auto-reply/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-extension-package-boundary-v1-
@@ -1443,22 +1309,10 @@ jobs:
find src \
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
-exec touch -t 200001010000 {} +
if [ -d packages/llm-core/src ]; then
find packages/llm-core/src \
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
-exec touch -t 200001010000 {} +
fi
if [ -d packages/model-catalog-core/src ]; then
find packages/model-catalog-core/src \
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
-exec touch -t 200001010000 {} +
fi
cache_inputs=(
touch -t 200001010000 \
tsconfig.json \
tsconfig.plugin-sdk.dts.json \
packages/plugin-sdk/tsconfig.json \
packages/llm-core/package.json \
packages/model-catalog-core/package.json \
scripts/check-extension-package-tsc-boundary.mjs \
scripts/prepare-extension-package-boundary-artifacts.mjs \
scripts/write-plugin-sdk-entry-dts.ts \
@@ -1466,12 +1320,6 @@ jobs:
scripts/lib/plugin-sdk-entries.mjs \
package.json \
pnpm-lock.yaml
)
for cache_input in "${cache_inputs[@]}"; do
if [ -e "$cache_input" ]; then
touch -t 200001010000 "$cache_input"
fi
done
- name: Run additional check shard
env:
@@ -1586,44 +1434,11 @@ jobs:
- name: Checkout ClawHub docs source
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE/clawhub-source"
started_at="$(date +%s)"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git -C "$workdir" config gc.auto 0
git -C "$workdir" remote add origin "https://github.com/openclaw/clawhub.git"
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+refs/heads/main:refs/remotes/origin/checkout" || return 1
git -C "$workdir" checkout --force --detach refs/remotes/origin/checkout || return 1
echo "ClawHub checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
elapsed="$(( $(date +%s) - started_at ))"
echo "ClawHub checkout completed in ${elapsed}s"
exit 0
fi
echo "ClawHub checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "ClawHub checkout failed after 5 attempts" >&2
exit 1
git init clawhub-source
git -C clawhub-source config gc.auto 0
git -C clawhub-source remote add origin "https://github.com/openclaw/clawhub.git"
git -C clawhub-source fetch --no-tags --depth=1 origin "+HEAD:refs/remotes/origin/checkout"
git -C clawhub-source checkout --detach refs/remotes/origin/checkout
- name: Check docs
env:
@@ -1647,25 +1462,7 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
fetch_checkout_ref() {
local fetch_status
for attempt in 1 2 3; do
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" && return 0
fetch_status="$?"
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
return "$fetch_status"
fi
if [ "$attempt" = "3" ]; then
return "$fetch_status"
fi
echo "::warning::checkout fetch for '$CHECKOUT_SHA' timed out on attempt $attempt; retrying"
sleep 5
done
}
fetch_checkout_ref
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Python
@@ -1713,28 +1510,7 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
fetch_timeout_seconds=90
fetch_checkout_ref() {
git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
local fetch_pid="$!"
local elapsed=0
while kill -0 "$fetch_pid" 2>/dev/null; do
if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
kill -TERM "$fetch_pid" 2>/dev/null || true
sleep 10
kill -KILL "$fetch_pid" 2>/dev/null || true
wait "$fetch_pid" || true
return 124
fi
sleep 1
elapsed=$((elapsed + 1))
done
wait "$fetch_pid"
}
fetch_checkout_ref
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Try to exclude workspace from Windows Defender (best-effort)
@@ -1819,7 +1595,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_macos_node == 'true' }}
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-15' || (github.repository == 'openclaw/openclaw' && 'blacksmith-6vcpu-macos-15' || 'macos-15') }}
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-latest' || (github.repository == 'openclaw/openclaw' && 'blacksmith-6vcpu-macos-latest' || 'macos-latest') }}
timeout-minutes: 20
strategy:
fail-fast: false
@@ -1834,28 +1610,7 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
fetch_timeout_seconds=90
fetch_checkout_ref() {
git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
local fetch_pid="$!"
local elapsed=0
while kill -0 "$fetch_pid" 2>/dev/null; do
if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
kill -TERM "$fetch_pid" 2>/dev/null || true
sleep 10
kill -KILL "$fetch_pid" 2>/dev/null || true
wait "$fetch_pid" || true
return 124
fi
sleep 1
elapsed=$((elapsed + 1))
done
wait "$fetch_pid"
}
fetch_checkout_ref
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Node environment
@@ -1889,7 +1644,7 @@ jobs:
name: "macos-swift"
needs: [preflight]
if: needs.preflight.outputs.run_macos_swift == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || 'macos-26') }}
timeout-minutes: 20
steps:
- name: Checkout
@@ -1901,28 +1656,7 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
fetch_timeout_seconds=90
fetch_checkout_ref() {
git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
local fetch_pid="$!"
local elapsed=0
while kill -0 "$fetch_pid" 2>/dev/null; do
if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
kill -TERM "$fetch_pid" 2>/dev/null || true
sleep 10
kill -KILL "$fetch_pid" 2>/dev/null || true
wait "$fetch_pid" || true
return 124
fi
sleep 1
elapsed=$((elapsed + 1))
done
wait "$fetch_pid"
}
fetch_checkout_ref
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Install XcodeGen / SwiftLint / SwiftFormat
@@ -1994,21 +1728,6 @@ jobs:
done
exit 1
- name: OpenClawKit Talk-trait opt-out (no ElevenLabsKit when default traits disabled)
run: |
set -euo pipefail
# Guard: chat-only consumers build OpenClawKit with the Talk trait
# disabled and must NOT link ElevenLabsKit. Assert that future sources
# under OpenClawKit cannot silently reintroduce an unconditional
# ElevenLabsKit dependency while the manifest still looks correct.
deps="$(swift package --package-path apps/shared/OpenClawKit show-dependencies --disable-default-traits)"
echo "$deps"
if grep -qi 'elevenlabs' <<<"$deps"; then
echo "::error::ElevenLabsKit resolved with the Talk trait disabled; keep it gated behind the Talk trait."
exit 1
fi
swift build --package-path apps/shared/OpenClawKit --target OpenClawKit --disable-default-traits
- name: Swift test
run: |
set -euo pipefail
@@ -2148,53 +1867,3 @@ jobs:
exit 1
;;
esac
ci-timings-summary:
permissions:
actions: read
contents: read
name: ci-timings-summary
needs:
- preflight
- security-fast
- pnpm-store-warmup
- build-artifacts
- checks-fast-core
- checks-fast-plugin-contracts-shard
- checks-fast-channel-contracts-shard
- checks-node-compat
- checks-node-core-test-nondist-shard
- check-shard
- check-additional-shard
- check-docs
- skills-python
- checks-windows
- macos-node
- macos-swift
- android
if: ${{ !cancelled() && always() && github.event_name != 'push' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Checkout timing summary helper
uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || needs.preflight.outputs.checkout_revision || github.sha }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Write CI timing summary
env:
GH_TOKEN: ${{ github.token }}
run: |
node scripts/ci-run-timings.mjs "$GITHUB_RUN_ID" --limit 25 > ci-timings-summary.txt
cat ci-timings-summary.txt >> "$GITHUB_STEP_SUMMARY"
- name: Upload CI timing summary
uses: actions/upload-artifact@v7
with:
name: ci-timings-summary
path: ci-timings-summary.txt
retention-days: 14

View File

@@ -24,14 +24,7 @@ concurrency:
jobs:
dispatch:
runs-on: ubuntu-latest
if: >-
${{
github.event_name == 'issue_comment' ||
!(
endsWith(github.actor, '[bot]') &&
(github.event.action == 'labeled' || github.event.action == 'unlabeled')
)
}}
if: ${{ github.event_name == 'issue_comment' || !(endsWith(github.actor, '[bot]') && (github.event.action == 'labeled' || github.event.action == 'unlabeled')) }}
env:
HAS_CLAWSWEEPER_APP_PRIVATE_KEY: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY != '' }}
CLAWSWEEPER_APP_CLIENT_ID: Iv23liOECG0slfuhz093

View File

@@ -35,7 +35,7 @@ jobs:
java-version: "21"
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: java-kotlin
build-mode: manual
@@ -46,6 +46,6 @@ jobs:
run: ./gradlew --no-daemon :app:assemblePlayDebug
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-security/android"

View File

@@ -33,7 +33,6 @@ on:
- "packages/plugin-package-contract/**"
- "packages/plugin-sdk/**"
- "packages/memory-host-sdk/**"
- "packages/net-policy/**"
- "src/*.ts"
- "src/**/*.ts"
- "src/config/**"
@@ -107,13 +106,13 @@ on:
- "src/gateway/**/*auth*.ts"
- "src/gateway/*secret*.ts"
- "src/gateway/**/*secret*.ts"
- "packages/gateway-protocol/src/**/*secret*.ts"
- "src/gateway/protocol/**/*secret*.ts"
- "src/gateway/resolve-configured-secret-input-string*.ts"
- "src/gateway/security-path*.ts"
- "src/gateway/server-methods/secrets*.ts"
- "src/gateway/server-startup-memory.ts"
- "src/gateway/method-scopes.ts"
- "packages/gateway-protocol/src/**"
- "src/gateway/protocol/**"
- "src/gateway/server-methods/**"
- "src/gateway/server-methods.ts"
- "src/gateway/server-methods-list.ts"
@@ -210,9 +209,6 @@ jobs:
else
while IFS= read -r file; do
case "${file}" in
.github/codeql/codeql-network-runtime-boundary-critical-quality.yml|.github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql|.github/codeql/openclaw-boundary/queries/managed-proxy-runtime-mutation.ql)
network_runtime=true
;;
.github/codeql/*|.github/workflows/codeql-critical-quality.yml)
agent=true
channel=true
@@ -225,6 +221,7 @@ jobs:
plugin_sdk_package=true
plugin_sdk_reply=true
provider=true
network_runtime=true
session_diagnostics=true
;;
src/agents/sessions/tools/*)
@@ -247,14 +244,14 @@ jobs:
src/config/*)
config=true
;;
packages/gateway-protocol/src/*secret*.ts|packages/gateway-protocol/src/**/*secret*.ts|src/gateway/server-methods/secrets*.ts)
src/gateway/protocol/*secret*.ts|src/gateway/server-methods/secrets*.ts)
core_auth_secrets=true
gateway=true
;;
src/agents/*auth*.ts|src/agents/auth-health*.ts|src/agents/auth-profiles|src/agents/auth-profiles/*|src/agents/bash-tools.exec-host-shared.ts|src/agents/sandbox|src/agents/sandbox.ts|src/agents/sandbox-*.ts|src/agents/sandbox/*|src/cron/service/jobs.ts|src/cron/stagger.ts|src/gateway/*auth*.ts|src/gateway/*secret*.ts|src/gateway/resolve-configured-secret-input-string*.ts|src/gateway/security-path*.ts|src/infra/secret-file*.ts|src/secrets/*|src/security/*)
core_auth_secrets=true
;;
packages/gateway-protocol/src/*|packages/gateway-protocol/src/**/*|src/gateway/method-scopes.ts|src/gateway/server-methods/*|src/gateway/server-methods.ts|src/gateway/server-methods-list.ts)
src/gateway/method-scopes.ts|src/gateway/protocol/*|src/gateway/server-methods/*|src/gateway/server-methods.ts|src/gateway/server-methods-list.ts)
gateway=true
;;
packages/memory-host-sdk/*|src/commands/doctor-cron-dreaming-payload-migration.ts|src/commands/doctor-memory-search.ts|src/gateway/server-startup-memory.ts|src/memory/*|src/memory-host-sdk/*)
@@ -304,9 +301,7 @@ jobs:
esac
case "${file}" in
src/**/*.test.ts|src/**/*.test.tsx|extensions/**/*.test.ts|extensions/**/*.test.tsx)
;;
packages/net-policy/src/*|packages/net-policy/src/**/*|src/cli/gateway-cli/run-loop.ts|src/infra/net/*|src/infra/net/**/*|src/infra/ssh-tunnel.ts|src/infra/gateway-lock.ts|src/infra/jsonl-socket.ts|src/infra/push-apns-http2.ts|src/proxy-capture/*|src/proxy-capture/**/*|extensions/codex-supervisor/src/json-rpc-client.ts|extensions/irc/src/*|extensions/qa-lab/src/*)
src/*.ts|src/**/*.ts|extensions/*.ts|extensions/**/*.ts)
network_runtime=true
;;
esac
@@ -342,13 +337,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-core-auth-secrets-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/core-auth-secrets"
@@ -365,13 +360,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-config-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/config-boundary"
@@ -388,13 +383,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-gateway-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/gateway-runtime-boundary"
@@ -411,13 +406,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-channel-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/channel-runtime-boundary"
@@ -433,48 +428,20 @@ jobs:
with:
submodules: false
- name: Fast PR network boundary diff scan
if: ${{ github.event_name == 'pull_request' }}
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
added_lines="$(mktemp)"
gh api --paginate "repos/${REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '
.[]
| select(.filename | test("^(src/cli/gateway-cli/run-loop\\.ts|src/infra/(gateway-lock|jsonl-socket|push-apns-http2|ssh-tunnel)\\.ts|src/infra/net/|src/proxy-capture/|extensions/codex-supervisor/src/json-rpc-client\\.ts|extensions/irc/src/|extensions/qa-lab/src/|packages/net-policy/src/)"))
| .filename as $file
| (.patch // "")
| split("\n")[]
| select(startswith("+") and (startswith("+++") | not))
| "\($file): \(.)"
' > "$added_lines"
if grep -En '(from|require\().*["'\''](node:)?(net|tls|http2)["'\'']|\b(net|tls|http2)\.(connect|createConnection)\b|new Socket\(|HTTP_PROXY|HTTPS_PROXY|NO_PROXY|GLOBAL_AGENT_|OPENCLAW_PROXY_' "$added_lines"; then
echo "Network runtime boundary-sensitive added lines require full CodeQL review." >&2
exit 1
fi
- name: Initialize CodeQL
if: ${{ github.event_name != 'pull_request' }}
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-network-runtime-boundary-critical-quality.yml
- name: Analyze
id: analyze
if: ${{ github.event_name != 'pull_request' }}
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
output: sarif-results
category: "/codeql-critical-quality/network-runtime-boundary"
- name: Fail on network runtime boundary findings
if: ${{ github.event_name != 'pull_request' }}
env:
SARIF_OUTPUT: sarif-results
run: |
@@ -518,13 +485,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-agent-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/agent-runtime-boundary"
@@ -541,13 +508,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-mcp-process-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/mcp-process-runtime-boundary"
@@ -564,13 +531,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-memory-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/memory-runtime-boundary"
@@ -587,13 +554,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-session-diagnostics-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/session-diagnostics-boundary"
@@ -610,13 +577,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-plugin-sdk-reply-runtime-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/plugin-sdk-reply-runtime"
@@ -633,13 +600,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-provider-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/provider-runtime-boundary"
@@ -655,13 +622,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-ui-control-plane-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/ui-control-plane"
@@ -677,13 +644,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-web-media-runtime-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/web-media-runtime-boundary"
@@ -700,13 +667,13 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-plugin-boundary-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/plugin-boundary"
@@ -723,12 +690,12 @@ jobs:
submodules: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: javascript-typescript
config-file: ./.github/codeql/codeql-plugin-sdk-package-contract-critical-quality.yml
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-critical-quality/plugin-sdk-package-contract"

View File

@@ -20,7 +20,7 @@ permissions:
jobs:
macos:
name: Critical Security (macOS)
runs-on: blacksmith-6vcpu-macos-15
runs-on: blacksmith-6vcpu-macos-latest
timeout-minutes: 45
steps:
- name: Checkout
@@ -35,7 +35,7 @@ jobs:
swift --version
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: swift
build-mode: manual
@@ -46,7 +46,7 @@ jobs:
- name: Analyze
id: analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
output: sarif-results
upload: failure-only
@@ -83,7 +83,7 @@ jobs:
done
- name: Upload filtered SARIF
uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
sarif_file: sarif-results-filtered
category: "/codeql-critical-security/macos"

View File

@@ -85,28 +85,17 @@ jobs:
config_file: ./.github/codeql/codeql-actions-critical-security.yml
steps:
- name: Checkout
if: ${{ matrix.category != 'actions' }}
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
- name: Checkout Actions security sources
if: ${{ matrix.category == 'actions' }}
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: false
sparse-checkout: |
.github/actions
.github/workflows
.github/codeql
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
languages: ${{ matrix.language }}
config-file: ${{ matrix.config_file }}
- name: Analyze
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
with:
category: "/codeql-security-high/${{ matrix.category }}"

View File

@@ -138,7 +138,7 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENCLAW_CONTROL_UI_I18N_PROVIDER: ${{ secrets.ANTHROPIC_API_KEY != '' && 'anthropic' || 'openai' }}
OPENCLAW_CONTROL_UI_I18N_MODEL: ${{ secrets.ANTHROPIC_API_KEY != '' && 'claude-opus-4-8' || vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
OPENCLAW_CONTROL_UI_I18N_MODEL: ${{ secrets.ANTHROPIC_API_KEY != '' && 'claude-opus-4-7' || vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
OPENCLAW_CONTROL_UI_I18N_THINKING: low
OPENCLAW_CONTROL_UI_I18N_AUTH_OPTIONAL: "1"
LOCALE: ${{ matrix.locale }}

View File

@@ -32,11 +32,11 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
PNPM_CONFIG_CHILD_CONCURRENCY: "1"
PNPM_CONFIG_MODULES_DIR: "/var/tmp/openclaw-pnpm/node_modules"
PNPM_CONFIG_MODULES_DIR: "/tmp/openclaw-pnpm-node-modules"
PNPM_CONFIG_NETWORK_CONCURRENCY: "1"
PNPM_CONFIG_STORE_DIR: "/var/cache/crabbox/pnpm/store"
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
PNPM_CONFIG_VIRTUAL_STORE_DIR: "/var/tmp/openclaw-pnpm/virtual-store"
PNPM_CONFIG_VIRTUAL_STORE_DIR: "/tmp/openclaw-pnpm-virtual-store"
jobs:
hydrate:
@@ -120,31 +120,9 @@ jobs:
append_pnpm_option_arg PNPM_CONFIG_MODULES_DIR modules-dir
append_pnpm_option_arg PNPM_CONFIG_NETWORK_CONCURRENCY network-concurrency
append_pnpm_option_arg PNPM_CONFIG_VIRTUAL_STORE_DIR virtual-store-dir
require_safe_writable_dir() {
local dir="$1"
if [ -L "$dir" ] || [ ! -d "$dir" ] || [ ! -w "$dir" ]; then
echo "::error::Refusing unsafe pnpm directory: $dir"
exit 1
fi
}
prepare_crabbox_pnpm_dirs() {
local volatile_root="/var/tmp/openclaw-pnpm"
case "${PNPM_CONFIG_MODULES_DIR:?}" in "$volatile_root"/*) ;; *) echo "::error::PNPM_CONFIG_MODULES_DIR must stay under $volatile_root"; exit 1 ;; esac
case "${PNPM_CONFIG_VIRTUAL_STORE_DIR:?}" in "$volatile_root"/*) ;; *) echo "::error::PNPM_CONFIG_VIRTUAL_STORE_DIR must stay under $volatile_root"; exit 1 ;; esac
rm -rf -- "$volatile_root"
mkdir -p "$volatile_root" "$PNPM_CONFIG_STORE_DIR"
require_safe_writable_dir "$volatile_root"
require_safe_writable_dir "$PNPM_CONFIG_STORE_DIR"
mkdir -p "$PNPM_CONFIG_MODULES_DIR" "$PNPM_CONFIG_VIRTUAL_STORE_DIR"
}
prepare_crabbox_pnpm_dirs
if [ -L node_modules ] && [ "$(readlink node_modules)" = "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
rm -f node_modules
fi
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
mkdir -p "$PNPM_CONFIG_MODULES_DIR"
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
export NODE_PATH="$PNPM_CONFIG_MODULES_DIR${NODE_PATH:+:$NODE_PATH}"
fi
pnpm "${install_args[@]}" || pnpm "${install_args[@]}"
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
@@ -159,10 +137,7 @@ jobs:
set -euo pipefail
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
fi
- name: Prepare Crabbox shell
@@ -343,26 +318,7 @@ jobs:
$ErrorActionPreference = "Stop"
if (git rev-parse --is-inside-work-tree 2>$null) {
$repo = (Get-Location).Path
$fetchInfo = New-Object System.Diagnostics.ProcessStartInfo
$fetchInfo.FileName = "git"
$fetchInfo.WorkingDirectory = $repo
$fetchInfo.UseShellExecute = $false
$fetchInfo.Arguments = '-c protocol.version=2 fetch --no-tags --no-progress --prune --no-recurse-submodules --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"'
$fetch = New-Object System.Diagnostics.Process
$fetch.StartInfo = $fetchInfo
if (-not $fetch.Start()) {
throw "git fetch failed to start"
}
if (-not $fetch.WaitForExit(30000)) {
$fetch.Kill()
$fetch.WaitForExit()
throw "git fetch timed out after 30 seconds"
}
if ($fetch.ExitCode -ne 0) {
throw "git fetch failed with exit code $($fetch.ExitCode)"
}
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
}
- name: Setup pnpm and dependencies
@@ -378,10 +334,9 @@ jobs:
$env:XDG_CACHE_HOME = Join-Path $cacheRoot "cache"
$env:COREPACK_HOME = Join-Path $env:XDG_CACHE_HOME "corepack"
$env:PNPM_HOME = Join-Path $cacheRoot "pnpm-home"
$pnpmCacheRoot = Join-Path $cacheRoot "openclaw-pnpm"
$env:PNPM_CONFIG_STORE_DIR = Join-Path $pnpmCacheRoot "store"
$env:PNPM_CONFIG_MODULES_DIR = Join-Path $pnpmCacheRoot "node_modules"
$env:PNPM_CONFIG_VIRTUAL_STORE_DIR = Join-Path $pnpmCacheRoot "virtual-store"
$env:PNPM_CONFIG_STORE_DIR = Join-Path $cacheRoot "openclaw-pnpm-store"
$env:PNPM_CONFIG_MODULES_DIR = Join-Path $workspace "node_modules"
$env:PNPM_CONFIG_VIRTUAL_STORE_DIR = Join-Path $workspace "node_modules\.pnpm"
$env:PNPM_CONFIG_CHILD_CONCURRENCY = "4"
$env:PNPM_CONFIG_NETWORK_CONCURRENCY = "8"
$env:PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN = "false"
@@ -453,25 +408,6 @@ jobs:
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$workspaceNodeModules = Join-Path $workspace "node_modules"
if (Test-Path $workspaceNodeModules) {
$workspaceNodeModulesItem = Get-Item $workspaceNodeModules -Force
if (($workspaceNodeModulesItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) {
$nodeModulesChildren = @(Get-ChildItem -LiteralPath $workspaceNodeModules -Force)
$hasOnlyPnpmWorkspaceState = $nodeModulesChildren.Count -eq 1 -and $nodeModulesChildren[0].Name -eq ".pnpm-workspace-state-v1.json"
if ($nodeModulesChildren.Count -ne 0 -and -not $hasOnlyPnpmWorkspaceState) {
throw "workspace node_modules exists and is not a link: $workspaceNodeModules"
}
foreach ($nodeModulesChild in $nodeModulesChildren) {
Remove-Item -LiteralPath $nodeModulesChild.FullName -Force
}
Remove-Item -LiteralPath $workspaceNodeModules -Force
New-Item -ItemType Junction -Path $workspaceNodeModules -Target $env:PNPM_CONFIG_MODULES_DIR | Out-Null
}
} else {
New-Item -ItemType Junction -Path $workspaceNodeModules -Target $env:PNPM_CONFIG_MODULES_DIR | Out-Null
}
$corepackShimDir = Join-Path $nodeBin "node_modules\corepack\shims"
if (Test-Path $corepackShimDir) {
$env:PNPM_HOME = $corepackShimDir
@@ -577,10 +513,7 @@ jobs:
set -euo pipefail
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
fi
node_bin="$(dirname "$(node -p 'process.execPath')")"

View File

@@ -0,0 +1,176 @@
name: Dependency Change Awareness
on:
pull_request_target: # zizmor: ignore[dangerous-triggers] metadata-only workflow; no checkout or untrusted code execution
types: [opened, reopened, synchronize, ready_for_review]
permissions:
pull-requests: write
issues: write
concurrency:
group: dependency-change-awareness-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
dependency-change-awareness:
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Label and comment on dependency changes
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const marker = "<!-- openclaw:dependency-change-awareness -->";
const labelName = "dependencies-changed";
const maxListedFiles = 25;
const pullRequest = context.payload.pull_request;
if (!pullRequest) {
core.info("No pull_request payload found; skipping.");
return;
}
const isDependencyFile = (filename) =>
filename === "package.json" ||
filename === "package-lock.json" ||
filename === "npm-shrinkwrap.json" ||
filename === "pnpm-lock.yaml" ||
filename === "pnpm-workspace.yaml" ||
filename === "ui/package.json" ||
filename.startsWith("patches/") ||
/^packages\/[^/]+\/package\.json$/u.test(filename) ||
/^extensions\/[^/]+\/package-lock\.json$/u.test(filename) ||
/^extensions\/[^/]+\/npm-shrinkwrap\.json$/u.test(filename) ||
/^extensions\/[^/]+\/package\.json$/u.test(filename);
const sanitizeDisplayValue = (value) =>
String(value)
.replace(/[\u0000-\u001f\u007f]/gu, "?")
.slice(0, 240);
const markdownCode = (value) =>
`\`${sanitizeDisplayValue(value).replaceAll("`", "\\`")}\``;
const ignoreUnavailableWritePermission = (action) => (error) => {
if (error?.status === 403) {
core.warning(
`Skipping dependency change ${action}; token does not have issue write permission.`,
);
return;
}
if (error?.status === 404 || error?.status === 422) {
core.warning(`Dependency change ${action} is unavailable.`);
return;
}
throw error;
};
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullRequest.number,
per_page: 100,
});
const dependencyFiles = files
.map((file) => file.filename)
.filter((filename) => typeof filename === "string" && isDependencyFile(filename))
.sort((left, right) => left.localeCompare(right));
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
per_page: 100,
});
const existingComment = comments.find(
(comment) =>
comment.user?.login === "github-actions[bot]" && comment.body?.includes(marker),
);
const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
per_page: 100,
});
const hasLabel = labels.some((label) => label.name === labelName);
if (dependencyFiles.length === 0) {
if (hasLabel) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
name: labelName,
}).catch(ignoreUnavailableWritePermission("label removal"));
}
if (existingComment) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
}).catch(ignoreUnavailableWritePermission("comment deletion"));
}
await core.summary
.addHeading("Dependency Change Awareness")
.addRaw("No dependency-related file changes detected.")
.write();
core.info("No dependency-related file changes detected.");
return;
}
if (!hasLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
labels: [labelName],
}).catch(ignoreUnavailableWritePermission(`label "${labelName}" update`));
}
const listedFiles = dependencyFiles.slice(0, maxListedFiles);
const omittedCount = dependencyFiles.length - listedFiles.length;
const fileLines = listedFiles.map((filename) => `- ${markdownCode(filename)}`);
if (omittedCount > 0) {
fileLines.push(`- ${omittedCount} additional dependency-related files not shown`);
}
const body = [
marker,
"",
"### Dependency Changes Detected",
"",
"This PR changes dependency-related files. Maintainers should confirm these changes are intentional.",
"",
"Changed files:",
...fileLines,
"",
"Maintainer follow-up:",
"- Review whether the dependency changes are intentional.",
"- Inspect resolved package deltas when lockfile, shrinkwrap, or workspace dependency policy changes are present.",
"- Treat `package-lock.json` and `npm-shrinkwrap.json` diffs as security-review surfaces.",
"- Run `pnpm deps:changes:report -- --base-ref origin/main --markdown /tmp/dependency-changes.md --json /tmp/dependency-changes.json` locally for detailed release-style evidence.",
].join("\n");
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body,
}).catch(ignoreUnavailableWritePermission("comment update"));
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
body,
}).catch(ignoreUnavailableWritePermission("comment creation"));
}
await core.summary
.addHeading("Dependency Change Awareness")
.addRaw(`Detected ${dependencyFiles.length} dependency-related file change(s).`)
.addList(dependencyFiles.map((filename) => markdownCode(filename)))
.write();
core.notice(`Detected ${dependencyFiles.length} dependency-related file change(s).`);

View File

@@ -1,109 +0,0 @@
name: Dependency Guard
on:
pull_request_target: # zizmor: ignore[dangerous-triggers] checks trusted base script only; never checks out PR head
types: [opened, reopened, synchronize, ready_for_review]
permissions:
contents: read
pull-requests: write
issues: write
concurrency:
group: dependency-guard-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
dependency-guard-detect:
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
autoscrub: ${{ steps.guard.outputs.autoscrub }}
autoscrub-owner: ${{ steps.guard.outputs.autoscrub-owner }}
autoscrub-repository: ${{ steps.guard.outputs.autoscrub-repository }}
steps:
- name: Check out trusted base workflow scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: Detect dependency changes
id: guard
env:
GITHUB_TOKEN: ${{ github.token }}
OPENCLAW_DEPENDENCY_GUARD_MODE: detect
OPENCLAW_SECURITY_APPROVERS: vincentkoc,steipete,joshavant
OPENCLAW_SECURITY_TEAM_SLUG: openclaw-secops
run: node scripts/github/dependency-guard.mjs
dependency-guard-autoscrub:
if: ${{ !github.event.pull_request.draft && needs.dependency-guard-detect.outputs.autoscrub == 'true' }}
needs: dependency-guard-detect
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
issues: write
pull-requests: read
steps:
- name: Check out trusted base workflow scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: Create autoscrub app token
id: app-token
continue-on-error: true
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
owner: ${{ needs.dependency-guard-detect.outputs.autoscrub-owner }}
repositories: ${{ needs.dependency-guard-detect.outputs.autoscrub-repository }}
permission-contents: write
- name: Create fallback autoscrub app token
id: app-token-fallback
continue-on-error: true
if: steps.app-token.outcome == 'failure'
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
owner: ${{ needs.dependency-guard-detect.outputs.autoscrub-owner }}
repositories: ${{ needs.dependency-guard-detect.outputs.autoscrub-repository }}
permission-contents: write
- name: Remove package lockfile changes
env:
GITHUB_TOKEN: ${{ github.token }}
OPENCLAW_DEPENDENCY_GUARD_AUTOSCRUB_TOKEN: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
OPENCLAW_DEPENDENCY_GUARD_MODE: autoscrub
OPENCLAW_SECURITY_APPROVERS: vincentkoc,steipete,joshavant
OPENCLAW_SECURITY_TEAM_SLUG: openclaw-secops
run: node scripts/github/dependency-guard.mjs
dependency-guard:
if: ${{ !github.event.pull_request.draft && always() }}
needs:
- dependency-guard-detect
- dependency-guard-autoscrub
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Check out trusted base workflow scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: Enforce dependency guard
env:
GITHUB_TOKEN: ${{ github.token }}
OPENCLAW_DEPENDENCY_GUARD_MODE: enforce
OPENCLAW_SECURITY_APPROVERS: vincentkoc,steipete,joshavant
OPENCLAW_SECURITY_TEAM_SLUG: openclaw-secops
run: node scripts/github/dependency-guard.mjs

View File

@@ -4,7 +4,6 @@ on:
push:
tags:
- "v*"
- "!v*-alpha.*"
paths-ignore:
- "docs/**"
- "**/*.md"
@@ -39,11 +38,7 @@ jobs:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
if [[ "${RELEASE_TAG}" == *"-alpha."* ]]; then
echo "Docker alpha image publishing is disabled."
exit 1
fi
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-beta\.[1-9][0-9]*)?$ ]]; then
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-(alpha|beta)\.[1-9][0-9]*)?$ ]]; then
echo "Invalid release tag: ${RELEASE_TAG}"
exit 1
fi
@@ -89,7 +84,7 @@ jobs:
fetch-depth: 0
- name: Set up Docker Builder
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
@@ -161,7 +156,7 @@ jobs:
- name: Build and push amd64 image
id: build
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64
@@ -179,7 +174,7 @@ jobs:
id: build-browser
if: steps.tags.outputs.browser != ''
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64
@@ -280,7 +275,7 @@ jobs:
fetch-depth: 0
- name: Set up Docker Builder
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
@@ -352,7 +347,7 @@ jobs:
- name: Build and push arm64 image
id: build
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/arm64
@@ -370,7 +365,7 @@ jobs:
id: build-browser
if: steps.tags.outputs.browser != ''
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/arm64
@@ -562,7 +557,7 @@ jobs:
fetch-depth: 1
- name: Set up Docker Builder
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4

View File

@@ -149,7 +149,7 @@ jobs:
- name: Run Codex docs agent
if: steps.gate.outputs.run_agent == 'true'
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02
env:
DOCS_AGENT_BASE_SHA: ${{ steps.gate.outputs.review_base_sha }}
DOCS_AGENT_HEAD_SHA: ${{ steps.gate.outputs.review_head_sha }}

View File

@@ -229,7 +229,7 @@ jobs:
needs: [resolve_target]
if: inputs.rerun_group == 'all'
runs-on: ubuntu-24.04
timeout-minutes: 20
timeout-minutes: 45
permissions:
contents: read
steps:
@@ -245,11 +245,54 @@ jobs:
DOCKER_BUILDKIT: "1"
run: |
set -euo pipefail
timeout --kill-after=30s 15m docker build \
timeout --kill-after=30s 35m docker build \
--target runtime-assets \
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
.
- name: Build and smoke test final Docker runtime image
env:
DOCKER_BUILDKIT: "1"
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
run: |
set -euo pipefail
image_ref="openclaw-release-runtime-smoke:${TARGET_SHA}"
timeout --kill-after=30s 35m docker build \
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
-t "${image_ref}" \
.
docker run --rm --entrypoint /bin/sh "${image_ref}" -lc '
set -eu
test -f /app/src/agents/templates/HEARTBEAT.md
temp_root="$(mktemp -d)"
trap "rm -rf \"${temp_root}\"" EXIT
mkdir -p "${temp_root}/home" "${temp_root}/cwd"
cd "${temp_root}/cwd"
set +e
HOME="${temp_root}/home" \
USERPROFILE="${temp_root}/home" \
OPENCLAW_HOME="${temp_root}/home" \
OPENCLAW_NO_ONBOARD=1 \
OPENCLAW_SUPPRESS_NOTES=1 \
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 \
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK=1 \
AWS_EC2_METADATA_DISABLED=true \
AWS_SHARED_CREDENTIALS_FILE="${temp_root}/home/.aws/credentials" \
AWS_CONFIG_FILE="${temp_root}/home/.aws/config" \
node /app/openclaw.mjs agent --message "workspace bootstrap smoke" --session-id "workspace-bootstrap-smoke" --local --timeout 1 --json \
>"${temp_root}/out.log" 2>&1
status="$?"
set -e
if grep -F "Missing workspace template:" "${temp_root}/out.log"; then
cat "${temp_root}/out.log"
exit 1
fi
test -f "${temp_root}/home/.openclaw/workspace/HEARTBEAT.md"
if [ "${status}" -ne 0 ]; then
cat "${temp_root}/out.log"
fi
'
normal_ci:
name: Run normal full CI
needs: [resolve_target, docker_runtime_assets_preflight]
@@ -337,21 +380,6 @@ jobs:
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
}
fail_fast_failed_jobs() {
local failed_jobs_json
failed_jobs_json="$(
fetch_child_jobs |
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
@@ -367,9 +395,6 @@ jobs:
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
@@ -485,21 +510,6 @@ jobs:
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
}
fail_fast_failed_jobs() {
local failed_jobs_json
failed_jobs_json="$(
fetch_child_jobs |
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
@@ -515,9 +525,6 @@ jobs:
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
@@ -683,24 +690,6 @@ jobs:
[[ "$saw_advisory" == "1" && "$failed" == "0" ]]
}
fail_fast_failed_jobs() {
local failed_jobs_json
if [[ "$workflow" == "openclaw-release-checks.yml" && "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
return 0
fi
failed_jobs_json="$(
fetch_child_jobs |
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
@@ -716,9 +705,6 @@ jobs:
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
@@ -976,21 +962,6 @@ jobs:
}
trap cancel_child EXIT INT TERM
fail_fast_failed_jobs() {
local failed_jobs_json
failed_jobs_json="$(
gh_with_retry run view "$run_id" --json jobs \
--jq '[.jobs[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::npm-telegram-beta-e2e.yml has failed child jobs before the workflow completed; cancelling the remaining run."
jq '.[] | {name, conclusion, url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
poll_count=0
while true; do
status="$(gh_with_retry run view "$run_id" --json status --jq '.status')"
@@ -998,9 +969,6 @@ jobs:
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
gh_with_retry run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true

View File

@@ -143,7 +143,7 @@ jobs:
for (const [dep, rel] of Object.entries(workspace.patchedDependencies ?? {})) {
const absolute = path.join(\"/app\", rel);
if (!fs.existsSync(absolute)) {
throw new Error(\"missing patch for \" + dep + \": \" + rel);
throw new Error(`missing patch for ${dep}: ${rel}`);
}
}
"
@@ -337,7 +337,7 @@ jobs:
for (const [dep, rel] of Object.entries(workspace.patchedDependencies ?? {})) {
const absolute = path.join(\"/app\", rel);
if (!fs.existsSync(absolute)) {
throw new Error(\"missing patch for \" + dep + \": \" + rel);
throw new Error(`missing patch for ${dep}: ${rel}`);
}
}
"

View File

@@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v6
- name: Login to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

@@ -445,7 +445,7 @@ jobs:
sudo chown -R codex:codex "$GITHUB_WORKSPACE"
- name: Run Codex Mantis Telegram agent
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02
env:
BASELINE_REF: ${{ needs.resolve_request.outputs.baseline_ref }}
BASELINE_SHA: ${{ needs.validate_refs.outputs.baseline_revision }}

View File

@@ -377,7 +377,6 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR }}

View File

@@ -218,7 +218,6 @@ jobs:
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE: ci
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
INPUT_SCENARIO: ${{ inputs.scenario }}

View File

@@ -451,7 +451,7 @@ jobs:
OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline
run: |
mkdir -p "${OUTPUT_DIR}"
timeout --preserve-status 300s npm pack --ignore-scripts --json "${BASELINE_SPEC}" --pack-destination "${OUTPUT_DIR}" > "${OUTPUT_DIR}/pack.json"
npm pack --ignore-scripts --json "${BASELINE_SPEC}" --pack-destination "${OUTPUT_DIR}" > "${OUTPUT_DIR}/pack.json"
- name: Capture candidate metadata
id: candidate_metadata

View File

@@ -480,35 +480,6 @@ jobs:
fi
exit 1
plan_release_workflow_matrices:
needs: validate_selected_ref
runs-on: ubuntu-24.04
outputs:
docker_e2e_count: ${{ steps.plan.outputs.docker_e2e_count }}
docker_e2e_matrix: ${{ steps.plan.outputs.docker_e2e_matrix }}
docker_e2e_omitted_json: ${{ steps.plan.outputs.docker_e2e_omitted_json }}
live_models_count: ${{ steps.plan.outputs.live_models_count }}
live_models_matrix: ${{ steps.plan.outputs.live_models_matrix }}
live_models_omitted_json: ${{ steps.plan.outputs.live_models_omitted_json }}
steps:
- name: Checkout trusted release harness
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.sha }}
fetch-depth: 1
- name: Plan release workflow matrices
id: plan
env:
DOCKER_LANES: ${{ inputs.docker_lanes }}
INCLUDE_LIVE_SUITES: ${{ inputs.include_live_suites }}
INCLUDE_RELEASE_PATH_SUITES: ${{ inputs.include_release_path_suites }}
LIVE_MODEL_PROVIDERS: ${{ inputs.live_model_providers }}
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
RELEASE_TEST_PROFILE: ${{ inputs.release_test_profile }}
run: node scripts/plan-release-workflow-matrix.mjs >> "$GITHUB_OUTPUT"
validate_release_live_cache:
needs: validate_selected_ref
if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'live-cache')
@@ -563,7 +534,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_repo_e2e && inputs.live_suite_filter == ''
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
env:
OPENCLAW_VITEST_MAX_WORKERS: "2"
@@ -595,7 +566,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_repo_e2e && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'openshell-e2e')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
@@ -665,15 +636,72 @@ jobs:
run: ${{ matrix.command }}
validate_docker_e2e:
needs: [validate_selected_ref, prepare_docker_e2e_image, plan_release_workflow_matrices]
if: inputs.include_release_path_suites && inputs.docker_lanes == '' && needs.plan_release_workflow_matrices.outputs.docker_e2e_count != '0'
needs: [validate_selected_ref, prepare_docker_e2e_image]
if: inputs.include_release_path_suites && inputs.docker_lanes == ''
name: Docker E2E (${{ matrix.label }})
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.plan_release_workflow_matrices.outputs.docker_e2e_matrix) }}
matrix:
include:
- chunk_id: core
label: core
timeout_minutes: 60
profiles: stable full
- chunk_id: package-update-openai
label: package/update OpenAI install
timeout_minutes: 45
profiles: beta minimum stable full
- chunk_id: package-update-anthropic
label: package/update Anthropic install
timeout_minutes: 60
profiles: beta minimum stable full
- chunk_id: package-update-core
label: package/update core
timeout_minutes: 60
profiles: beta minimum stable full
- chunk_id: plugins-runtime-plugins
label: plugins/runtime plugins
timeout_minutes: 60
profiles: stable full
- chunk_id: plugins-runtime-services
label: plugins/runtime services
timeout_minutes: 60
profiles: stable full
- chunk_id: plugins-runtime-install-a
label: plugins/runtime install A
timeout_minutes: 60
profiles: stable full
- chunk_id: plugins-runtime-install-b
label: plugins/runtime install B
timeout_minutes: 60
profiles: stable full
- chunk_id: plugins-runtime-install-c
label: plugins/runtime install C
timeout_minutes: 60
profiles: stable full
- chunk_id: plugins-runtime-install-d
label: plugins/runtime install D
timeout_minutes: 60
profiles: stable full
- chunk_id: plugins-runtime-install-e
label: plugins/runtime install E
timeout_minutes: 60
profiles: stable full
- chunk_id: plugins-runtime-install-f
label: plugins/runtime install F
timeout_minutes: 60
profiles: stable full
- chunk_id: plugins-runtime-install-g
label: plugins/runtime install G
timeout_minutes: 60
profiles: stable full
- chunk_id: plugins-runtime-install-h
label: plugins/runtime install H
timeout_minutes: 60
profiles: stable full
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
@@ -1603,14 +1631,42 @@ jobs:
validate_live_models_docker:
name: Docker live models (${{ matrix.provider_label }})
needs: [validate_selected_ref, prepare_live_test_image, plan_release_workflow_matrices]
if: inputs.include_live_suites && inputs.live_model_providers == '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models') && needs.plan_release_workflow_matrices.outputs.live_models_count != '0'
needs: [validate_selected_ref, prepare_live_test_image]
if: inputs.include_live_suites && inputs.live_model_providers == '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.plan_release_workflow_matrices.outputs.live_models_matrix) }}
matrix:
include:
- provider_label: Anthropic
providers: anthropic
profiles: stable full
- provider_label: Google
providers: google
profiles: stable full
- provider_label: MiniMax
providers: minimax
profiles: stable full
- provider_label: OpenAI
providers: openai
profiles: beta minimum stable full
- provider_label: OpenCode
providers: opencode-go
profiles: full
- provider_label: OpenRouter
providers: openrouter
profiles: full
- provider_label: xAI
providers: xai
profiles: full
- provider_label: Z.ai
providers: zai
profiles: full
- provider_label: Fireworks
providers: fireworks
profiles: full
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
@@ -1688,8 +1744,6 @@ jobs:
- name: Validate provider credential
if: contains(matrix.profiles, inputs.release_test_profile)
shell: bash
env:
LIVE_MODEL_PROVIDERS: ${{ matrix.providers }}
run: |
set -euo pipefail
@@ -1706,7 +1760,7 @@ jobs:
exit 1
}
case "${LIVE_MODEL_PROVIDERS}" in
case "${{ matrix.providers }}" in
anthropic) require_any Anthropic ANTHROPIC_API_KEY ANTHROPIC_API_KEY_OLD ANTHROPIC_API_TOKEN ;;
google) require_any Google GEMINI_API_KEY GOOGLE_API_KEY ;;
minimax) require_any MiniMax MINIMAX_API_KEY ;;
@@ -1717,7 +1771,7 @@ jobs:
zai) require_any Z.ai ZAI_API_KEY Z_AI_API_KEY ;;
fireworks) require_any Fireworks FIREWORKS_API_KEY ;;
*)
echo "Unhandled live model provider shard: ${LIVE_MODEL_PROVIDERS}" >&2
echo "Unhandled live model provider shard: ${{ matrix.providers }}" >&2
exit 1
;;
esac
@@ -1932,7 +1986,7 @@ jobs:
- suite_id: native-live-src-gateway-profiles-anthropic-opus
suite_group: native-live-src-gateway-profiles-anthropic
label: Native live gateway profiles Anthropic Opus
command: OPENCLAW_LIVE_GATEWAY_THINKING=low OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-opus-4-8 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
command: OPENCLAW_LIVE_GATEWAY_THINKING=low OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-opus-4-7 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
timeout_minutes: 30
profile_env_only: false
advisory: true
@@ -1947,19 +2001,19 @@ jobs:
profiles: full
- suite_id: native-live-src-gateway-profiles-google
label: Native live gateway profiles Google
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview,google/gemini-3-flash-preview node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
timeout_minutes: 60
profile_env_only: false
profiles: stable full
- suite_id: native-live-src-gateway-profiles-minimax
label: Native live gateway profiles MiniMax
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
timeout_minutes: 60
profile_env_only: false
profiles: stable full
- suite_id: native-live-src-gateway-profiles-openai
label: Native live gateway profiles OpenAI
command: OPENCLAW_LIVE_GATEWAY_THINKING=off OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=180000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
timeout_minutes: 60
profile_env_only: false
profiles: beta minimum stable full
@@ -2234,7 +2288,7 @@ jobs:
include:
- suite_id: live-gateway-docker
label: Docker live gateway OpenAI
command: OPENCLAW_LIVE_GATEWAY_THINKING=off OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_THINKING=low OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: beta minimum stable full
@@ -2246,13 +2300,13 @@ jobs:
profiles: stable full
- suite_id: live-gateway-google-docker
label: Docker live gateway Google
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview,google/gemini-3-flash-preview OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full
- suite_id: live-gateway-minimax-docker
label: Docker live gateway MiniMax
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
timeout_minutes: 40
profile_env_only: false
profiles: stable full

View File

@@ -51,8 +51,7 @@ jobs:
# so this source workflow can stay focused on OIDC publish only.
preflight_openclaw_npm:
if: ${{ inputs.preflight_only }}
# Preflight builds the full release package before publish; ubuntu-latest can OOM in tsdown.
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ubuntu-latest
permissions:
contents: read
steps:
@@ -257,8 +256,7 @@ jobs:
return -1;
}
for (const match of input.matchAll(/\[/g)) {
const start = match.index;
for (let start = input.indexOf("["); start !== -1; start = input.indexOf("[", start + 1)) {
const end = arrayEndFrom(start);
if (end === -1) {
continue;
@@ -374,11 +372,6 @@ jobs:
actions: read
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Require trusted workflow ref for publish
env:
RELEASE_TAG: ${{ inputs.tag }}
@@ -436,13 +429,12 @@ jobs:
echo "Direct OpenClaw npm publish; relying on this workflow's npm-release environment approval."
exit 0
fi
direct_recovery=false
if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then
direct_recovery=true
echo "Direct OpenClaw npm recovery with release_publish_run_id; relying on this workflow's npm-release environment approval."
echo "OpenClaw npm publish must be dispatched by the OpenClaw Release Publish workflow, not directly by ${GITHUB_ACTOR}." >&2
exit 1
fi
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw Release Publish"], ["headBranch", process.env.EXPECTED_WORKFLOW_BRANCH], ["event", "workflow_dispatch"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } if (run.status !== "in_progress") { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must still be in_progress, got ${run.status ?? "<missing>"}.`); process.exit(1); } if (run.conclusion) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} already concluded ${run.conclusion}.`); process.exit(1); } console.log(`Using release publish approval run ${process.env.RELEASE_PUBLISH_RUN_ID}: ${run.url}`);'
publish_openclaw_npm:
# KEEP THE REAL RELEASE/PUBLISH PATH ON A GITHUB-HOSTED RUNNER.

View File

@@ -798,7 +798,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: pnpm build
- name: Run parity lane
env:
@@ -813,7 +813,7 @@ jobs:
alt_model="openai/gpt-5.5-alt"
;;
baseline)
model="anthropic/claude-opus-4-8"
model="anthropic/claude-opus-4-7"
alt_model="anthropic/claude-sonnet-4-6"
;;
*)
@@ -876,7 +876,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: pnpm build
- name: Generate parity report
run: |
@@ -885,7 +885,7 @@ jobs:
--candidate-summary .artifacts/qa-e2e/openai-candidate/qa-suite-summary.json \
--baseline-summary .artifacts/qa-e2e/anthropic-baseline/qa-suite-summary.json \
--candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \
--baseline-label anthropic/claude-opus-4-8 \
--baseline-label anthropic/claude-opus-4-7 \
--output-dir .artifacts/qa-e2e/parity
- name: Upload parity artifacts
@@ -903,7 +903,7 @@ jobs:
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
continue-on-error: true
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 45
timeout-minutes: 30
permissions:
contents: read
env:
@@ -934,7 +934,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: pnpm build
- name: Run runtime parity lane
id: runtime_parity_lane
@@ -1075,7 +1075,7 @@ jobs:
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_matrix_enabled == 'true'
continue-on-error: true
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
contents: read
@@ -1101,7 +1101,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: pnpm build
- name: Run Matrix live lane
id: run_lane
@@ -1199,7 +1199,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: pnpm build
- name: Run Telegram live lane
id: run_lane
@@ -1207,7 +1207,6 @@ jobs:
env:
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
run: |
@@ -1295,7 +1294,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: pnpm build
- name: Run Discord live lane
id: run_lane
@@ -1393,7 +1392,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: pnpm build
- name: Run WhatsApp live lane
id: run_lane
@@ -1488,7 +1487,7 @@ jobs:
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
run: pnpm build
- name: Run Slack live lane
id: run_lane

View File

@@ -46,12 +46,11 @@ on:
default: true
type: boolean
release_profile:
description: Release coverage profile used for release evidence summaries; default reads it from the validation manifest
description: Release coverage profile used for release evidence summaries
required: false
default: from-validation
default: beta
type: choice
options:
- from-validation
- beta
- stable
- full
@@ -123,10 +122,6 @@ jobs:
echo "publish_openclaw_npm=true requires dispatching this workflow from main, release/YYYY.M.D, or a Tideclaw alpha branch for alpha prereleases." >&2
exit 1
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${PLUGIN_PUBLISH_SCOPE}" != "all-publishable" ]]; then
echo "publish_openclaw_npm=true requires plugin_publish_scope=all-publishable so every publishable official plugin is released with OpenClaw." >&2
exit 1
fi
if [[ "${PLUGIN_PUBLISH_SCOPE}" == "selected" && -z "${PLUGINS}" ]]; then
echo "plugin_publish_scope=selected requires plugins." >&2
exit 1
@@ -136,9 +131,9 @@ jobs:
exit 1
fi
case "$RELEASE_PROFILE" in
from-validation|beta|stable|full) ;;
beta|stable|full) ;;
*)
echo "release_profile must be one of: from-validation, beta, stable, full" >&2
echo "release_profile must be one of: beta, stable, full" >&2
exit 1
;;
esac
@@ -260,7 +255,6 @@ jobs:
echo "sha=$release_sha" >> "$GITHUB_OUTPUT"
- name: Validate full release validation manifest
id: full_manifest
if: ${{ inputs.publish_openclaw_npm }}
env:
GH_TOKEN: ${{ github.token }}
@@ -291,7 +285,7 @@ jobs:
echo "Full release validation target SHA mismatch: expected $EXPECTED_SHA, got $target_sha" >&2
exit 1
fi
if [[ "$EXPECTED_RELEASE_PROFILE" != "from-validation" && "$release_profile" != "$EXPECTED_RELEASE_PROFILE" ]]; then
if [[ "$release_profile" != "$EXPECTED_RELEASE_PROFILE" ]]; then
echo "Full release validation profile mismatch: expected $EXPECTED_RELEASE_PROFILE, got $release_profile" >&2
exit 1
fi
@@ -299,7 +293,6 @@ jobs:
echo "Full release validation must run rerun_group=all before npm publish; got $rerun_group" >&2
exit 1
fi
echo "release_profile=$release_profile" >> "$GITHUB_OUTPUT"
- name: Validate release tag is reachable from a trusted release branch
env:
@@ -335,7 +328,7 @@ jobs:
env:
RELEASE_TAG: ${{ inputs.tag }}
TARGET_SHA: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
RELEASE_PROFILE: ${{ steps.full_manifest.outputs.release_profile || inputs.release_profile }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
run: |
{
@@ -504,7 +497,7 @@ jobs:
wait_for_run() {
local workflow="$1"
local run_id="$2"
local status conclusion url updated_at created_at duration_seconds duration_label last_state failed_json
local status conclusion url updated_at created_at duration_seconds duration_label last_state
last_state=""
while true; do
@@ -513,14 +506,6 @@ jobs:
if [[ "$status" == "completed" ]]; then
break
fi
failed_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json jobs \
--jq '[.jobs[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]' || true)"
if [[ -n "${failed_json}" ]] && jq -e 'length > 0' <<< "$failed_json" >/dev/null; then
echo "${workflow} has failed jobs before the workflow completed: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" >&2
jq '.[] | {name, conclusion, url}' <<< "$failed_json" >&2 || true
print_failed_run_summary "${run_id}"
return 1
fi
url="$(printf '%s' "$run_json" | jq -r '.url')"
updated_at="$(printf '%s' "$run_json" | jq -r '.updatedAt')"
state="${status}:${updated_at}"

View File

@@ -32,11 +32,11 @@ jobs:
- name: Install opengrep
env:
# Pin both the install script (by commit SHA) and the binary version.
# The script SHA must match the v1.22.0 release tag in opengrep/opengrep
# The script SHA must match the v1.19.0 release tag in opengrep/opengrep
# so a compromised or force-pushed `main` cannot RCE in our CI runner.
# Bump both together when upgrading.
OPENGREP_VERSION: v1.22.0
OPENGREP_INSTALL_SHA: f458d7f0d52cc58eae1ca3cf3d5caf101e637519
OPENGREP_VERSION: v1.19.0
OPENGREP_INSTALL_SHA: 9a4c0a68220618441608cd2bad4ff2eddccf8113
run: |
curl -fsSL "https://raw.githubusercontent.com/opengrep/opengrep/${OPENGREP_INSTALL_SHA}/install.sh" \
| bash -s -- -v "$OPENGREP_VERSION"
@@ -53,7 +53,7 @@ jobs:
scripts/run-opengrep.sh --sarif --error
- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v4.36.1
uses: github/codeql-action/upload-sarif@v4
# Only upload if the scan actually produced a SARIF file.
if: always() && hashFiles('.opengrep-out/precise.sarif') != ''
with:

View File

@@ -44,7 +44,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 2
fetch-depth: 0
fetch-tags: false
persist-credentials: false
submodules: false
@@ -58,11 +58,11 @@ jobs:
- name: Install opengrep
env:
# Pin both the install script (by commit SHA) and the binary version.
# The script SHA must match the v1.22.0 release tag in opengrep/opengrep
# The script SHA must match the v1.19.0 release tag in opengrep/opengrep
# so a compromised or force-pushed `main` cannot RCE in our CI runner.
# Bump both together when upgrading.
OPENGREP_VERSION: v1.22.0
OPENGREP_INSTALL_SHA: f458d7f0d52cc58eae1ca3cf3d5caf101e637519
OPENGREP_VERSION: v1.19.0
OPENGREP_INSTALL_SHA: 9a4c0a68220618441608cd2bad4ff2eddccf8113
run: |
curl -fsSL "https://raw.githubusercontent.com/opengrep/opengrep/${OPENGREP_INSTALL_SHA}/install.sh" \
| bash -s -- -v "$OPENGREP_VERSION"
@@ -74,7 +74,6 @@ jobs:
- name: Run opengrep on PR diff
env:
OPENCLAW_OPENGREP_BASE_REF: ${{ github.event.pull_request.base.sha }}...HEAD
OPENCLAW_OPENGREP_MERGE_HEAD_FIRST_PARENT: "1"
# Findings from precise rules block this workflow. Pull requests scan
# changed first-party source paths only so findings stay attributable to
# the PR diff. Test/fixture/QA path exclusions live in `.semgrepignore`
@@ -84,7 +83,7 @@ jobs:
scripts/run-opengrep.sh --changed --sarif --error
- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v4.36.1
uses: github/codeql-action/upload-sarif@v4
# Only upload if the scan actually produced a SARIF file.
if: always() && hashFiles('.opengrep-out/precise.sarif') != ''
with:

View File

@@ -207,11 +207,6 @@ jobs:
actions: read
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Validate release publish approval run
env:
GH_TOKEN: ${{ github.token }}
@@ -227,13 +222,12 @@ jobs:
echo "Direct Plugin ClawHub Release dispatch; relying on this workflow's clawhub-plugin-release environment approval."
exit 0
fi
direct_recovery=false
if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then
direct_recovery=true
echo "Direct Plugin ClawHub Release recovery with release_publish_run_id; relying on this workflow's clawhub-plugin-release environment approval."
echo "Plugin ClawHub publish must be dispatched by the OpenClaw Release Publish workflow, not directly by ${GITHUB_ACTOR}." >&2
exit 1
fi
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw Release Publish"], ["headBranch", process.env.EXPECTED_WORKFLOW_BRANCH], ["event", "workflow_dispatch"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } if (run.status !== "in_progress") { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must still be in_progress, got ${run.status ?? "<missing>"}.`); process.exit(1); } if (run.conclusion) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} already concluded ${run.conclusion}.`); process.exit(1); } console.log(`Using release publish approval run ${process.env.RELEASE_PUBLISH_RUN_ID}: ${run.url}`);'
preview_plugin_pack:
needs: preview_plugins_clawhub
@@ -437,8 +431,7 @@ jobs:
EOF
echo "CLAWHUB_CONFIG_PATH=${RUNNER_TEMP}/clawhub-config.json" >> "$GITHUB_ENV"
- name: Check ClawHub package version
id: clawhub_package_version
- name: Ensure version is not already published
env:
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
PACKAGE_VERSION: ${{ matrix.plugin.version }}
@@ -463,17 +456,14 @@ jobs:
done
if [[ "${status}" =~ ^2 ]]; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on ClawHub."
echo "already_published=true" >> "$GITHUB_OUTPUT"
exit 0
exit 1
fi
if [[ "${status}" != "404" ]]; then
echo "Unexpected ClawHub response (${status}) for ${PACKAGE_NAME}@${PACKAGE_VERSION}."
exit 1
fi
echo "already_published=false" >> "$GITHUB_OUTPUT"
- name: Publish
if: steps.clawhub_package_version.outputs.already_published != 'true'
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}

View File

@@ -184,11 +184,6 @@ jobs:
actions: read
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Validate release publish approval run
env:
GH_TOKEN: ${{ github.token }}
@@ -204,13 +199,12 @@ jobs:
echo "Direct Plugin NPM Release dispatch; relying on this workflow's npm-release environment approval."
exit 0
fi
direct_recovery=false
if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then
direct_recovery=true
echo "Direct Plugin NPM Release recovery with release_publish_run_id; relying on this workflow's npm-release environment approval."
echo "Plugin npm publish must be dispatched by the OpenClaw Release Publish workflow, not directly by ${GITHUB_ACTOR}." >&2
exit 1
fi
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw Release Publish"], ["headBranch", process.env.EXPECTED_WORKFLOW_BRANCH], ["event", "workflow_dispatch"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } if (run.status !== "in_progress") { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} must still be in_progress, got ${run.status ?? "<missing>"}.`); process.exit(1); } if (run.conclusion) { console.error(`Referenced release publish run ${process.env.RELEASE_PUBLISH_RUN_ID} already concluded ${run.conclusion}.`); process.exit(1); } console.log(`Using release publish approval run ${process.env.RELEASE_PUBLISH_RUN_ID}: ${run.url}`);'
preview_plugin_pack:
needs: preview_plugins_npm
@@ -269,8 +263,7 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
install-bun: "false"
- name: Check npm package version
id: npm_package_version
- name: Ensure version is not already published
env:
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
PACKAGE_VERSION: ${{ matrix.plugin.version }}
@@ -278,13 +271,10 @@ jobs:
set -euo pipefail
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on npm."
echo "already_published=true" >> "$GITHUB_OUTPUT"
else
echo "already_published=false" >> "$GITHUB_OUTPUT"
exit 1
fi
- name: Publish
if: steps.npm_package_version.outputs.already_published != 'true'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -199,13 +199,13 @@ jobs:
--alt-model openai/gpt-5.5-alt \
--output-dir .artifacts/qa-e2e/openai-candidate
- name: Run Opus 4.8 lane
- name: Run Opus 4.7 lane
run: |
pnpm openclaw qa suite \
--provider-mode mock-openai \
--parity-pack agentic \
--concurrency "${QA_PARITY_CONCURRENCY}" \
--model anthropic/claude-opus-4-8 \
--model anthropic/claude-opus-4-7 \
--alt-model anthropic/claude-sonnet-4-6 \
--output-dir .artifacts/qa-e2e/anthropic-baseline
@@ -216,7 +216,7 @@ jobs:
--candidate-summary .artifacts/qa-e2e/openai-candidate/qa-suite-summary.json \
--baseline-summary .artifacts/qa-e2e/anthropic-baseline/qa-suite-summary.json \
--candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \
--baseline-label anthropic/claude-opus-4-8 \
--baseline-label anthropic/claude-opus-4-7 \
--output-dir .artifacts/qa-e2e/parity
- name: Upload parity artifacts
@@ -530,7 +530,6 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || '' }}
@@ -818,7 +817,6 @@ jobs:
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_SLACK_CAPTURE_CONTENT: "1"
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.slack_scenario || '' }}
run: |
set -euo pipefail

View File

@@ -35,7 +35,7 @@ jobs:
submodules: false
- name: Set up Docker Builder
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Build minimal sandbox base (USER sandbox)
shell: bash

View File

@@ -129,7 +129,7 @@ jobs:
- name: Run Codex test performance agent
if: steps.gate.outputs.run_agent == 'true'
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
uses: openai/codex-action@5c3f4ccdb2b8790f73d6b21751ac00e602aa0c02
with:
openai-api-key: ${{ secrets.OPENCLAW_TEST_PERF_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
prompt-file: .github/codex/prompts/test-performance-agent.md

View File

@@ -27,9 +27,7 @@ env:
jobs:
tui-pty:
runs-on: ubuntu-24.04
timeout-minutes: 8
env:
OPENCLAW_TUI_PTY_INCLUDE_LOCAL: "1"
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -40,4 +38,4 @@ jobs:
install-bun: "false"
- name: Run TUI PTY tests
run: timeout --kill-after=30s 240s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts
run: timeout --kill-after=30s 120s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts

View File

@@ -43,4 +43,4 @@ jobs:
published_upgrade_survivor_baselines: ${{ inputs.baselines }}
published_upgrade_survivor_scenarios: ${{ inputs.scenarios }}
telegram_mode: none
secrets: inherit # zizmor: ignore[secrets-inherit] Maintainer-dispatched package acceptance lane intentionally forwards its declared live-test secret matrix.
secrets: inherit

View File

@@ -90,7 +90,7 @@ jobs:
bash -lc 'apt-get update -y && apt-get install -y curl && bash /tmp/install-cli.sh --prefix /tmp/openclaw --no-onboard --version latest && /tmp/openclaw/bin/openclaw --version'
macos-installer:
runs-on: macos-15
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v6

View File

@@ -197,4 +197,4 @@ jobs:
- name: Testbox action marker
if: ${{ false }}
uses: useblacksmith/run-testbox@3f60ff9ceb2c10c3feefa87dc0c6490cffae059d
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc

View File

@@ -1,126 +0,0 @@
name: Windows Node Release
on:
workflow_dispatch:
inputs:
tag:
description: Existing OpenClaw release tag to receive Windows Hub installers, for example v2026.6.1
required: true
type: string
windows_node_tag:
description: openclaw-windows-node release tag to promote, or latest
required: true
default: latest
type: string
permissions:
contents: write
concurrency:
group: windows-node-release-${{ inputs.tag }}
cancel-in-progress: false
jobs:
promote_signed_windows_installers:
name: Promote signed Windows installers
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: Validate inputs
shell: pwsh
env:
RELEASE_TAG: ${{ inputs.tag }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
GH_TOKEN: ${{ github.token }}
run: |
if ($env:RELEASE_TAG -notmatch '^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?$') {
throw "Invalid OpenClaw release tag: $env:RELEASE_TAG"
}
if ($env:WINDOWS_NODE_TAG -ne "latest" -and $env:WINDOWS_NODE_TAG -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$') {
throw "Invalid openclaw-windows-node release tag: $env:WINDOWS_NODE_TAG"
}
gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY | Out-Null
- name: Download Windows Hub release installers
shell: pwsh
env:
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
GH_TOKEN: ${{ github.token }}
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
$tagArgs = @()
if ($env:WINDOWS_NODE_TAG -ne "latest") {
$tagArgs += $env:WINDOWS_NODE_TAG
}
gh release download @tagArgs `
--repo openclaw/openclaw-windows-node `
--pattern "OpenClawCompanion-Setup-*.exe" `
--dir dist
$expected = @(
"dist/OpenClawCompanion-Setup-x64.exe",
"dist/OpenClawCompanion-Setup-arm64.exe"
)
foreach ($file in $expected) {
if (-not (Test-Path -LiteralPath $file)) {
throw "Missing expected Windows installer: $file"
}
}
- name: Verify Authenticode signatures
shell: pwsh
run: |
Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" | ForEach-Object {
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
if ($signature.Status -ne "Valid") {
throw "$($_.Name) Authenticode signature was $($signature.Status)."
}
if (-not $signature.SignerCertificate) {
throw "$($_.Name) has no signer certificate."
}
[pscustomobject]@{
File = $_.Name
Signer = $signature.SignerCertificate.Subject
Thumbprint = $signature.SignerCertificate.Thumbprint
} | Format-List
}
- name: Write SHA-256 manifest
shell: pwsh
run: |
Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" |
Sort-Object Name |
ForEach-Object {
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName
"$($hash.Hash.ToLowerInvariant()) $($_.Name)"
} | Set-Content -Encoding utf8NoBOM -Path dist/OpenClawCompanion-SHA256SUMS.txt
- name: Upload to OpenClaw release
shell: pwsh
env:
RELEASE_TAG: ${{ inputs.tag }}
GH_TOKEN: ${{ github.token }}
run: |
gh release upload $env:RELEASE_TAG `
dist/OpenClawCompanion-Setup-x64.exe `
dist/OpenClawCompanion-Setup-arm64.exe `
dist/OpenClawCompanion-SHA256SUMS.txt `
--repo $env:GITHUB_REPOSITORY `
--clobber
- name: Summary
shell: pwsh
env:
RELEASE_TAG: ${{ inputs.tag }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
run: |
@"
## Windows Hub installers promoted
OpenClaw release: $env:RELEASE_TAG
Source release: openclaw/openclaw-windows-node@$env:WINDOWS_NODE_TAG
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-x64.exe
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-arm64.exe
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-SHA256SUMS.txt
"@ >> $env:GITHUB_STEP_SUMMARY

View File

@@ -61,14 +61,12 @@ jobs:
submodules: false
- name: Probe native Windows
env:
TARGET_REF: ${{ inputs.target_ref || github.ref }}
run: |
$ErrorActionPreference = "Stop"
Write-Host "runner=$env:RUNNER_NAME"
Write-Host "machine=$env:COMPUTERNAME"
Write-Host "workspace=$env:GITHUB_WORKSPACE"
Write-Host "target_ref=$env:TARGET_REF"
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())

View File

@@ -34,10 +34,7 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Fail on tabs in workflow files
@@ -78,71 +75,9 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Prepare trusted workflow audit configs
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
trusted_config="$RUNNER_TEMP/pre-commit-base.yaml"
trusted_zizmor_config="$RUNNER_TEMP/zizmor-base.yml"
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
"+${BASE_SHA}:refs/remotes/origin/security-base" ||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
fi
if git cat-file -e "${BASE_SHA}:.pre-commit-config.yaml" 2>/dev/null; then
git show "${BASE_SHA}:.pre-commit-config.yaml" > "$trusted_config"
elif git show "refs/remotes/origin/${BASE_REF}:.pre-commit-config.yaml" \
> "$trusted_config" 2>/dev/null; then
echo "Base SHA ${BASE_SHA} does not expose .pre-commit-config.yaml; using origin/${BASE_REF} instead."
else
echo "::error title=trusted pre-commit config unavailable::Could not read .pre-commit-config.yaml from ${BASE_SHA} or origin/${BASE_REF}."
exit 1
fi
if git cat-file -e "${BASE_SHA}:.github/zizmor.yml" 2>/dev/null; then
git show "${BASE_SHA}:.github/zizmor.yml" > "$trusted_zizmor_config"
elif git show "refs/remotes/origin/${BASE_REF}:.github/zizmor.yml" \
> "$trusted_zizmor_config" 2>/dev/null; then
echo "Base SHA ${BASE_SHA} does not expose .github/zizmor.yml; using origin/${BASE_REF} instead."
else
echo "::error title=trusted zizmor config unavailable::Could not read .github/zizmor.yml from ${BASE_SHA} or origin/${BASE_REF}."
exit 1
fi
python3 - "$trusted_config" "$trusted_zizmor_config" <<'PY'
from pathlib import Path
import sys
config_path = Path(sys.argv[1])
zizmor_config_path = sys.argv[2]
text = config_path.read_text()
if ".github/zizmor.yml" not in text:
raise SystemExit("trusted pre-commit config does not reference .github/zizmor.yml")
config_path.write_text(text.replace(".github/zizmor.yml", zizmor_config_path))
PY
echo "PRE_COMMIT_CONFIG_PATH=$trusted_config" >> "$GITHUB_ENV"
- name: Install pre-commit
run: python -m pip install --disable-pip-version-check pre-commit==4.2.0
- name: Install actionlint
shell: bash
run: |
@@ -162,15 +97,6 @@ jobs:
- name: Lint workflows
run: actionlint
- name: Audit all workflows with zizmor
shell: bash
run: |
set -euo pipefail
mapfile -t workflow_files < <(
find .github/workflows -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) | sort
)
pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" zizmor --files "${workflow_files[@]}"
- name: Disallow direct inputs interpolation in composite run blocks
run: python3 scripts/check-composite-action-input-interpolation.py
@@ -190,10 +116,7 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Node environment

2
.gitignore vendored
View File

@@ -42,7 +42,6 @@ apps/macos-mlx-tts/.build/
apps/shared/MoltbotKit/.build/
apps/shared/OpenClawKit/.build/
apps/shared/*/.build/
packages/*/dist/
apps/shared/OpenClawKit/Package.resolved
**/ModuleCache/
bin/
@@ -179,7 +178,6 @@ mantis/
/local/
/client_secret_*.json
package-lock.json
!src/commands/copilot-sdk-install-manifest/package-lock.json
.claude/
.agent/
skills-lock.json

View File

@@ -20,46 +20,34 @@
"eslint/no-multi-str": "error",
"eslint/no-new": "error",
"eslint/no-object-constructor": "error",
"eslint/no-param-reassign": "error",
"eslint/no-proto": "error",
"eslint/no-promise-executor-return": "error",
"eslint/no-regex-spaces": "error",
"eslint/no-return-assign": "error",
"eslint/no-sequences": "error",
"eslint/no-self-compare": "error",
"eslint/no-shadow": "error",
"eslint/no-implicit-coercion": "error",
"eslint/no-shadow": "off",
"eslint/no-var": "error",
"eslint/no-useless-call": "error",
"eslint/no-useless-computed-key": "error",
"eslint/no-useless-concat": "error",
"eslint/no-useless-constructor": "error",
"eslint/no-useless-rename": "error",
"eslint/no-useless-return": "error",
"eslint/no-useless-assignment": "error",
"eslint/no-unused-vars": "error",
"eslint/no-unused-vars": "off",
"eslint/no-warning-comments": "error",
"eslint/no-unmodified-loop-condition": "error",
"eslint/no-new-wrappers": "error",
"eslint/no-else-return": "error",
"eslint/no-lonely-if": "error",
"eslint/no-case-declarations": "error",
"eslint/default-case-last": "error",
"eslint/default-param-last": "error",
"eslint/prefer-exponentiation-operator": "error",
"eslint/prefer-const": "error",
"eslint/prefer-numeric-literals": "error",
"eslint/prefer-object-has-own": "error",
"eslint/object-shorthand": "error",
"eslint/prefer-rest-params": "error",
"eslint/prefer-spread": "error",
"eslint/radix": "error",
"eslint/unicode-bom": "error",
"eslint/yoda": "error",
"import/no-absolute-path": "error",
"import/first": "error",
"import/no-empty-named-blocks": "error",
"import/no-duplicates": "error",
"import/no-self-import": "error",
"node/no-exports-assign": "error",
"eslint-plugin-unicorn/prefer-set-size": "error",
@@ -78,14 +66,8 @@
"typescript/no-empty-object-type": ["error", { "allowInterfaces": "with-single-extends" }],
"typescript/no-explicit-any": "error",
"typescript/no-extraneous-class": "error",
"typescript/no-import-type-side-effects": "error",
"typescript/no-meaningless-void-operator": "error",
"typescript/no-misused-promises": "error",
"typescript/no-inferrable-types": "error",
"typescript/only-throw-error": "error",
"typescript/no-non-null-asserted-nullish-coalescing": "error",
"typescript/prefer-promise-reject-errors": "error",
"typescript/restrict-plus-operands": "error",
"typescript/no-unnecessary-qualifier": "error",
"typescript/no-unnecessary-type-assertion": "error",
"typescript/no-unnecessary-type-arguments": "error",
@@ -104,7 +86,6 @@
"typescript/prefer-namespace-keyword": "error",
"typescript/prefer-return-this-type": "error",
"typescript/prefer-find": "error",
"typescript/prefer-for-of": "error",
"typescript/prefer-function-type": "error",
"typescript/prefer-includes": "error",
"typescript/prefer-reduce-type-parameter": "error",
@@ -112,8 +93,6 @@
"typescript/require-array-sort-compare": "error",
"typescript/restrict-template-expressions": "error",
"typescript/triple-slash-reference": "error",
"typescript/unbound-method": "error",
"typescript/use-unknown-in-catch-callback-variable": "error",
"unicorn/consistent-date-clone": "error",
"unicorn/consistent-empty-array-spread": "error",
"unicorn/consistent-function-scoping": "off",
@@ -127,18 +106,14 @@
"unicorn/no-new-buffer": "error",
"unicorn/no-thenable": "error",
"unicorn/no-typeof-undefined": "error",
"unicorn/no-unreadable-array-destructuring": "error",
"unicorn/no-unnecessary-array-flat-depth": "error",
"unicorn/no-unnecessary-array-splice-count": "error",
"unicorn/no-unnecessary-slice-end": "error",
"unicorn/no-useless-error-capture-stack-trace": "error",
"unicorn/no-useless-promise-resolve-reject": "error",
"unicorn/no-useless-switch-case": "error",
"unicorn/no-zero-fractions": "error",
"unicorn/prefer-date-now": "error",
"unicorn/prefer-dom-node-text-content": "error",
"unicorn/prefer-keyboard-event-key": "error",
"unicorn/prefer-array-flat": "error",
"unicorn/prefer-array-some": "error",
"unicorn/prefer-math-min-max": "error",
"unicorn/prefer-node-protocol": "error",
@@ -148,8 +123,6 @@
"unicorn/prefer-prototype-methods": "error",
"unicorn/prefer-regexp-test": "error",
"unicorn/prefer-set-size": "error",
"unicorn/prefer-set-has": "error",
"unicorn/prefer-structured-clone": "error",
"unicorn/prefer-string-starts-ends-with": "error",
"unicorn/prefer-string-slice": "error",
"unicorn/require-array-join-separator": "error",
@@ -210,11 +183,10 @@
"docs/_layouts/",
"extensions/diffs/assets/viewer-runtime.js",
"extensions/diffs-language-pack/assets/viewer-runtime.js",
"extensions/canvas/src/host/a2ui/a2ui.bundle.js",
"node_modules/",
"patches/",
"pnpm-lock.yaml",
"skills/**",
"skills/",
"src/auto-reply/reply/export-html/template.js",
"src/canvas-host/a2ui/a2ui.bundle.js",
"vendor/",
@@ -227,6 +199,13 @@
"**/node_modules/**"
],
"overrides": [
{
"files": ["src/security/**"],
"rules": {
"eslint/no-warning-comments": "off",
"oxc/no-map-spread": "off"
}
},
{
"files": [
"**/*.test.ts",
@@ -238,7 +217,9 @@
"**/*test-support.ts"
],
"rules": {
"typescript/no-explicit-any": "off"
"typescript/no-explicit-any": "off",
"typescript/unbound-method": "off",
"eslint/no-unsafe-optional-chaining": "off"
}
}
]

View File

@@ -9,11 +9,7 @@ Skills own workflows; root owns hard policy and routing.
- Replies: repo-root refs only: `extensions/telegram/src/index.ts:80`. No absolute paths, no `~/`.
- Docs/user-visible work: `pnpm docs:list`, then read relevant docs only.
- Fix/triage answers need source, tests, current/shipped behavior, and dependency contract proof.
- Reviews/answers: high confidence required. Default to exhaustive relevant codebase search/read, including owners, callers, siblings, tests, docs, and upstream/dependency contracts before verdict. Diff-only review is insufficient.
- Review default: read the whole changed function/module plus callers, callees, sibling implementations, adjacent tests, scoped docs, and dependency/Codex contracts before saying `good`, `bad`, `best fix`, `proof sufficient`, or posting a comment. If challenged, keep reading first; do not defend the earlier verdict until the missing path is checked.
- Dependency-touching work: direct dependency inspection is mandatory when feasible; do not rely on assumptions, wrappers, or memory. Most dependencies are OSS, so read their source/docs/types. Codex-related work has a hard gate: the acting agent must personally inspect sibling `../codex` source for the exact protocol/runtime behavior before any verdict, comment, approval, merge recommendation, code change, or `proof sufficient` claim. If missing, clone `https://github.com/openai/codex.git` there first. Subagent reports, PR text, OpenClaw wrappers, generated schemas, memory, and prior bot reviews do not satisfy this gate. No direct `../codex` check means no Codex verdict. Cite Codex files/lines checked in final/review/comment.
- Dependency-backed behavior: read upstream docs/source/types first. No API/default/error/timing guesses.
- External API work: live test required. Google/search for additional proof. Prefer official docs/source/types; cite current proof. No memory-only API claims.
- Live-verify when feasible. Never print secrets.
- Missing deps: `pnpm install`, retry once, then report first actionable error.
- CODEOWNERS: maint/refactor/tests ok. Larger behavior/product/security/ownership: owner ask/review.
@@ -30,8 +26,6 @@ Skills own workflows; root owns hard policy and routing.
- Plugin APIs, provider routing, auth/session state, persisted preferences, config loading, config/default additions, migrations, setup, startup checks, and fallback behavior are compatibility/upgrade-sensitive. Treat config breaks, new config/default surfaces, removed fallbacks, fail-closed changes, stricter validation, or new operator action as merge risk even with green CI when they can affect existing users, upgrades, provider/plugin behavior, or maintainer operations.
- For PRs that add, remove, or change config/default surfaces with possible compatibility, upgrade, provider/plugin, operator, setup, startup, or fallback impact, ClawSweeper review should emit a `reviewMetrics` entry when practical. The metric should name the count and direction of the changes, such as added, changed, or removed config/default surfaces, and explain why the metric matters before merge. When the metric indicates concrete merge risk, also surface the concern in `risks`, use `mergeRiskLabels` when the risk matches the label rubric, make `bestSolution` name the desired pre-merge state, and ensure `labelJustifications` explain the specific reason rather than restating the label.
- Review whole decision surfaces, not only the touched runtime, provider, channel, harness, plugin seam, or context path. Check sibling Codex/Pi-style runtimes, provider/model routing, channel delivery, gateway/protocol, plugin SDK, and context-management paths when relevant.
- Every PR review must explicitly ask whether the PR is the best fix, not merely a plausible fix. Verdicts need a best-fix judgment backed by enough code reading to compare owner boundaries, callers, siblings, tests, docs, current `main`, shipped behavior when relevant, and dependency/Codex contracts when involved.
- Before a PR verdict, build a small evidence map: changed surface, entry point, owner boundary, at least one caller and callee, sibling surfaces that share the invariant, existing tests, and current `main` behavior. If any cell is missing, say the gap instead of concluding.
- One-sided fixes need sibling-surface proof, an explanation for why siblings are unaffected, or explicit follow-up work.
- Changelog findings: see Docs / Changelog.
- Public ClawSweeper comments prefer `https://docs.openclaw.ai/...` when a public docs page exists; structured evidence still cites repo files, lines, SHAs.
@@ -41,9 +35,9 @@ Skills own workflows; root owns hard policy and routing.
## Map
- Core TS: `src/`, `ui/`, `packages/`; plugins: `extensions/`; SDK: `src/plugin-sdk/*`; channels: `src/channels/*`; loader: `src/plugins/*`; protocol: `packages/gateway-protocol/*`; docs/apps: `docs/`, `apps/`.
- Core TS: `src/`, `ui/`, `packages/`; plugins: `extensions/`; SDK: `src/plugin-sdk/*`; channels: `src/channels/*`; loader: `src/plugins/*`; protocol: `src/gateway/protocol/*`; docs/apps: `docs/`, `apps/`.
- Installers: sibling `../openclaw.ai`.
- Scoped guides: `extensions/`, `src/{plugin-sdk,channels,plugins,gateway,agents}/`, `packages/`, `test/helpers*/`, `docs/`, `ui/`, `scripts/`.
- Scoped guides: `extensions/`, `src/{plugin-sdk,channels,plugins,gateway,gateway/protocol,agents}/`, `test/helpers*/`, `docs/`, `ui/`, `scripts/`.
## Docs
@@ -63,25 +57,12 @@ Skills own workflows; root owns hard policy and routing.
- External official plugins own package/deps and are excluded from core dist; core uses registry-aware `facade-runtime` or generic contracts.
- Externalizing a bundled plugin: update package excludes, official catalogs, docs, tests, and prove core runtime paths resolve installed plugin roots before root-dep removal.
- Runtime reads canonical config only. No silent compat for old/malformed config keys. If a config change invalidates existing files, add a matching `openclaw doctor --fix` migration. Core/auth config repairs live in core doctor; plugin-owned config repairs live in that plugin's doctor contract (`legacyConfigRules` / `normalizeCompatibilityConfig`).
- OpenAI Codex is folded into `openai`. No new/live `openai-codex` provider/plugin/auth/model routes; treat them as legacy input only. Runtime/setup/auth/catalog use `openai` + `openai/*`; doctor/migrations repair stale `openai-codex/*` profiles/metadata.
- Config/env surface bar is high; `openclaw.json` and environment variables are already large. Before adding a config option or env var, first prove existing product behavior, provider selection, defaults, or doctor migration cannot solve it. Prefer removing or consolidating config/env options when touching these surfaces. Core supports only the latest config shape; `openclaw doctor --fix` migrates older shipped shapes into the current one.
- CLI setup flows are public API when external docs, installers, or integrations can copy them. Changes to `openclaw onboard`, `openclaw configure`, their documented flags, non-interactive behavior, or generated config shape are compatibility-sensitive API contract changes; prefer additive flags/aliases, deprecation windows, and backward-preserving migrations over breaking existing snippets.
- Fix shape: default to clean bounded refactor, not smallest patch. Move ownership to right boundary; delete stale abstractions, duplicate policy, dead branches, wrappers, fallback stacks.
- Fix observed local failures with generic product rules; do not hardcode names, ids, log phrases, or user examples in prod code unless they are an explicit contract.
- Tests may use observed examples, but prod literals need a short contract reason.
- Compatibility is opt-in. "Shipped" means reachable from a release Git tag; main/GitHub/PR/unreleased code is not shipped.
- Refactor default: one canonical path. Delete the old path unless user explicitly wants compat or the shipped public contract is obvious and cited.
- Core runtime consumes only current canonical shapes/config/data. Legacy or retired shapes normalize only in doctor/migration code before runtime; no runtime shims, aliases, or fallback readers.
- State/storage migrations are database-first. Runtime reads/writes the canonical store only. Old file stores, sidecars, aliases, and fallback readers belong in `openclaw doctor --fix` migration code only, never steady-state runtime.
- Storage default: SQLite only. Do not add JSON/JSONL/TXT/sidecar files for OpenClaw-owned runtime state, caches, queues, registries, indexes, cursors, checkpoints, or plugin scratch data.
- SQLite runtime access uses Kysely helpers, not raw SQL statement strings, except schema DDL, migrations, low-level DB bootstrap, or narrowly justified SQLite primitives.
- Use the shared state DB (`state/openclaw.sqlite`) for global runtime state and plugin KV data. Use the per-agent DB (`agents/<agentId>/agent/openclaw-agent.sqlite`) for agent-scoped state/cache. Use a dedicated SQLite DB only when schema, volume, or lifecycle clearly does not fit those stores.
- Legacy state/cache files are migration debt. When touching code that reads/writes them, prefer moving the data into SQLite or calling out the refactor follow-up; do not add parallel file paths.
- File storage must be a named product artifact: import/export, user attachment, log, backup, or external tool contract. If it is app state or cache, it belongs in SQLite.
- Before adding any path under state dirs, choose one: shared state DB, plugin KV, agent DB, or dedicated SQLite schema. If none fits, design the SQLite owner/schema first.
- Cache/transient state gets no compat migration unless a shipped user contract is cited. Prefer delete/drop/rebuild over import. If old state can be lost without user-visible data loss, remove the old path entirely.
- Persistent user state gets one migration owner. Doctor migrates, verifies, and then runtime assumes the new shape. No dual-write, read-through fallback, lazy import, or "if SQLite fails use JSON" branches.
- Fallback is a product decision, not an implementation convenience. Before adding one, name the shipped contract, failure mode, removal plan, and why doctor cannot solve it. Otherwise delete it.
- Keep old behavior only for an explicit public API/config/plugin SDK/data contract, tagged upgrade path, security/migration boundary, dependency contract, or observed prod state.
- If unsure, ask before preserving compat. Do not keep aliases, shims, fallback stacks, stale names, or obsolete tests just in case.
- Tests alone do not make internals contracts. If compat stays, name the contract and migration/removal plan in code, test, or PR.
@@ -91,16 +72,12 @@ Skills own workflows; root owns hard policy and routing.
- Plugin SDK exception: shipped external API gets new API first plus named compat/deprecation, small tests/docs if useful, removal plan.
- Migrate internal/bundled callers to modern API in the same change. Do not let internal compat become permanent architecture.
- Channels are implementation under `src/channels/**`; plugin authors get SDK seams. Providers own auth/catalog/runtime hooks; core owns generic loop.
- Message/channel plugins stay transport-only. They render portable presentation/actions, enforce transport limits, and map native callback envelopes. They do not own product command trees, plugin/provider policy, or feature-specific menus.
- Portable command UI must use typed presentation actions, not raw string inference. Do not make channels guess that `value` starting with `/` means a native command; core/owner plugins declare command actions, channels map them when supported.
- Raw callback data is transport/private. Approval, command, URL, web-app, and select actions must stay distinguishable before channel encoding so transport adapters do not special-case product strings.
- Agent run terminal state: normalize/merge via `src/agents/agent-run-terminal-outcome.ts`; do not rederive timeout/cancel precedence in projections.
- Hot paths should carry prepared facts forward: provider id, model ref, channel id, target, capability family, attachment class. Do not rediscover with broad plugin/provider/channel/capability loaders.
- Do not fix repeated request-time discovery with scattered caches. Move the canonical fact earlier; reuse prepared runtime objects; delete duplicate lookup branches.
- Gateway/plugin metadata is process-stable: installs, manifests, catalogs, generated paths, bundled metadata. Changes require restart or explicit owner reload/install/doctor flow.
- Runtime hot paths: no freshness polling (`stat`/`realpath`/JSON reread/hash). Reuse current snapshots, install records, discovery, lookup tables, root scopes, resolved paths.
- Process-local metadata caches ok when lifecycle-owned and bounded/single-slot. Freshness exceptions need named owner + tests.
- Inline comments: preserve reviewer context at the code site. Required for non-obvious cross-path/state invariants, lifecycle ordering, ownership boundaries, queue/dedupe symmetry, TTL/cache expiry, cleanup/release coupling, session/id adoption, fallback behavior, platform/dependency caps, deterministic ordering, compact encoded state, or intentional caller differences.
- Inline comments: preserve reviewer context at the code site. Use for cross-path/state invariants, platform/dependency caps, deterministic ordering, compact encoded state, lifecycle ordering, ownership boundaries, session/id adoption, queue-depth symmetry, fallbacks, or intentional caller differences.
- Comment shape: 1-3 short lines; state why the branch/helper exists, what contract it protects, and the bad outcome if removed. Cite nearby constants/helpers when useful. No syntax narration, PR/user-specific lore, or obvious mechanics.
- Gateway protocol changes: additive first; incompatible needs versioning/docs/client follow-through.
- Protocol version bumps: explicit owner confirmation only; never automatic/generated.
@@ -115,10 +92,9 @@ Skills own workflows; root owns hard policy and routing.
- Install: `pnpm install` (keep Bun lock/patches aligned if touched).
- CLI: `pnpm openclaw ...` or `pnpm dev`; build: `pnpm build`.
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
- If raw Vitest is unavoidable, use `vitest run ...`; bare `vitest ...` starts local watch mode and will not exit on its own.
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
- Checks in a normal source checkout: `pnpm check:changed` delegates to Crabbox/Testbox; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... -- env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed` so pnpm runs inside Testbox, not locally.
- Checks in a normal source checkout: `pnpm check:changed`; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... --shell -- "pnpm check:changed"` so pnpm runs inside Testbox, not locally.
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); never add `tsc --noEmit`, `typecheck`, `check:types`.
- Formatting: `oxfmt`, not Prettier. Use repo wrappers (`pnpm format:*`, `pnpm lint:*`, `scripts/run-oxlint.mjs`).
@@ -128,13 +104,12 @@ Skills own workflows; root owns hard policy and routing.
- Use `$openclaw-testing` for test/CI choice and `$crabbox` for remote/full/E2E proof.
- Crabbox request means real scenario proof: install/update/call/repro user path; not just copy tests and run them remotely.
- Visual proof: use Crabbox, set up like a user, then screenshot-verify. No harness/bypass/shortcut unless explicitly asked.
- Small/narrow tests, lints, format checks, and type probes are fine locally only in a healthy normal checkout.
- In Codex worktrees, direct local `pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, and `scripts/committer` can trigger pnpm dependency reconciliation or install prompts. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated proof.
- Full suites, broad changed gates, Docker/package/E2E/live/cross-OS proof, or anything that bogs down the Mac: Crabbox/Testbox.
- One/few files local. If a local command fans out, stop and move broad proof to Crabbox/Testbox.
- Before handoff/push: prove touched surface. Before landing to `main`: issue proof plus appropriate full/broad proof unless scope is clearly narrow.
- Pre-land/pre-commit code changes: mandatory fresh `$autoreview` until no accepted/actionable findings remain. Do not land code on CI, ClawSweeper, prior review comments, or your own manual review alone unless user explicitly opts out or scope is truly trivial/docs-only. If findings want refactor, refactor; no ugly fixes.
- Pre-land/pre-commit code changes: use `$autoreview` until no accepted/actionable findings remain, unless equivalent manual review already done, trivial/docs-only, or user opts out.
- If proof is blocked, say exactly what is missing and why.
- Do not land related failing format/lint/type/build/tests. If unrelated on latest `origin/main`, say so with scoped proof.
- Docs/changelog-only and CI/workflow metadata-only: `git diff --check` plus relevant docs/workflow sanity; escalate only if scripts/config/generated/package/runtime behavior changed.
@@ -143,35 +118,27 @@ Skills own workflows; root owns hard policy and routing.
## GitHub / PRs
- Use `$openclaw-pr-maintainer` immediately for maintainer-side OpenClaw issue/PR review, triage, duplicates, labels, comments, close, land, or evidence. Contributor PR creation/refresh follows the requested contributor workflow; linked refs alone do not require maintainer archive tooling.
- Issue/PR start: `git status -sb`; if clean, `git pull --ff-only`; if dirty, yell before pull/rebase.
- Pasted GitHub issue/PR: first `git status -sb`; if dirty, yell; then `git push` + `git pull --ff-only`.
- PR refs: `gh pr view/diff` or `gh api`, not web search. Prefer `gitcrawl` for maintainer discovery; missing/stale `gitcrawl` falls through to live `gh`, not contributor setup. Verify live with `gh` before mutation.
- Bare issue/PR URL/number: inspect live and take the efficient maintainer path; switch branches/refs when useful.
- No unsolicited PR labels/retitles/rebases/fixups/landing. Comments/reviews ok only for reviewable findings, pre-merge proof, or close/duplicate reason after explicit close/sweep/landing request.
- Bare issue/PR URL/number means review/report in chat. Suggest comment/close/merge when appropriate; mutate only when asked.
- No unsolicited PR comments/reviews/labels/retitles/rebases/fixups/landing. Exception: close/duplicate action that needs a reason comment after explicit close/sweep/landing request.
- Maintainer decision closes the cluster: if deciding reported behavior/proposed fix is not planned, comment+close all directly associated open issues/PRs unless explicitly told to keep one open. Associated means linked PRs/issues, duplicates, companion workaround PRs, and the canonical issue for the rejected behavior.
- Do not leave associated issues open for hypothetical future repros. Close with rationale; ask for a new issue or reopen only if concrete new evidence appears. Close comment states: decision, why, supported alternative, and what evidence would change the decision.
- Issue/PR work: search strong related issues/PRs before final; close proven dupes/fixed siblings. If none close, suggest one next related follow-up.
- PR superseded by `main`: if code proof shows `main` already has same-or-better behavior, comment canonical commit/PR + focused proof, then close. Bar high: inspect PR diff, current code/tests, linked issue, caller/sibling path. If unsure, leave open.
- Issue/PR numbers need a short summary every time; assume the reader has not opened or read them.
- Before presenting a batch of issues/PRs, use smart subagents to verify live state and current `main`; omit closed/fixed items, and comment+close items already fixed on `main` when maintainer action is authorized.
- PR review answer: bug/behavior, URL(s), affected surface, provenance for regressions when traceable, best-fix judgment, evidence from code/tests/CI/current or shipped behavior.
- PR reviewable findings: post them on the PR, not chat-only, so author sees actionable feedback.
- Issue/PR final answer: last line is the full GitHub URL.
- PR verification: before merge, post land-ready work done, exact local commands, CI/Testbox run IDs, before/after proof when used, and known proof gaps.
- PR verification: before merge, post exact local commands, CI/Testbox run IDs, before/after proof when used, and known proof gaps.
- Issue fixed on `main` with proof: comment proof + commit/PR, then close.
- After landing or requested close/sweep: search duplicates; comment proof + canonical commit/PR/release before closing.
- After landing/ship final: include 2-5 sentence recap of what landed: behavior change, key files/surface, proof run, issue/PR state. Do not answer with only status/links.
- `ship` that fixes an issue: after push, comment proof + commit link, then close the issue.
- Public GH comments: show draft in chat first unless user explicitly asked to post/comment/reply/close/merge/land. After work starts and changes/proof exist, post the review/proof/commit comment.
- Representing user: if user already has a comment/thread for the point, update/reply there when possible; avoid duplicate PR/issue comments.
- No surprise GH writes: chat must mention every posted/updated public comment with URL.
- GH comments with backticks, `$`, or shell snippets: use heredoc/body file, not inline double-quoted `--body`.
- PR create: real body required. Include Summary + Verification; mention refs, behavior, and proof.
- PR create/refresh: keep PR branches takeover-ready. Use a branch maintainers can push to, or for fork PRs ensure `maintainer_can_modify` / GitHub's `Allow edits by maintainers` is enabled unless explicitly told otherwise or GitHub's Actions/secrets warning makes that unsafe.
- GitHub issue/PR create: read `$agent-transcript`; ask about sanitized transcript logs when available.
- Contributor PRs: parsed `Real behavior proof` uses exact `field: value` labels: `Behavior addressed`, `Real environment tested`, `Exact steps or command run after this patch`, `Evidence after fix`, `Observed result after fix`, `What was not tested`.
- Real behavior proof section is parsed. Use exact `field: value` labels: `Behavior addressed`, `Real environment tested`, `Exact steps or command run after this patch`, `Evidence after fix`, `Observed result after fix`, `What was not tested`.
- PR artifacts/screenshots: attach to PR/comment/external artifact store. Never push screenshots, videos, proof images, or proof assets to OpenClaw or any product repo branch, including temp artifact branches. Use Crabbox artifact publishing plus the manifest URL. Do not commit `.github/pr-assets`.
- CI polling: exact SHA, relevant checks only, minimal fields. Skip routine noise (`Auto response`, `Labeler`, docs agents, performance/stale). Logs only after failure/completion or concrete need.
- OpenClaw write-access maintainers may skip `Real behavior proof` when local tests or Crabbox verified behavior; record proof in PR verification.
- Maintainers: may skip/ignore `Real behavior proof` when local tests or Crabbox verified behavior; record proof in PR verification.
- `/landpr`: use `~/.codex/prompts/landpr.md`; do not idle on `auto-response` or `check-docs`.
## Code
@@ -187,17 +154,12 @@ Skills own workflows; root owns hard policy and routing.
- Use named intermediates only for domain meaning or readability; avoid temp-variable soup.
- Code size matters. Prefer small clear code; maintainability includes not growing LOC without payoff.
- Refactors should delete about as much local complexity as they add. If LOC grows, the new ownership/API needs to clearly pay for it.
- Refactors should reduce non-test LOC unless they remove a larger architectural cost. Treat positive prod LOC as a smell. Before closeout, run `git diff --numstat`; if non-test LOC grew, trim or explicitly justify why fewer paths now exist.
- Prefer deleting branches, modes, adapters, and tests over preserving them. A refactor that adds a second path has probably failed unless the old path is a cited shipped contract.
- New helpers/files must pay rent immediately: fewer call paths, fewer concepts, or less repeated logic. No helpers for one-off compat, naming translation, or speculative resilience.
- Before adding helpers/files, check whether existing code can absorb the behavior with less new surface.
- Keep APIs narrow: export only current caller needs; keep types/helpers local by default.
- Return the smallest useful shape. Avoid broad result objects, flags, metadata unless callers use them.
- Avoid adapter layers that only rename fields. Move real responsibility or leave code local.
- Inline simple one-use objects/spreads when clearer. Extract only when it removes duplication or hard logic.
- Tests prove behavior/regressions, not every internal branch.
- Tests are welcome, but review them before landing for duplication and value. Delete useless tests, such as assertions for behavior or paths just removed.
- Tests protect canonical behavior and migration boundaries, not obsolete internals. Delete tests for removed fallback paths instead of updating them.
- For non-trivial refactors, check `git diff --numstat` before closeout. If LOC grew, trim or explain why.
- Prefer existing narrow helpers over repeated casts/guards. Add local helpers when 2+ nearby call sites share real boundary logic.
- Prefer ctor parameter properties for injected deps/config. Do not ban them for erasable-syntax purity.
@@ -227,7 +189,7 @@ Skills own workflows; root owns hard policy and routing.
- Use `$technical-documentation` for docs writing/review. Docs change with behavior/API.
- Codex harness upgrade (`extensions/codex/package.json` `@openai/codex`): refresh `docs/plugins/codex-harness.md` model snapshot from the new harness `model/list`.
- Docs final answers: include relevant full `https://docs.openclaw.ai/...` URL(s). If issue/PR work too, GitHub URL last.
- `CHANGELOG.md`: release-only. Do not edit for normal PRs, direct `main` fixes, or `ship it`; release generation owns it. Do not ask contributors/agents for changelog edits.
- `CHANGELOG.md`: release-owned. Do not edit for normal PRs, direct `main` fixes, or `ship it`; only explicit release/changelog generation may rewrite it. Do not ask contributors/agents for changelog edits.
- User-facing `fix`/`feat`/`perf`: put release-note context in PR body, squash message, or direct commit: behavior, surface, issue/PR refs, credited human author/reporter.
- Release generation: derive `CHANGELOG.md` from merged PRs + all direct `main` commits. Entries: active `### Changes`/`### Fixes`, single-line, thank credited humans; never thank bots/forbidden handles: `@openclaw`, `@clawsweeper`, `@codex`, `@steipete`.
@@ -235,19 +197,18 @@ Skills own workflows; root owns hard policy and routing.
- Commit via `scripts/committer "<msg>" <file...>`; stage intended files only.
- Commits: conventional-ish, concise, grouped.
- No manual stash/autostash unless explicit. Branch switches ok when useful; no new worktrees unless requested.
- No manual stash/autostash unless explicit. No branch/worktree changes unless requested.
- `main`: no merge commits; rebase on latest `origin/main` before push. After one green run plus clean rebase sanity, do not chase moving `main` with repeated full gates.
- User says `commit`: your changes only. `commit all`: all changes in grouped chunks. `push`: may `git pull --rebase` first.
- User says `ship it`: commit intended changes, pull --rebase, push.
- Do not delete/rename unexpected files; ask if blocking, else ignore.
- Bulk PR close/reopen >50: ask with count/scope.
- Bulk PR close/reopen >5: ask with count/scope.
## Security / Release
- Never commit real phone numbers, videos, credentials, live config.
- Secrets: channel/provider creds in `~/.openclaw/credentials/`; model auth profiles in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`.
- Dependency patches/overrides/vendor changes need explicit approval. `pnpm-workspace.yaml` patched dependencies use exact versions only.
- Release/package guards: no hard-coded retired-package denylists; use generic artifact/dependency checks or fix build source.
- Lockfiles/shrinkwrap are security surface: review `pnpm-lock.yaml`, `npm-shrinkwrap.json`, `package-lock.json`; root/plugin npm packages ship shrinkwrap, not package-lock.
- Carbon pins owner-only: do not change `@buape/carbon` unless Shadow (`@thewilloftheshadow`, verified by `gh`) asks.
- Releases/publish/version bumps need explicit approval. Use `$release-openclaw-maintainer`.
@@ -265,10 +226,7 @@ Skills own workflows; root owns hard policy and routing.
- Version bump surfaces live in `$release-openclaw-maintainer`.
- Parallels: `$openclaw-parallels-smoke`; Discord roundtrip: `$parallels-discord-roundtrip`.
- Crabbox/WebVNC human demos: keep remote desktop visible/windowed; no fullscreen remote browser unless video/capture-style output.
- Before sharing WebVNC links, use Crabbox screenshot first; verify real app/path works and target UI is not broken.
- ClawSweeper ops: `$clawsweeper`. Deployed hook sessions may post one concise `#clawsweeper` note only when surprising/actionable/risky; if using message tool, reply exactly `NO_REPLY`.
- Generated-media completions wake the requester agent first. Requester visible-reply config decides final text vs message tool; direct media send is fallback/recovery only.
- `message_tool_only`: normal agent final visible reply = current-source `message(action=send)` only. No `NO_REPLY` prompt/contract; no message call = no source reply. Plugin-owned bound-thread reply = plugin return value; no message tool needed. Never auto-publish private final.
- Memory wiki prompt digest stays tiny; prefer `wiki_search` / `wiki_get`; verify contact data before use; source-class provenance for generated people facts.
- Rebrand/migration/config warnings: run `openclaw doctor`.
- Never edit `node_modules`.

View File

@@ -2,219 +2,14 @@
Docs: https://docs.openclaw.ai
## 2026.6.5
### Highlights
- QQBot now strips model reasoning/thinking scaffolding before native delivery, preventing raw `<thinking>` content from leaking into channel replies. (#89913, #90132) Thanks @openperf.
- MCP tool results now coerce `resource_link`, `resource`, `audio`, malformed image, and future non-text/image blocks at the materialize boundary, preventing Anthropic 400s and poisoned session history after a tool returns richer MCP content. (#90710, #90728) Thanks @RanSHammer and @849261680.
- Anthropic extended-thinking sessions recover after prompt-cache expiry or Gateway restart because stream start events wait for `message_start`, letting pre-generation signature errors trigger the existing recovery retry. (#90667, #90697) Thanks @openperf.
- Parallel is now a bundled `web_search` provider with `PARALLEL_API_KEY` discovery, guarded endpoint handling, cache-safe session ids, onboarding picker support, and docs. (#85158) Thanks @NormallyGaussian.
- Google Vertex ADC users get static catalog rows and runtime model resolution again, while single-provider cooldown recovery and memory adapter status checks are more reliable. (#90506, #90609, #90717, #90816) Thanks @849261680.
- Matrix can preflight voice notes before mention gating, preserve thread reads/replies through Matrix relations pagination, and carry QA coverage for voice and thread flows. (#78016, #90415)
- Auth and plugin install state is more durable: auth profiles now live in SQLite, official npm plugin install records keep their trusted pins, and prerelease fallback integrity checks avoid carrying stale integrity forward. (#89102, #88585)
- macOS node mode no longer silently self-reconnects away from a healthy direct Gateway session, reducing unexpected companion app session churn. (#90668, #90815) Thanks @vrurg.
- Upgrade and service paths are safer: cron legacy JSON stores migrate during doctor preflight, service env placeholders no longer mask state-dir secrets, WhatsApp startup waits are bounded, and disabled WhatsApp accounts tear down on config reload. (#90072, #90208, #90277, #90488, #90486, #87951, #87965) Thanks @MonkeyLeeT, @sallyom, @mcaxtr, and @MukundaKatta.
### Changes
- Search/providers: add the Parallel bundled web-search plugin, live provider tests, registration contracts, onboarding/docs wiring, and guarded `api.parallel.ai/v1/search` support. (#85158) Thanks @NormallyGaussian.
- Matrix/channels: add voice-message preflight and thread-aware read/reply behavior, including Matrix QA scenario wiring and docs for voice-message behavior. (#78016, #90415)
- Skills/ClawHub: install ClawHub skills backed by GitHub repositories through the resolved install API, download the pinned GitHub commit, keep install-policy checks, and report install telemetry after success. (#90478) Thanks @Patrick-Erichsen.
- Google Chat/channels: add native approval card actions and click handling so Google Chat approvals use platform-native cards instead of generic message flow.
- Mobile: Android provider/model screens now surface expiring, unavailable, unresolved, and attention states more clearly, while iOS settings and Talk tabs keep diagnostics, gateway rows, attachment labels, and unavailable Talk controls reachable.
- Memory: QMD search can use the new rerank toggle, and memory adapter status uses the resolved default model identity when checking plain status. (#61834)
- Docs/tooling: add Parallel search docs, refresh weather-skill guidance toward `web_fetch`, clarify legacy `openai-codex` auth, document release/test helper scripts, and tighten changed-test routing docs for CI/debugging work. (#90028, #90250) Thanks @fuller-stack-dev.
- Platform maintenance: refresh Android, Swift/macOS, Docker, CodeQL, Buildx, Docker build/push, and Codex Action dependencies for this release train. (#74980, #81757, #86481, #86483, #90601)
### Fixes
- Channel content boundaries: QQBot now strips reasoning/thinking tags before sending, preserving final answers while hiding internal model narration from users. (#89913, #90132) Thanks @openperf.
- Agents/MCP/providers: coerce non-text/image MCP tool-result blocks before they reach provider converters, preserving valid images and turning richer MCP content into text instead of malformed image blocks. (#90710, #90728) Thanks @RanSHammer and @849261680.
- Anthropic/Codex/ACP/agent recovery: defer Anthropic stream start events until `message_start`, strip stale compaction thinking signatures before Anthropic replay, detect unsigned thinking-only stalls, refresh prompt fences after compaction writes, reject empty completion handoffs, preserve parent streaming-off overrides/shared progress commentary, forward heartbeat metadata to context-engine hooks, and cover Codex session/thread migration edge cases. (#90667, #90697, #90163, #90108, #89874, #89505, #90632, #89302, #90729, #90317, #90319) Thanks @openperf, @100yenadmin, and @ooiuuii.
- Provider/model resolution: preserve Google Vertex ADC auth markers in generated catalogs, re-probe a single-provider primary after cooldown, share Codex model visibility, fail closed for unknown model auth, preserve Codex alias availability, keep unresolved profile refs unknown, and avoid resolving auth while listing models. (#90506, #90609, #90717, #90702) Thanks @849261680.
- Gateway/macOS/mobile: avoid duplicate Gateway probe warnings by identity, rate-limit node pairing requests while preserving paired-node reconnects, keep macOS node mode on a healthy direct Gateway session, keep iOS diagnostics and gateway rows reachable, and avoid Linux ARM Gradle resource tasks during Android builds. (#85791, #90147, #90668, #90815) Thanks @giodl73-repo and @vrurg.
- TUI/chat/Workboard/auto-reply: optimistic user messages stay stable across stale history reloads, runId reassignment, and abort windows instead of disappearing, jumping, or lingering as ghost rows; Workboard stale lifecycle bulk updates no longer overwrite newer status/provenance; message-tool sends now count as delivery. (#86205, #89600, #88592, #90123) Thanks @RomneyDa.
- Cron/update/service env: doctor config preflight now migrates legacy cron JSON stores into SQLite before runtime reads, service env planning skips unresolved placeholders that would mask state-dir `.env` values, and session transcript rewrites keep registry markers/discriminants consistent. (#90072, #90208, #90277, #90488) Thanks @MonkeyLeeT and @sallyom.
- Security/config/tooling: guard MCP HTTP redirects, protect global agent config defaults, and keep release/test/tooling proof failures bounded and explicit. (#89732, #90145)
- Channels: WhatsApp restarts when per-account config changes, bounds background startup waits, closes failed sockets, and preserves reconnect behavior; Mattermost slash commands keep their state on `globalThis`; Feishu streaming cards preserve full merged content; voice-call tracks Twilio streams after connect; ClickClack reply tools respect `toolsAllow`. (#87951, #87965, #90486, #68113, #90534, #90181, #90607, #89500) Thanks @MukundaKatta, @mcaxtr, @infoanton, @mushuiyu886, and @sahibzada-allahyar.
- Release/CI/E2E: main CI guard drift, PR merge diff scoping, live Docker credential staging, base-image qualification, installer Docker classification, Playwright dependency install recovery, API-key auth for Codex live Docker lanes, Parallels option terminators, and JSON-mode progress handling are tighter so release proof fails cleaner. (#90532, #90287, #90058) Thanks @RomneyDa, @hxy91819, and @mrunalp.
- Tests/state isolation: provider, media, auth, cron, task, session, sandbox, Gateway, and Codex timeout fixtures now scope more home/state/env data per test, reducing cross-test leakage and making release validation failures less noisy. (#90027, #89974)
## 2026.6.2
### Highlights
- Plugin and skill installs now use an operator install policy instead of the old dangerous-code scanner path, with clearer doctor, CLI, ClawHub, and troubleshooting surfaces for package, archive, source, upload, and marketplace installs. (#89516) Thanks @joshavant.
- Telegram, Feishu, Discord, WhatsApp, and outbound delivery paths got safer around duplicate transcript mirrors, Telegram admin writeback, streamed-final previews, approval allowlists, setup runtime state, poll modifiers, Discord voice errors, and internal progress traces. (#88973, #89626, #89812, #89035, #89814, #89813, #89601) Thanks @pgondhi987, @Petru2224, @zhangguiping-xydt, @codezz, and @takhoffman.
- Chat, Control UI, Skill Workshop, Workboard, Android companion shell, and WebChat flows now preserve visible streaming text, reconcile completed sends, expose ACK timing, add Workboard keyboard movement, harden dialog accessibility, lazy-load usage views, keep current chat toggles working, and improve Android companion-first shell navigation. (#89801, #89777, #89802) Thanks @vincentkoc.
- Security, policy, and config recovery now reject corrupt shell snapshots, unsupported policy keys, unsafe exec approval precheck environments, malformed script limits, and suspicious gateway startup configs while adding data-handling conformance checks. (#89701, #87074, #81488, #87056, #89480) Thanks @RomneyDa, @giodl73-repo, and @mmaps.
- Gateway, agent, Codex, provider, model, and memory paths now recover session write-lock release failures, abandoned Codex app-server startups, stream-to-parent ACP spawns, custom-provider runtime fanout, bundled provider aliases, prompt-cache boundaries, Gemini stop sequences, Kimi cache markers, and watcher pressure warnings. (#89811, #89244) Thanks @RomneyDa and @takhoffman.
- Release, CI, Docker, Crabbox/Testbox, package, and E2E validation lanes now bound more network calls, malformed numeric limits, process groups, cleanup leaks, package hydration paths, Windows installer publishing, release asset verification, and log drains so failures produce bounded proof instead of hanging.
### Changes
- Plugins/security: replace dangerous-code scanner enforcement with operator install policy, install-policy context, doctor checks, install/update CLI wiring, ClawHub metadata paths, and package/archive/source/upload lifecycle coverage. (#89516) Thanks @joshavant.
- Policy: add data-handling conformance checks and reject unsupported policy keys. (#87056, #87074) Thanks @giodl73-repo.
- Telegram/channels: show commentary and reasoning in progress drafts, share progress draft compositors across channel plugins, and keep Telegram polling stop/reset boundaries cheaper and more reliable.
- UI/mobile: add Workboard keyboard movement controls, tighten Workboard card operations, improve Android companion-first shell UX, and document chat ACK timing metadata. (#89802) Thanks @vincentkoc.
- Release metadata: align the root package, publishable plugin manifests, generated shrinkwraps, appcast, iOS, Android, macOS, Matrix plugin changelog, and docs/generated baselines with the 2026.6.2 beta train.
- Release/packaging: promote Windows node installer publishing, require verified Windows release asset links, and document GitHub release-note edits.
- Docs: refresh Windows Hub setup guidance and document Gateway, CLI, and plugin SDK helper contracts.
### Fixes
- Channels/outbound: keep channel sends durable when transcript mirroring fails, stop schema-padded poll modifiers from blocking normal sends, preserve WebChat `sessions_send` handoffs, preserve Discord channel-label suppression while hiding internal agent failure traces, match Discord libopus error shapes, and sanitize Discord tool progress scaffolding. (#89626, #89812, #89601) Thanks @Petru2224, @codezz, and @takhoffman.
- Telegram/Feishu: require admin rights for Telegram target writeback, keep Telegram DM exec approval allowlists working with `ask:off`, prevent Telegram preview duplication across streaming modes, isolate verbose status after streamed finals, cancel clean restart stop timers, slow polling restart storms, and wire Feishu setup runtime setters. (#88973, #89035, #89813, #89814) Thanks @pgondhi987, @zhangguiping-xydt, and @takhoffman.
- Feishu: preserve full streaming card content by sending the merged text on each update instead of only the latest delta, so card readers see complete output when intermediate frames are missed. (#90181) Thanks @mushuiyu886.
- Chat/UI/Gateway: preserve visible chat stream text, clear stale stream buffers before terminal commits, reconcile completed sends, scroll pending sends into view, harden Workboard dialog accessibility, stabilize WebChat prompt-cache affinity, overlap chat catalog startup, render chat history incrementally, lazy-load usage dashboard, and report gateway health auth diagnostics. (#89337) Thanks @RomneyDa.
- Agents/Codex/providers/models: release session write locks when prompt-release fence reads fail, retire abandoned Codex app-server startups, keep stream-to-parent ACP spawns registered, close Codex startup clients on timeout, recover bundled provider aliases, avoid custom-provider runtime fanout, preserve provider prompt-cache boundaries, forward Gemini stop sequences, and strip Kimi-incompatible Anthropic cache markers. (#89811) Thanks @takhoffman.
- Memory/build/update: warn after startup watcher pressure checks, externalize optional Baileys image backends, restore and pin Canvas A2UI compatibility assets, keep plugin repair fetch failures nonblocking, restore Skill Workshop view switching, and keep the current chat toggle active after awaited session switches. (#89244) Thanks @RomneyDa.
- Plugins/auth: keep Hermes migration reports pointed at SQLite auth-profile stores and keep plugin auth-profile reuse tests on the current store path.
- Plugins/CLI: avoid importing the runtime plugin loader only to clear in-process caches after short-lived plugin install, enable, disable, update, and uninstall commands refresh registry metadata.
- Security/config/tooling: reject corrupt shell snapshots, suspicious gateway startup configs, malformed release/test/tooling/Docker/perf numeric limits, oversized audit responses, unsafe exec precheck env, and invalid pending-agent SQLite scaffold denials. (#89701, #89705, #89480, #81488) Thanks @RomneyDa and @mmaps.
- Release/CI/E2E: restore package changelog extraction after the post-2026.6.1 version bump, keep hydrated pnpm modules under `node_modules` for ARM/Linux package lifecycle scripts, keep OpenAI live-cache prerequisites advisory while Anthropic prerequisites stay blocking, retry Windows Parallels background log appends on transient file-lock errors, bound candidate GitHub and cross-OS Discord fetches, harden ARM smoke/browser checks, show Docker build heartbeats, reset Crabbox pnpm hydrate state, and isolate Testbox/Docker/release journey artifacts.
- Release/CI/E2E: keep Crabbox hydrate pnpm stores on the persistent cache volume while still resetting volatile modules, reducing cold installs and runner memory churn.
- Release/CI/E2E: fail secret-provider proof startup immediately when the gateway exits by signal instead of waiting for the readiness timeout.
- Release/CI/E2E: report plugin gateway gauntlet command-log write failures as failed rows instead of crashing the harness from child-process callbacks.
- Release/CI/E2E: abort stalled Kitchen Sink RPC readiness probes as soon as the gateway exits so proof failures return promptly.
- Release/CI/E2E: keep Parallels JSON-mode progress on stderr so macOS, Linux, Windows, and aggregate update smoke summaries stay parseable on stdout.
- Release/CI/E2E: fail Crabbox sparse-sync runs clearly when their temporary full checkout disappears while the child process is running, instead of pretending the child's deleted cwd can be repaired.
- Release/CI/E2E: fail PTY-backed E2E commands when transcript logs cannot be written instead of letting missing proof capture crash around a live child process.
- Release/CI/E2E: fail mock OpenAI request-log write errors with clear HTTP responses instead of leaving provider proof clients waiting on a broken socket.
- Release/CI/E2E: fail Parallels host-command log write errors through the command result path instead of leaving streaming smoke phases unresolved.
## 2026.6.1
### Highlights
- Agents and CLI-backed runtimes recover more cleanly from interrupted tool calls, stale session bindings, compaction handoffs, auth-profile failover, reasoning-tag cleanup, and media delivery retries. (#85798, #87484, #88129, #88136, #88141, #88162, #88182, #88924, #89220) Thanks @RomneyDa, @neeravmakwana, and @omarshahine.
- Channels and mobile delivery are steadier across Telegram, WhatsApp, iMessage, Slack, Discord, Microsoft Teams, Google Chat, Google Meet, QQBot, and iOS realtime Talk. (#88096, #88105, #88183, #88749, #88866, #88948, #88984, #89015, #88231) Thanks @omarshahine, @Jensenwgd, and @sliverp.
- Provider and plugin requests now bound more timers, retries, OAuth/device-code lifetimes, media downloads, local service probes, generated-content polling, provider-catalog failures, reasoning output, and model catalog paths before they can hang a run. (#88480, #88512, #88767, #88781, #88851, #88860, #89343, #89379, #89400) Thanks @vincentkoc, @charles-openclaw, @zz327455573, @849261680, and @XuZehan-iCenter.
- Skills, Skill Workshop, and plugin loading now handle proposal review, stale disabled snapshots, support-file approvals, locale/routing fixes, and loader failures more clearly, so channel turns avoid disabled SecretRefs and operators get better recovery guidance. (#79072, #79173, #88734) Thanks @zeus1959 and @shakkernerd.
- Workboard, SecretRef plugin manifests, hosted iOS push relay, typed presentation command actions, and external Copilot/Tokenjuice packaging add broader orchestration, integration, SDK, and plugin delivery surfaces. (#82326, #87469, #87796, #88107, #88117, #88721, #89336) Thanks @RomneyDa.
- Chat and Control UI startup paths keep sends alive through history loading, stream deltas incrementally, skip markdown work while streaming, keep drafts local while typing, clear the composer after sends, trace first-output latency, cache transcript renders, prioritize first connect, and expose calmer composer controls and notification settings. (#74715, #88772, #88825, #88952, #88960, #88998, #89030, #89106) Thanks @VladyslavLevchuk, @vincentkoc, and @sallyom.
- iMessage monitor state, inbound queues, Discord thread bindings, plugin install ledgers, session metadata, gateway runtime state, plugin metadata, memory watchers, and store writes moved toward SQLite-backed or cached state so restarts and hot paths do less repeated work. (#88794, #88797, #88866, #89075, #89185, #89188, #85351) Thanks @RomneyDa and @NianJiuZst.
- Release, CI, Docker, E2E, plugin install, update, doctor, diagnostics, and security lanes now cap more logs, response bodies, readiness probes, artifact checks, status polling, child workflow waits, docker package cleanup, quiet test stalls, downgrade repair, and health probes so failures report bounded proof instead of stalling. (#84988, #87914, #87952, #88966, #89169, #89701, #89731) Thanks @LibraHo, @Niriakot, @MukundaKatta, and @RomneyDa.
### Changes
- Docs: add a dedicated Skill Workshop guide covering governed skill creation, reviewable proposals, CLI, Gateway, agent tool behavior, approval policy, support files, and recovery; refresh ClawHub cards; and add ClawHub CLI, iMessage SSH-wrapper TCC, Android helper, diff-language, and host-local media-send guidance. (#79658, #88734, #88758, #88865, #89297) Thanks @simplyclever914, @shakkernerd, @vyctorbrzezowski, @TurboTheTurtle, @RomneyDa, and @Wang-Yeah623.
- Skills: let the `skill_workshop` agent tool apply, reject, and quarantine explicit proposals through the guarded review flow. Thanks @shakkernerd.
- Skills: let proposals carry approved support files under standard skill folders, with scanner, hash, and rollback safeguards. Thanks @shakkernerd.
- Skills: let pending proposals be revised in place with versioned, dated proposal frontmatter before approval. Thanks @shakkernerd.
- Skills: add Skill Workshop with pending proposals, CLI/Gateway review actions, rollback metadata, and the `skill_workshop` agent tool. Thanks @shakkernerd.
- Skill Workshop: add the Control UI navigation, styled dashboard, proposal today view, revision dialog, file preview modal, searchable preview files, reusable session handoff, and localized strings.
- Plugins: externalize Tokenjuice as the official `@openclaw/tokenjuice` plugin with npm and ClawHub publish metadata.
- Plugins: externalize the GitHub Copilot agent runtime as the official `@openclaw/copilot` plugin with npm and ClawHub publish metadata.
- iOS: add hosted push relay defaults, realtime Talk playback, and a guarded WebSocket ping path for more reliable mobile sessions. (#88096, #88105, #88231)
- iOS: support native iPad display layouts.
- Android: add installed-app inspection commands, notification picker helpers, and updated-system-app classification.
- Workboard: add orchestration primitives and agent coordination tools for multi-agent planning and run tracking. (#87469)
- Workboard: wire task-backed board runs and show task comments in the edit modal.
- Code mode: add internal namespaces for scoped agent/global sessions and exact namespace tool dispatch. (#88043)
- Code mode: add MCP API files and docs for code-mode integrations.
- Gateway: support Tailscale Serve service names for local service routing.
- Control UI: add a Dreaming-tab agent selector and propagate the selected agent through Dreaming status, diary, and diary actions. (#78748) Thanks @stevenepalmer.
- Control UI: add calmer chat composer controls, local draft typing state, and first-output latency instrumentation for active chat entry. (#88772, #88998) Thanks @vincentkoc.
- Plugins: add a SecretRef provider integration manifest contract and extract shared LLM core packages for provider/plugin reuse. (#82326, #88117)
- Plugin SDK: add typed presentation command actions and the bounded `resolve_exec_env` hook for plugin-provided exec environment contributions. (#88721)
- Plugins: persist the plugin install index in SQLite so installed package lookup survives reloads with less filesystem scanning. (#88794)
- Providers: add MiniMax M3 model support. (#88860)
- Tools/media: allow validated host-local text document media sends while keeping unsafe plain-text media sends blocked. (#79658) Thanks @simplyclever914.
- Doctor: add disk space health checks and stabilize post-upgrade JSON probes.
- Channels: store inbound queues in SQLite and migrate iMessage monitor state to SQLite-backed tracking. (#88797)
- Skills: add the core skills index and centralize skills runtime loading, status, filtering, and prompt formatting.
### Fixes
- Release/CI/E2E: fail early when Crabbox sparse-sync full checkouts do not have enough local disk, with guidance for moving the sync root.
- Build: render independent CLI startup metadata help snapshots concurrently to cut cold build-all metadata time.
- Plugins: stop timed-out package-boundary prep steps by process group so descendant TypeScript/helper processes do not survive local check cleanup.
- Control UI: serve static assets asynchronously after safe-open checks so large UI files do not block Gateway request handling.
- Scripts/UI: forward direct wrapper SIGHUP shutdown to child processes so terminal hangups do not leave wrapped dev commands running.
- Gateway: return the post-expiration pending-work revision from node drains so reconnecting nodes do not observe stale queue revisions after expired items are pruned.
- Release/CI/E2E: keep temporary full-sync checkouts alive while slow Crabbox leases boot, so sparse worktree runs do not lose their sync source before file-list generation.
- Release/CI/E2E: normalize inherited Linux `C.UTF-8` locale settings before raw AWS macOS Crabbox bootstrap commands, avoiding macOS locale warnings during package-manager hydration.
- Release/CI/E2E: keep gateway watch regression checks from copying large static plugin assets inside the measured idle window.
- Update: keep core updates nonblocking when missing external plugin repair downloads or soft plugin repair warnings would otherwise stall, pin post-core plugin compatibility to the downgraded core version, and still block installed active plugin payload smoke failures. (#84431, #87914, #87952) Thanks @TurboTheTurtle, @Niriakot, and @MukundaKatta.
- Agents/providers: keep streaming tool-call argument parsing record-shaped when providers emit valid non-object JSON such as `null` or arrays.
- Release/CI/E2E: reset incremental log readers when watched log files rotate without shrinking, so same-size replacements do not hide new readiness or RPC lines.
- Talk: preserve explicit `null` payloads on controller-created turn and output-audio lifecycle events.
- Agents/TUI: keep local custom provider runs from loading plugin runtime and auth alias metadata when plugins are disabled.
- Agents/TUI: restore in-flight TUI run switch-back behavior, keep no-policy native hook fallback available, guard vanished workspaces, and keep lightweight isolated subagents lightweight.
- Agents/media: keep async image, music, and video generation starts from ending the Codex turn, avoid duplicate generated-media fallbacks, and let mixed requests continue with summaries or other work while media renders in the background. (#89220) Thanks @omarshahine.
- Agents/Codex: keep public OpenAI API-key profiles from being treated as native Codex app-server auth while preserving persisted Codex OAuth sessions.
- Agents/Codex: stream Codex app-server final-answer partials to live reply previews, preserve ACP metadata in SQLite, prefer real tool results over synthetic repair output, prevent aborted app-server turn handles from lingering, migrate legacy OpenAI Codex `lastGood` auth state, and preserve workspace/session metadata through ACP runtime refactors. (#88405, #88724, #88730) Thanks @vincentkoc.
- Control UI: keep collapsed tool cards labeled with the tool name and action instead of generic output text. Thanks @shakkernerd.
- Agents/Codex: surface Skill Workshop guidance in Codex app-server prompts when `skill_workshop` is available. Thanks @shakkernerd.
- Skill Workshop: restore and localize the Control UI board/today view switcher so review workflows keep their intended layout toggle across locales. Thanks @shakkernerd.
- Agents/auth: write auth profiles atomically, dispatch auth failures by type, add force re-login and exhausted-failover recovery, clear legacy auto fallback pins, preserve workspaces during state-only uninstall, and compact before oversized turns so recovery paths avoid partial state. (#85798, #87484, #89181) Thanks @RomneyDa and @neeravmakwana.
- Skills: skip disabled skill env overrides from stale persisted snapshots so disabled skill `apiKey` SecretRefs cannot abort embedded or channel turns. (#79072, #79173) Thanks @zeus1959.
- Skill Workshop: render the Control UI tab from filtered navigation state and keep filtered fallback routing stable.
- CLI: avoid live catalog validation during `openclaw agents add`, so adding a secondary agent no longer depends on provider catalog availability. (#76284, #88314) Thanks @zhangguiping-xydt.
- CLI: harden CLI and plugin edge cases, and keep `plugins list --json` on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph. (#88896)
- CLI/desktop: bridge WSL clipboard operations through the shell, recognize manual-update launchd jobs, and keep machine-readable startup output parseable during progress setup. (#88764, #88689) Thanks @alexzhu0.
- Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.
- Plugins: clarify plugin loader failure guidance and treat soft plugin repair warnings as nonfatal so missing or incompatible plugin packages point operators at the right repair path without blocking unrelated work. (#84431) Thanks @TurboTheTurtle.
- Plugins: preserve npm plugin roots after blocked installs, skip plugin-local `openclaw` peer symlinks during rollback snapshots, relink those peers after restore, isolate cached tool runtime siblings, isolate provider catalog projections and web-provider factory failures, and keep private LLM-core declarations bundled so one bad plugin does not poison sibling runtime paths. (#77237, #88767, #88807, #89336) Thanks @vincentkoc and @RomneyDa.
- Cron: keep SQLite cron migrations compatible with legacy run-log tables, archived job stores, diagnostic cron names, single-job run-history names, startup cron retries, and legacy one-shot delete-after-run behavior. (#88285, #88294, #89075) Thanks @kip-claw.
- Cron: keep update delivery validation scoped, harden restart state, and retire MCP runtimes on isolated cron cleanup.
- Auto-reply: guard dispatcher failure-count probes so missing optional counters do not break SDK-typed recovery paths. (#89318) Thanks @Alix-007 and @takhoffman.
- Memory: serialize QMD update/embed writes per store, reduce Linux watcher fan-out, avoid noisy gateway watcher warnings, retry transient FileProvider-backed reads, preserve phase signals on read errors, harden envelope metadata sanitization, reattach Linux native watchers when directories are recreated, and rewrite generated transcript paths on rollover so memory/search state survives concurrent gateway and CLI activity. (#66339, #85931, #89185, #89188, #89246, #85351) Thanks @openperf, @amittell, @RomneyDa, and @NianJiuZst.
- Memory: keep vector-disabled FTS indexes from resolving embedding providers during sync and search.
- Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.
- Providers: resolve Google defaults to `google-generative-ai`, register Vertex static catalog rows and `gemini-3.1-flash-lite`, align Foundry reasoning metadata, skip DeepSeek V4 thinking params on Foundry fallback, use MiniMax account OAuth endpoints, preserve Copilot Claude 1M capabilities, suppress disabled Ollama reasoning output, forward Gemini stop sequences, switch direct Gemini reasoning to native mode, strip provider self-prefixes and Kimi-incompatible Anthropic cache markers, keep OpenAI stop-finished tool calls, and avoid replay ids when the Responses store is disabled. (#88480, #88512, #88781, #89343, #89379, #89400, #76612) Thanks @coder999999999, @BryanTegomoh, @vliuyt, @charles-openclaw, @zz327455573, @849261680, and @XuZehan-iCenter.
- Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.
- Cron: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot.
- Agents/Codex: keep live session locks during cleanup, recover interrupted CLI tool transcripts, preserve Codex auth and compaction session identity, clear orphan tool state, cap app-server idle timers, and keep media completion delivery retryable. (#88129, #88136, #88141, #88162, #88182)
- Chat/UI: show Gateway chat failures as visible assistant messages in the Control UI instead of only setting an invisible error state.
- Channels: cap Telegram, Discord, WhatsApp, Signal, Feishu, Google Chat, Microsoft Teams, QQBot, Nostr, Zalo, Zalouser, and Nextcloud-style request/retry timers; preserve SMS approval reply routes; keep iMessage typing active during tool work; allow RFC2544 benchmark ranges for QQBot token fetches; and retry WhatsApp QR login 408 timeouts. (#88183, #88948, #88984, #89015) Thanks @omarshahine, @Jensenwgd, and @sliverp.
- Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, corrupt shell snapshots, untrusted workspace setup-only channel loads, remote media reference overreads, trajectory export leaks, hooks-token auth reuse, and gateway WebSocket calls after close. (#86953, #87376, #88974, #89354, #89701) Thanks @hxy91819, @coygeek, @pgondhi987, and @RomneyDa.
- Providers/media: cap local service, model, usage, queue, generated media, TTS, music, workflow polling, and provider OAuth request timers across hosted and local providers.
- Release/CI/E2E: bound release candidate reads, beta smoke REST calls, plugin npm verification commands, changelog restore, cross-OS process groups, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Telegram credential timeouts, Control UI i18n and CLI startup metadata generation, Vitest routing, dependency guard admin approvals, child workflow failure detection, quiet Node test shard stalls, dist cache restores, Docker base-image/package cleanup, and mainline test flakes. (#84988, #88127, #88137, #88155, #88160, #88966, #89169) Thanks @LibraHo and @RomneyDa.
- Release/CI/E2E: keep Kitchen Sink live plugin MCP probes resolving source-checkout workspace packages and align the live gauntlet with current Kitchen Sink diagnostics.
- Backup: accept root-relative hardlink targets during backup verification. (#89328) Thanks @abnershang.
- Release/CI/E2E: run the secret-provider integration proof through the repo pnpm runner so native macOS and Windows validation use the hydrated package-manager shim.
- Release/CI/E2E: run the Telegram desktop proof gateway through the repo pnpm runner so native macOS proof uses the hydrated package-manager shim.
- Docs/CI: run Mintlify anchor checks through the repo pnpm runner so docs link validation works when pnpm is only available through the hydrated package-manager shim.
- Agents: keep configured fallback model metadata typed so provider params, context-token caps, and media input limits do not break changed-gate typechecks.
- Agents: accept hidden `sessions_send` body aliases before validation while keeping the model-facing `message` schema canonical. (#88229) Thanks @zhangguiping-xydt.
- Chat/UI: preserve startup chat sends during history loading, unblock the initial Control UI chat send, stream chat deltas incrementally, skip markdown parsing while streaming, keep drafts local while typing, guard composer rerenders, cache chat transcript renders, record pending-send paint timing, show the Communication Notifications tab, honor Chromium executable overrides, and detect system Chromium for E2E. (#74715, #88952, #88960, #88998) Thanks @VladyslavLevchuk and @vincentkoc.
- Channels: stop schema-padded poll modifiers from turning normal `send` actions into invalid poll sends. (#89601) Thanks @codezz and @takhoffman.
- Channels: preserve long Feishu streaming replies, recover failed progress draft starts, send visible fallbacks when accepted Feishu turns produce no final reply, preserve external `sessions_send` routes, persist Discord thread bindings in SQLite, tolerate iMessage self-chat timestamp skew, preserve colon-prefixed slash commands in mention parsing, decode Nostr `npub` allowlists correctly, and suppress raw provider errors during channel delivery. (#87896, #88749, #88803, #88866) Thanks @MonkeyLeeT.
- Config/status/doctor: skip unresolved shell references in state-dir dotenv files, resolve gateway auth secrets during deep status audits, surface disabled Codex plugin routes in doctor lint, respect explicit PI runtime policy, report runtime tool-schema and gateway health credential errors, clear recovered embedded-run activity, migrate voice-call call logs through doctor, and keep post-upgrade JSON stable. (#88731, #88761, #88820, #88288, #89731) Thanks @brokemac79, @openperf, and @RomneyDa.
- Gateway/session state: list commands from the Gateway plugin registry, harden MCP loopback tool schemas, hide phantom agent-store rows from `sessions.list`, make task persistence failures explicit, support Tailscale Serve service names, guard Browser/Chrome pending attach aborts, and carry session UUIDs on interactive dispatch events. (#88305) Thanks @rohitjavvadi.
- Gateway/plugins: narrow plugin lookup memoization to the stable plugin/runtime inputs, avoiding repeated lookup work without mixing disabled or filtered plugin state.
- OpenAI/TTS: handle speed directives for OpenAI TTS voices. (#74089)
- CI/Crabbox: keep default runner capacity on the Azure credit-backed on-demand D4 lane with the Azure SSH port and a Git-independent full check job, so broad validation avoids low-priority spot quota stalls, hydrate port mismatches, non-Git hydrated workspaces, and stale AWS region hints.
- CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.
- CI/workflows: route workflow sanity helper edits to their guard tests and cover composite-action input interpolation checks.
- CI/tooling: route CI scope, dependency, changelog, and docs helper edits to their owner tests instead of silently skipping changed-test coverage.
- CI/tooling: route package, release, and install helper edits to their owner tests so changed-test gates cover publish and installer script changes.
- CI/tooling: route shared script library edits through their owner tests so lock, process, safety, and scan helpers do not skip changed-test coverage.
- CI/tooling: skip expensive import-graph scans once a changed diff already requires broad fallback, keeping local changed-test planning fast while still collecting explicit owner tests.
- CI/tooling: route script edits through conventional owner tests when matching `test/scripts` or `src/scripts` coverage already exists.
- CI/tooling: honor option terminators in the memory FD repro script so follow-on arguments are not reparsed.
- Release/CI/E2E: assert plugin lifecycle runtime inspect output instead of only capturing it.
- Release/CI/E2E: make gateway-network prove the advertised health RPC and retry early WebSocket closes without burning full open timeouts.
- Release/CI/E2E: honor option terminators across release, Parallels smoke, plugin gauntlet, and extension-memory scripts.
- Release/CI/E2E: fail plugin gateway gauntlet QA chunks when the requested suite summary is missing or invalid.
- Performance: prebuild QA runtime probes with generated plugin assets but without CLI startup metadata.
- Performance: skip declaration bundling for runtime-only CLI startup and gateway watch build profiles.
- Performance: reuse prepared provider handles, strict tool schemas, gateway runtime metadata, session maintenance config, plugin metadata, bundled skill allowlists, package-local plugin artifacts, single-entry store writes, and validated/serialized session prompt blobs.
## 2026.5.28
### Highlights
- Agent and Codex runtime recovery is steadier: subagents keep cwd/workspace separation, hook context stays prompt-local, session locks release on timeout abort while live OpenClaw locks survive cleanup, stale restart continuations are avoided, and Codex app-server/helper failures no longer tear down shared runtime state. (#87218, #86875, #87409, #87399, #87375, #88129)
- Channel delivery and session identity got safer across outbound plugin hooks, Matrix room ids, iMessage reactions/approvals, Slack final replies, Discord recovered tool warnings, runtime-config message actions, WhatsApp profile auth roots, Telegram polling, and Microsoft Teams service URL trust checks. (#73706, #75670, #87366, #87451, #87334, #84535, #82492, #83304, #87160)
- Mobile and chat surfaces got a broader refresh: the iOS Pro UI, hosted push relay default, realtime Talk tab playback, Gateway chat transport, onboarding, Talk permissions, WebChat reconnect delivery, and session picker behavior now preserve more state across reconnects and empty searches. (#87367, #87531, #87682, #88096, #88105) Thanks @ngutman and @BunsDev.
- Browser, channel, and automation inputs are stricter: Browser tool timeouts, viewport/tab indices, Gateway ports, cron retry handling, Discord component ids, schema array refs, Telegram callback pages, and channel progress callbacks now reject malformed values earlier and preserve the intended delivery context. (#82887)
- Provider, media, and document coverage expands with Claude Opus 4.8, Fal Krea image schemas, NVIDIA featured models, MiniMax streaming music responses, encrypted PDF extraction, voice model catalogs, GitHub Copilot agent runtime support, and a Codex Supervisor plugin path for delegated Codex workflows. (#87845, #87890, #80775, #84764, #87751, #87794)
- CLI, auth, doctor, and provider paths fail faster and recover more clearly: malformed numeric/version options are rejected, workspace dotenv provider credentials are ignored, heartbeat defaults, OAuth/token lifetimes, and local service startup requests are bounded, agent auth health labels are clearer, legacy `api_key` auth profiles migrate to canonical form, and restart guidance is actionable. (#87398, #86281, #87361, #88133, #83655, #87559, #88088, #85924) Thanks @vincentkoc and @giodl73-repo.
- Plugin and Gateway hot paths do less repeated work while preserving cache correctness for install records, config JSON parsing, tool search catalogs, session stores, manifest model rows, auto-enabled plugin config, browser tokens, viewer assets, and release-split external plugin packages. (#86699)
- Agent and Codex runtime recovery is steadier: subagents keep cwd/workspace separation, hook context stays prompt-local, session locks release on timeout abort, stale restart continuations are avoided, and Codex app-server/helper failures no longer tear down shared runtime state. (#87218, #86875, #87409, #87399, #87375)
- Channel delivery and session identity got safer across outbound plugin hooks, Matrix room ids, iMessage reactions/approvals, Slack final replies, Discord recovered tool warnings, and Microsoft Teams service URL trust checks. (#73706, #75670, #87366, #87451, #87334)
- CLI, auth, doctor, and provider paths fail faster and recover more clearly: malformed numeric/version options are rejected, OAuth and local service startup requests are bounded, legacy `api_key` auth profiles migrate to canonical form, and restart guidance is actionable. (#87398, #86281, #87361)
- Plugin and Gateway hot paths do less repeated work while preserving cache correctness for install records, config JSON parsing, tool search catalogs, session stores, manifest model rows, auto-enabled plugin config, browser tokens, and viewer assets. (#86699)
- Release, QA, and E2E validation now bound more log, artifact, harness, and cross-OS waits so failing lanes produce proof instead of hanging or false-greening.
### Changes
@@ -222,41 +17,20 @@ Docs: https://docs.openclaw.ai
- Status: show active subagent details in status output.
- Diffs: split the default language pack and expand default Diffs language coverage while keeping the host floor aligned. (#87370, #87372) Thanks @RomneyDa.
- ClawHub: add plugin display names plus skill verification and trust surfaces. (#87354, #86699) Thanks @thewilloftheshadow and @Patrick-Erichsen.
- iOS: refresh the dev app with Pro Command, Chat, Agents, Settings, hosted push relay defaults, and realtime Talk playback wired to gateway sessions, diagnostics, chat, and realtime Talk. (#87367, #88096, #88105) Thanks @Solvely-Colin and @ngutman.
- Docs: clarify Codex computer-use setup, paste-token stdin auth setup, macOS gateway sleep troubleshooting, native Codex hook relay recovery, container model auth, install deployment cards, device-token admin gating, CLI setup flow compatibility, Notte cloud browser CDP setup, and backport targets. (#87313, #63050, #87685) Thanks @bdjben, @liaoandi, and @thewilloftheshadow.
- PDF/tools: use ClawPDF for PDF extraction, support encrypted PDF extraction, and surface MCP structured content in agent tool results. (#87670, #87751)
- Providers: add Claude Opus 4.8 support, Fal Krea image model schemas, NVIDIA featured model catalogs, MiniMax streaming music responses, and provider-backed voice model catalogs. (#87845, #87890, #80775, #84764, #87794) Thanks @eleqtrizit and @vincentkoc.
- Codex/GitHub: add the GitHub Copilot agent runtime and the Codex Supervisor plugin package.
- Plugins: externalize GitHub Copilot and Tokenjuice as official install-on-demand plugins with npm and ClawHub publish metadata.
- Workboard: add agent coordination tools for tracking and handing off active agent work.
- Discord: show commentary in progress drafts so live Discord runs expose useful in-progress context. (#85200)
- Plugin SDK: add a reply payload sending hook for plugins that need to deliver channel-owned replies and flatten package types for SDK declarations. (#82823, #87165) Thanks @piersonr and @RomneyDa.
- Policy: add policy comparison, ingress-channel conformance, and sandbox-posture conformance checks. (#85572, #85744, #86768)
- iOS: refresh the dev app with Pro Command, Chat, Agents, and Settings tabs wired to gateway sessions, diagnostics, chat, and realtime Talk. (#87367) Thanks @Solvely-Colin.
- Docs: clarify Codex computer-use setup, paste-token stdin auth setup, macOS gateway sleep troubleshooting, native Codex hook relay recovery, container model auth, install deployment cards, device-token admin gating, and backport targets. (#87313, #63050) Thanks @bdjben, @liaoandi, and @thewilloftheshadow.
### Fixes
- Agents: fall back to local config pruning when the optional `agents delete` Gateway probe cannot authenticate, so offline installs can still delete agents without removing shared workspaces.
- Tighten phone-control mutation authorization [AI]. (#87150) Thanks @pgondhi987.
- Clarify directive persistence authorization policy [AI]. (#86369) Thanks @pgondhi987.
- Agents/Codex: keep spawned agent cwd/workspace state separated, forward ACP spawn attachments, keep hook context prompt-local, release session locks on timeout abort and runtime teardown without deleting live OpenClaw-owned locks during cleanup, avoid session event queue self-wait, clean up exec abort listeners, stream assistant deltas incrementally, recover raw missing-thread compaction failures, preserve rotated compaction session identity, keep compaction-timeout snapshots continuable, preserve shared app-server state across startup or helper failures, keep native hook relay alive across restarts and prune stale bridge files, close native hook relay replacement races, keep Claude live tool progress visible for watchdog recovery, suppress abandoned requester completion handoff, route workspace memory through tools, resolve Codex runtime models first, report quarantined dynamic tools, format `skills` command output, bind node auto-review to prepared plans, retry Claude CLI transcript probes, and bound compaction/steering retries. (#87218, #86875, #86123, #88129, #87399, #87375, #72574, #87383, #87400, #83022, #87671, #87738, #87747, #87706, #87546, #87541, #81048) Thanks @mbelinky, @Alix-007, @luoyanglang, @yetval, @sjf, @joshavant, @benjamin1492, @c19354837, @fuller-stack-dev, @pfrederiksen, and @dodge1218.
- Codex Supervisor: keep real-home app-server MCP session listing on the loaded state path, bound stored history scans, and close WebSocket probes cleanly.
- Channels: thread canonical session keys into outbound hooks, preserve Matrix room-id case, keep fallback tool warnings mention-inert, retain delivered Slack final replies during late cleanup, continue iMessage polling after denied reactions, suppress duplicate native exec approvals, resolve Gateway message actions against the active runtime config, preserve Telegram SecretRef prompt config and polling keepalives, preserve WhatsApp profile auth roots, QR display, document filenames, and plugin hook config, suppress Discord recovered tool warnings, preserve the Discord voice outbound helper, cap Discord/Signal/Zalo channel request and container timeouts, and block untrusted Teams service URLs while keeping TeamsSDK patterns aligned. (#73706, #75670, #87366, #87451, #87465, #87334, #84535, #76262, #83304, #82492, #87581, #77114, #86426, #85529, #87160) Thanks @zeroaltitude, @lukeboyett, @jarvis-mns1, @xiaotian, @funmerlin, @joshavant, @eleqtrizit, @heyitsaamir, @amittell, @lidge-jun, @liorb-mountapps, @masatohoshino, @bladin, and @giodl73-repo.
- CLI/auth/doctor/providers: reject malformed numeric/timeout/subcommand-version inputs, ignore workspace dotenv provider credentials, wait for respawn child shutdown, bound heartbeat defaults plus Codex, GitHub Copilot, OpenAI, Anthropic, Google, Feishu, LM Studio, MiniMax, Xiaomi TTS, and local-provider OAuth/token/model requests, harden Codex auth probes, label auth health by agent, preserve explicit agentRuntime pins during Codex model migration, warm provider auth off the main thread, honor Codex response timeouts, stop migrating current Claude Haiku 4.5 profiles to Sonnet, bound local service startup, resolve GPT-5.5 without cached catalog, migrate legacy memory auto-provider config, rewrite non-canonical `api_key` auth profiles, and make doctor restart follow-ups actionable. (#87398, #86281, #87361, #88133, #83655, #87559, #87719, #88088, #85924, #84362) Thanks @Patrick-Erichsen, @samzong, @giodl73-repo, @alkor2000, @mmaps, @nxmxbbd, and @vincentkoc.
- Gateway/security/session state: expire browser tokens after auth rotation, scope assistant idempotency dedupe, drain probe client closes, avoid stale restart continuation reuse, preserve retry-after fallbacks and stale rate-limit cooldown probes, bound webchat image and artifact transcript scans, include seconds in inbound metadata timestamps, clear completed session active runs, clear stale chat stream buffers, and evict current plugin-state namespaces at row caps. (#87810, #87833, #75089) Thanks @joshavant and @litang9.
- Config/parsing/network: reject partial numeric parsing, parse provider/Discord retry headers and dates strictly, honor IPv6 and bare IPv6 `no_proxy` entries, preserve empty plugin allowlists, canonicalize secret target array indexes, and reject malformed media content lengths, inspected TCP ports, marketplace content lengths, cron epochs, sandbox stat fields, unsafe duration values, empty config path segments, noncanonical schema array refs, unsafe Telegram callback pages, and invalid Teams attachment-fetch DNS targets. (#87883) Thanks @zhangguiping-xydt.
- Browser/input hardening: reject invalid tab indexes, excessive viewport resizes, explicit zero CDP ports, malformed geolocation options, unsafe screenshot or permission-grant timeouts, loose response-body limits, invalid cookie expiries, and non-finite Browser tool delays/timeouts.
- Cron/automation: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot, and preflight model fallbacks before skipping scheduled work. (#82887) Thanks @chen-zhang-cs-code.
- Auto-reply/directives: respect provider and relayed channel metadata during directive persistence so channel-originated decisions keep their intended context. (#87683)
- WhatsApp: resolve the auth directory from the active profile so profile-scoped WhatsApp installs do not drift to the wrong credential root. (#82492) Thanks @lidge-jun.
- Gateway/session state: clear completed session active runs, avoid cold-loading providers for MCP inventory, cache single-session child indexes, cap handshake timers, and bound preauth, auth-guard, media, transcript, readiness, and port options.
- Channels/replies: preserve channel-owned progress callbacks when verbose output is off, keep group-room progress suppression intact, prefer external session delivery context, escape Discord component id delimiters, force final TUI chat repaints, show Slack reasoning previews, and normalize Discord/Matrix/Mattermost channel numeric options. (#87476, #87423)
- Agents/tool args: harden smart-quoted argument repair for edit arrays and exact escaped arguments so model-produced tool calls recover without corrupting valid input. (#86611) Thanks @ferminquant.
- Providers/agents: preserve seeded Anthropic signatures, preserve signed thinking payloads, concatenate signature-delta chunks, preserve DeepSeek `reasoning_content` replay across tier suffixes, apply OpenRouter strict9 ids to Mistral routes, promote Ollama plain-text tool calls, load NVIDIA featured model catalogs, stream MiniMax music generation responses, and recover empty preflight compaction. (#87593, #87493, #80775, #84764) Thanks @Pluviobyte and @eleqtrizit.
- Media/images: skip CLI image cache refs when resolving generated images, allow trusted generated HTML attachments, and bound generated video downloads so stale refs and slow providers fail cleanly. (#87523, #87982)
- Agents/Codex: keep spawned agent cwd/workspace state separated, keep hook context prompt-local, release session locks on timeout abort, avoid session event queue self-wait, preserve shared app-server state across startup or helper failures, keep native hook relay alive across restarts, route workspace memory through tools, resolve Codex runtime models first, report quarantined dynamic tools, format `skills` command output, and bound compaction/steering retries. (#87218, #86875, #86123, #87399, #87375, #87383, #87400) Thanks @mbelinky, @Alix-007, @luoyanglang, @yetval, and @sjf.
- Channels: thread canonical session keys into outbound hooks, preserve Matrix room-id case, keep fallback tool warnings mention-inert, retain delivered Slack final replies during late cleanup, continue iMessage polling after denied reactions, suppress duplicate native exec approvals, preserve Telegram SecretRef prompt config, suppress Discord recovered tool warnings, and block untrusted Teams service URLs. (#73706, #75670, #87366, #87451, #87334) Thanks @zeroaltitude, @lukeboyett, @xiaotian, and @eleqtrizit.
- CLI/auth/doctor/providers: reject malformed numeric/timeout/subcommand-version inputs, wait for respawn child shutdown, bound Codex and GitHub Copilot OAuth/token requests, warm provider auth off the main thread, honor Codex response timeouts, bound local service startup, resolve GPT-5.5 without cached catalog, migrate legacy memory auto-provider config, rewrite non-canonical `api_key` auth profiles, and make doctor restart follow-ups actionable. (#87398, #86281, #87361) Thanks @Patrick-Erichsen, @samzong, @giodl73-repo, and @alkor2000.
- Gateway/security/session state: expire browser tokens after auth rotation, scope assistant idempotency dedupe, drain probe client closes, avoid stale restart continuation reuse, preserve retry-after fallbacks, bound webchat image and artifact transcript scans, include seconds in inbound metadata timestamps, and evict current plugin-state namespaces at row caps.
- File transfer: handle late tar stdin pipe errors after archive validation or unpacking has already settled.
- Performance: trust install-record caches between reloads, prefer native JSON parsing, reuse unchanged tool-search catalogs, reuse gateway session and plugin metadata paths, skip unchanged store serialization, patch single-entry session writes, add precomputed session patch writers, reduce store clone allocations, cache manifest model catalog rows and auto-enabled plugin config, avoid full session snapshots for entry reads, defer configured Slack full startup, prefer bundled plugin dist entries, and slim current metadata identity caches. (#87760)
- Docker/release/QA: package runtime workspace templates, stream cross-OS served artifacts, preserve sparse Crabbox run artifacts, isolate npm plugin installs per package, reject incompatible package plugin API installs, drop the leftover root Sharp dependency from package manifests after the Rastermill migration, bound OpenClaw instance logs, plugin gauntlet relay logs, MCP channel buffers, kitchen-sink scans, agent-turn assertions, QA-Lab credential broker calls, QA Matrix substrate requests, and release scenario logs, and keep release/google live guards current. (#87647, #87477) Thanks @rohitjavvadi and @vincentkoc.
- Release/CI: bound manual git fetches, ClawHub verifier responses, ClawHub owner metadata, dependency-guard error bodies, Parallels limits, startup/test/memory budget parsing, and diffs viewer build warnings so release lanes fail with useful proof instead of hanging. (#87839)
- Performance: trust install-record caches between reloads, prefer native JSON parsing, reuse unchanged tool-search catalogs, skip unchanged store serialization, add precomputed session patch writers, reduce store clone allocations, cache manifest model catalog rows and auto-enabled plugin config, and slim current metadata identity caches.
- Docker/release/QA: package runtime workspace templates, stream cross-OS served artifacts, preserve sparse Crabbox run artifacts, bound OpenClaw instance logs, plugin gauntlet relay logs, MCP channel buffers, kitchen-sink scans, agent-turn assertions, and release scenario logs, and keep release/google live guards current.
## 2026.5.27
@@ -760,7 +534,6 @@ Docs: https://docs.openclaw.ai
- Gateway/sessions: allow shared-secret bearer callers to read and stream session history without an explicit scope header. (#81815) Thanks @medns.
- Agents/embedded runner: classify HTML auth provider responses as `auth_html` and return a re-authentication hint instead of the CDN-blocked copy that `upstream_html` returns. Cloudflare Access login pages, nginx basic-auth challenges, and gateway login walls all produce HTML auth bodies that were previously misdiagnosed as transient CDN blocks. (#79900) Thanks @martingarramon.
- TUI/streaming watchdog: dismiss the `This response is taking longer than expected` notice as soon as a chat event for the same run arrives, so the message no longer sits next to the recovered response when the run was only briefly silent. Refs #67052, #69081 (closed), prior attempt #69026. Thanks @jpruit20 and @romneyda.
- Agents/auth profiles: replace the bare `No available auth profile for <provider> (all in cooldown or unavailable)` TUI error with plain-language copy that explains what happened in user terms (sign-in expired, provider asking us to slow down, billing issue on the account, etc.) and suggests the matching `openclaw models auth login --provider <provider>` recovery command for sign-in and billing causes, while falling back to the underlying provider error for cases without a clear recovery path. Thanks @romneyda.
- Agents/Pi: tolerate OpenClaw-owned transcript writes while embedded prompts are released for model I/O, keeping long-running Feishu, Slack, Telegram, and cron turns from failing with false session-takeover errors. Fixes #84059. (#84250) Thanks @tianxiaochannel-oss88.
## 2026.5.20

View File

@@ -9,18 +9,18 @@
# Build stages use full bookworm; the runtime image is always bookworm-slim.
ARG OPENCLAW_EXTENSIONS=""
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="docker.io/library/node:24-bookworm@sha256:8530f76a96d88820d288761f022e318970dda93d01536919fbc16076b7983e63"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="docker.io/library/node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
# Keep in sync with .github/actions/setup-node-env/action.yml bun-version.
# To update: docker buildx imagetools inspect docker.io/oven/bun:<version> and use the manifest-list digest.
ARG OPENCLAW_BUN_IMAGE="docker.io/oven/bun:1.3.13@sha256:87416c977a612a204eb54ab9f3927023c2a3c971f4f345a01da08ea6262ae30e"
# To update: docker buildx imagetools inspect oven/bun:<version> and use the manifest-list digest.
ARG OPENCLAW_BUN_IMAGE="oven/bun:1.3.13@sha256:87416c977a612a204eb54ab9f3927023c2a3c971f4f345a01da08ea6262ae30e"
# Base images are pinned to SHA256 digests for reproducible builds.
# Dependabot refreshes these blessed digests; release builds consume the
# reviewed base snapshot instead of mutating distro state on every build.
# To update, run: docker buildx imagetools inspect docker.io/library/node:24-bookworm and
# docker.io/library/node:24-bookworm-slim (or podman) and replace the digests below with the
# To update, run: docker buildx imagetools inspect node:24-bookworm and
# node:24-bookworm-slim (or podman) and replace the digests below with the
# current multi-arch manifest list entries.
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS workspace-deps
@@ -48,7 +48,6 @@ RUN --mount=type=bind,source=packages,target=/tmp/packages,readonly \
FROM ${OPENCLAW_BUN_IMAGE} AS bun-binary
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS build
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
ARG OPENCLAW_EXTENSIONS
# Copy pinned Bun binary from the official image instead of fetching via curl.
COPY --from=bun-binary /usr/local/bin/bun /usr/local/bin/bun
@@ -78,12 +77,7 @@ RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/sto
# pnpm v10+ may append peer-resolution hashes to virtual-store folder names; do not hardcode `.pnpm/...`
# paths. Matrix's native downloader can hit transient release CDN errors while
# still exiting successfully, so retry the package downloader before failing.
# Skip the entire check when matrix is not a bundled extension (e.g. msteams-only builds).
RUN set -eux; \
if ! printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' ' | tr ' ' '\n' | grep -qx 'matrix'; then \
echo "==> matrix not bundled, skipping matrix-sdk-crypto check"; \
exit 0; \
fi; \
echo "==> Verifying critical native addons..."; \
for attempt in 1 2 3 4 5; do \
if find /app/node_modules -name "matrix-sdk-crypto*.node" 2>/dev/null | grep -q .; then \

View File

@@ -30,8 +30,7 @@ Supported channels include: WhatsApp, Telegram, Slack, Discord, Google Chat, Sig
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
Preferred setup: run `openclaw onboard` in your terminal.
OpenClaw Onboard guides you step by step through setting up the gateway, workspace, channels, and skills. It is the recommended CLI setup path and works on **macOS, Linux, and Windows**.
Windows desktop users can start with the native [Windows Hub](https://docs.openclaw.ai/platforms/windows) companion app for setup, tray status, chat, node mode, and local MCP mode.
OpenClaw Onboard guides you step by step through setting up the gateway, workspace, channels, and skills. It is the recommended CLI setup path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
Works with npm, pnpm, or bun.
## Sponsors
@@ -165,7 +164,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — wake words on macOS/iOS and continuous voice on Android (ElevenLabs + system TTS fallback).
- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
- **[First-class tools](https://docs.openclaw.ai/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
- **[Companion apps](https://docs.openclaw.ai/platforms)** — Windows Hub, macOS menu bar app, and iOS/Android [nodes](https://docs.openclaw.ai/nodes).
- **[Companion apps](https://docs.openclaw.ai/platforms/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.openclaw.ai/nodes).
- **[Onboarding](https://docs.openclaw.ai/start/wizard) + [skills](https://docs.openclaw.ai/tools/skills)** — onboarding-driven setup with bundled/managed/workspace skills.
## Security model (important)
@@ -186,7 +185,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
- New here: [Getting started](https://docs.openclaw.ai/start/getting-started), [Onboarding](https://docs.openclaw.ai/start/wizard), [Updating](https://docs.openclaw.ai/install/updating)
- Channel setup: [Channels index](https://docs.openclaw.ai/channels), [WhatsApp](https://docs.openclaw.ai/channels/whatsapp), [Telegram](https://docs.openclaw.ai/channels/telegram), [Discord](https://docs.openclaw.ai/channels/discord), [Slack](https://docs.openclaw.ai/channels/slack)
- Apps + nodes: [Windows Hub](https://docs.openclaw.ai/platforms/windows), [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android), [Nodes](https://docs.openclaw.ai/nodes)
- Apps + nodes: [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android), [Nodes](https://docs.openclaw.ai/nodes)
- Config + security: [Configuration](https://docs.openclaw.ai/gateway/configuration), [Security](https://docs.openclaw.ai/gateway/security), [Exposure runbook](https://docs.openclaw.ai/gateway/security/exposure-runbook), [Sandboxing](https://docs.openclaw.ai/gateway/sandboxing)
- Remote + web: [Gateway](https://docs.openclaw.ai/gateway), [Remote access](https://docs.openclaw.ai/gateway/remote), [Tailscale](https://docs.openclaw.ai/gateway/tailscale), [Web surfaces](https://docs.openclaw.ai/web)
- Tools + automation: [Tools](https://docs.openclaw.ai/tools), [Skills](https://docs.openclaw.ai/tools/skills), [Cron jobs](https://docs.openclaw.ai/automation/cron-jobs), [Webhooks](https://docs.openclaw.ai/automation/webhook), [Gmail Pub/Sub](https://docs.openclaw.ai/automation/gmail-pubsub)

View File

@@ -2,197 +2,6 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.6.1</title>
<pubDate>Wed, 03 Jun 2026 21:26:22 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026060190</sparkle:version>
<sparkle:shortVersionString>2026.6.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.6.1</h2>
<h3>Highlights</h3>
<ul>
<li>Agents and CLI-backed runtimes recover more cleanly from interrupted tool calls, stale session bindings, compaction handoffs, and media delivery retries. (#88129, #88136, #88141, #88162, #88182)</li>
<li>Channels and mobile delivery are steadier across Telegram, WhatsApp, iMessage, Slack, Discord, Microsoft Teams, Google Chat, Google Meet, and iOS realtime Talk. (#88096, #88105, #88183, #88231)</li>
<li>Provider and plugin requests now bound more timers, retries, OAuth/device-code lifetimes, media downloads, local service probes, and generated-content polling paths before they can hang a run.</li>
<li>Skills, session metadata, gateway runtime state, plugin metadata, memory watchers, and store writes do less repeated work on hot paths while keeping config, dispatch, and Linux file-watch behavior stable. (#89185, #89188, #85351) Thanks @RomneyDa and @NianJiuZst.</li>
<li>Skills and plugin loading now handle stale disabled snapshots and loader failures more clearly, so channel turns avoid disabled SecretRefs and operators get better recovery guidance. (#79072, #79173) Thanks @zeus1959.</li>
<li>Workboard, SecretRef plugin manifests, hosted iOS push relay, and external Copilot/Tokenjuice packaging add broader orchestration, integration, and plugin delivery surfaces. (#82326, #87469, #87796, #88107, #88117)</li>
<li>Skill Workshop now has a fuller Control UI flow with proposal lists, today actions, revision handoff, searchable file previews, review states, locale coverage, and reusable session routing.</li>
<li>Chat and Control UI startup paths keep sends alive through history loading, stream deltas incrementally, skip markdown work while streaming, keep drafts local while typing, clear the composer after sends, trace first-output latency, prioritize first connect, and expose calmer composer controls. (#88772, #88825, #88998, #89030, #89106) Thanks @vincentkoc and @sallyom.</li>
<li>Provider coverage and model metadata now include MiniMax M3, account OAuth endpoints, Google/Vertex catalog fixes, OpenRouter SQLite model caching, Copilot Claude 1M capabilities, Foundry reasoning alignment, and OpenAI response replay guards. (#88480, #88512, #88851, #88860)</li>
<li>iMessage monitor state, inbound queues, and plugin install ledgers moved toward SQLite-backed state so restarts and local monitors recover with less duplicate filesystem scanning. (#88794, #88797)</li>
<li>Release, CI, Docker, E2E, plugin install, and diagnostics lanes now cap more logs, response bodies, readiness probes, artifact checks, status polling, child workflow waits, docker package cleanup, quiet test stalls, and rollback snapshots so failures report bounded proof instead of stalling. (#88966) Thanks @RomneyDa.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Docs: add a dedicated Skill Workshop guide covering governed skill creation, reviewable proposals, CLI, Gateway, agent tool behavior, approval policy, support files, and recovery, and refresh the ClawHub showcase cards. (#88734) Thanks @shakkernerd and @vyctorbrzezowski.</li>
<li>Skills: let the <code>skill_workshop</code> agent tool apply, reject, and quarantine explicit proposals through the guarded review flow. Thanks @shakkernerd.</li>
<li>Skills: let proposals carry approved support files under standard skill folders, with scanner, hash, and rollback safeguards. Thanks @shakkernerd.</li>
<li>Skills: let pending proposals be revised in place with versioned, dated proposal frontmatter before approval. Thanks @shakkernerd.</li>
<li>Skills: add Skill Workshop with pending proposals, CLI/Gateway review actions, rollback metadata, and the <code>skill_workshop</code> agent tool. Thanks @shakkernerd.</li>
<li>Skill Workshop: add the Control UI navigation, styled dashboard, proposal today view, revision dialog, file preview modal, searchable preview files, reusable session handoff, and localized strings.</li>
<li>Plugins: externalize Tokenjuice as the official <code>@openclaw/tokenjuice</code> plugin with npm and ClawHub publish metadata.</li>
<li>Plugins: externalize the GitHub Copilot agent runtime as the official <code>@openclaw/copilot</code> plugin with npm and ClawHub publish metadata.</li>
<li>iOS: add hosted push relay defaults, realtime Talk playback, and a guarded WebSocket ping path for more reliable mobile sessions. (#88096, #88105, #88231)</li>
<li>iOS: support native iPad display layouts.</li>
<li>Workboard: add orchestration primitives and agent coordination tools for multi-agent planning and run tracking. (#87469)</li>
<li>Workboard: wire task-backed board runs and show task comments in the edit modal.</li>
<li>Code mode: add internal namespaces for scoped agent/global sessions and exact namespace tool dispatch. (#88043)</li>
<li>Code mode: add MCP API files and docs for code-mode integrations.</li>
<li>Control UI: add a Dreaming-tab agent selector and propagate the selected agent through Dreaming status, diary, and diary actions. (#78748) Thanks @stevenepalmer.</li>
<li>Control UI: add calmer chat composer controls, local draft typing state, and first-output latency instrumentation for active chat entry. (#88772, #88998) Thanks @vincentkoc.</li>
<li>Plugins: add a SecretRef provider integration manifest contract and extract shared LLM core packages for provider/plugin reuse. (#82326, #88117)</li>
<li>Plugins: persist the plugin install index in SQLite so installed package lookup survives reloads with less filesystem scanning. (#88794)</li>
<li>Providers: add MiniMax M3 model support. (#88860)</li>
<li>Doctor: add disk space health checks and stabilize post-upgrade JSON probes.</li>
<li>Channels: store inbound queues in SQLite and migrate iMessage monitor state to SQLite-backed tracking. (#88797)</li>
<li>Skills: add the core skills index and centralize skills runtime loading, status, filtering, and prompt formatting.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Release/CI/E2E: fail early when Crabbox sparse-sync full checkouts do not have enough local disk, with guidance for moving the sync root.</li>
<li>Build: render independent CLI startup metadata help snapshots concurrently to cut cold build-all metadata time.</li>
<li>Plugins: stop timed-out package-boundary prep steps by process group so descendant TypeScript/helper processes do not survive local check cleanup.</li>
<li>Control UI: serve static assets asynchronously after safe-open checks so large UI files do not block Gateway request handling.</li>
<li>Scripts/UI: forward direct wrapper SIGHUP shutdown to child processes so terminal hangups do not leave wrapped dev commands running.</li>
<li>Gateway: return the post-expiration pending-work revision from node drains so reconnecting nodes do not observe stale queue revisions after expired items are pruned.</li>
<li>Release/CI/E2E: keep temporary full-sync checkouts alive while slow Crabbox leases boot, so sparse worktree runs do not lose their sync source before file-list generation.</li>
<li>Release/CI/E2E: normalize inherited Linux <code>C.UTF-8</code> locale settings before raw AWS macOS Crabbox bootstrap commands, avoiding macOS locale warnings during package-manager hydration.</li>
<li>Release/CI/E2E: keep gateway watch regression checks from copying large static plugin assets inside the measured idle window.</li>
<li>Update: keep core updates nonblocking when a missing external plugin repair download stalls, while still blocking installed active plugin payload smoke failures.</li>
<li>Agents/providers: keep streaming tool-call argument parsing record-shaped when providers emit valid non-object JSON such as <code>null</code> or arrays.</li>
<li>Release/CI/E2E: reset incremental log readers when watched log files rotate without shrinking, so same-size replacements do not hide new readiness or RPC lines.</li>
<li>Talk: preserve explicit <code>null</code> payloads on controller-created turn and output-audio lifecycle events.</li>
<li>Agents/TUI: keep local custom provider runs from loading plugin runtime and auth alias metadata when plugins are disabled.</li>
<li>Agents/TUI: restore in-flight TUI run switch-back behavior, keep no-policy native hook fallback available, guard vanished workspaces, and keep lightweight isolated subagents lightweight.</li>
<li>Agents/media: keep async image, music, and video generation starts from ending the Codex turn, so mixed requests can continue with summaries or other work while media renders in the background.</li>
<li>Agents/Codex: keep public OpenAI API-key profiles from being treated as native Codex app-server auth while preserving persisted Codex OAuth sessions.</li>
<li>Agents/Codex: stream Codex app-server final-answer partials to live reply previews, preserve ACP metadata in SQLite, prefer real tool results over synthetic repair output, prevent aborted app-server turn handles from lingering, migrate legacy OpenAI Codex <code>lastGood</code> auth state, and preserve workspace/session metadata through ACP runtime refactors. (#88405, #88724, #88730) Thanks @vincentkoc.</li>
<li>Control UI: keep collapsed tool cards labeled with the tool name and action instead of generic output text. Thanks @shakkernerd.</li>
<li>Agents/Codex: surface Skill Workshop guidance in Codex app-server prompts when <code>skill_workshop</code> is available. Thanks @shakkernerd.</li>
<li>Skill Workshop: restore and localize the Control UI board/today view switcher so review workflows keep their intended layout toggle across locales. Thanks @shakkernerd.</li>
<li>Agents/auth: write auth profiles atomically, dispatch auth failures by type, add force re-login recovery, preserve workspaces during state-only uninstall, and compact before oversized turns so recovery paths avoid partial state. (#89181) Thanks @RomneyDa.</li>
<li>Skills: skip disabled skill env overrides from stale persisted snapshots so disabled skill <code>apiKey</code> SecretRefs cannot abort embedded or channel turns. (#79072, #79173) Thanks @zeus1959.</li>
<li>Skill Workshop: render the Control UI tab from filtered navigation state and keep filtered fallback routing stable.</li>
<li>CLI: avoid live catalog validation during <code>openclaw agents add</code>, so adding a secondary agent no longer depends on provider catalog availability. (#76284, #88314) Thanks @zhangguiping-xydt.</li>
<li>CLI: keep <code>plugins list --json</code> on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.</li>
<li>CLI/desktop: bridge WSL clipboard operations through the shell, recognize manual-update launchd jobs, and keep machine-readable startup output parseable during progress setup. (#88764, #88689) Thanks @alexzhu0.</li>
<li>Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.</li>
<li>Plugins: clarify plugin loader failure guidance so missing or incompatible plugin packages point operators at the right repair path.</li>
<li>Plugins: preserve npm plugin roots after blocked installs, skip plugin-local <code>openclaw</code> peer symlinks during rollback snapshots, relink those peers after restore, isolate cached tool runtime siblings, and isolate web-provider factory failures so one bad plugin does not poison sibling runtime paths. (#77237, #88807)</li>
<li>Cron: keep SQLite cron migrations compatible with legacy run-log tables, archived job stores, diagnostic cron names, and legacy one-shot delete-after-run behavior. (#88285)</li>
<li>Cron: keep update delivery validation scoped, harden restart state, and retire MCP runtimes on isolated cron cleanup.</li>
<li>Memory: serialize QMD update/embed writes per store, reduce Linux watcher fan-out, retry transient FileProvider-backed reads, preserve phase signals on read errors, harden envelope metadata sanitization, reattach Linux native watchers when directories are recreated, and rewrite generated transcript paths on rollover so memory/search state survives concurrent gateway and CLI activity. (#66339, #85931, #89185, #89188, #85351) Thanks @openperf, @amittell, @RomneyDa, and @NianJiuZst.</li>
<li>Memory: keep vector-disabled FTS indexes from resolving embedding providers during sync and search.</li>
<li>Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.</li>
<li>Providers: resolve Google defaults to <code>google-generative-ai</code>, register Vertex static catalog rows, align Foundry reasoning metadata, skip DeepSeek V4 thinking params on Foundry fallback, use MiniMax account OAuth endpoints, preserve Copilot Claude 1M capabilities, suppress disabled Ollama reasoning output, forward Gemini stop sequences, strip Kimi-incompatible Anthropic cache markers, keep OpenAI stop-finished tool calls, and avoid replay ids when the Responses store is disabled. (#88480, #88512, #76612) Thanks @coder999999999, @BryanTegomoh, and @vliuyt.</li>
<li>Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.</li>
<li>Cron: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot.</li>
<li>Agents/Codex: keep live session locks during cleanup, recover interrupted CLI tool transcripts, preserve Codex auth and compaction session identity, clear orphan tool state, cap app-server idle timers, and keep media completion delivery retryable. (#88129, #88136, #88141, #88162, #88182)</li>
<li>Chat/UI: show Gateway chat failures as visible assistant messages in the Control UI instead of only setting an invisible error state.</li>
<li>Channels: cap Telegram, Discord, WhatsApp, Signal, Feishu, Google Chat, Microsoft Teams, QQBot, Nostr, Zalo, Zalouser, and Nextcloud-style request/retry timers; preserve SMS approval reply routes; and retry WhatsApp QR login 408 timeouts. (#88183)</li>
<li>Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, and gateway WebSocket calls after close.</li>
<li>Providers/media: cap local service, model, usage, queue, generated media, TTS, music, workflow polling, and provider OAuth request timers across hosted and local providers.</li>
<li>Release/CI/E2E: bound release candidate reads, beta smoke REST calls, plugin npm verification commands, changelog restore, cross-OS process groups, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Telegram credential timeouts, Control UI i18n and CLI startup metadata generation, Vitest routing, dependency guard admin approvals, child workflow failure detection, quiet Node test shard stalls, docker package cleanup, and mainline test flakes. (#88127, #88137, #88155, #88160, #88966) Thanks @RomneyDa.</li>
<li>Release/CI/E2E: keep Kitchen Sink live plugin MCP probes resolving source-checkout workspace packages and align the live gauntlet with current Kitchen Sink diagnostics.</li>
<li>Release/CI/E2E: run the secret-provider integration proof through the repo pnpm runner so native macOS and Windows validation use the hydrated package-manager shim.</li>
<li>Release/CI/E2E: run the Telegram desktop proof gateway through the repo pnpm runner so native macOS proof uses the hydrated package-manager shim.</li>
<li>Docs/CI: run Mintlify anchor checks through the repo pnpm runner so docs link validation works when pnpm is only available through the hydrated package-manager shim.</li>
<li>Agents: keep configured fallback model metadata typed so provider params, context-token caps, and media input limits do not break changed-gate typechecks.</li>
<li>Agents: accept hidden <code>sessions_send</code> body aliases before validation while keeping the model-facing <code>message</code> schema canonical. (#88229) Thanks @zhangguiping-xydt.</li>
<li>Chat/UI: preserve startup chat sends during history loading, unblock the initial Control UI chat send, stream chat deltas incrementally, skip markdown parsing while streaming, keep drafts local while typing, guard composer rerenders, honor Chromium executable overrides, and detect system Chromium for E2E. (#88998) Thanks @vincentkoc.</li>
<li>Channels: stop schema-padded poll modifiers from turning normal <code>send</code> actions into invalid poll sends. (#89601) Thanks @codezz.</li>
<li>Channels: preserve long Feishu streaming replies, send visible fallbacks when accepted Feishu turns produce no final reply, tolerate iMessage self-chat timestamp skew, preserve colon-prefixed slash commands in mention parsing, decode Nostr <code>npub</code> allowlists correctly, and suppress raw provider errors during channel delivery. (#87896)</li>
<li>Config/status/doctor: skip unresolved shell references in state-dir dotenv files, resolve gateway auth secrets during deep status audits, respect explicit PI runtime policy, report runtime tool-schema errors, and keep post-upgrade JSON stable. (#88288)</li>
<li>Gateway/session state: list commands from the Gateway plugin registry, harden MCP loopback tool schemas, hide phantom agent-store rows from <code>sessions.list</code>, make task persistence failures explicit, and carry session UUIDs on interactive dispatch events.</li>
<li>Gateway/plugins: narrow plugin lookup memoization to the stable plugin/runtime inputs, avoiding repeated lookup work without mixing disabled or filtered plugin state.</li>
<li>OpenAI/TTS: handle speed directives for OpenAI TTS voices. (#74089)</li>
<li>CI/Crabbox: keep default runner capacity on the Azure credit-backed on-demand D4 lane with the Azure SSH port and a Git-independent full check job, so broad validation avoids low-priority spot quota stalls, hydrate port mismatches, non-Git hydrated workspaces, and stale AWS region hints.</li>
<li>CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.</li>
<li>CI/workflows: route workflow sanity helper edits to their guard tests and cover composite-action input interpolation checks.</li>
<li>CI/tooling: route CI scope, dependency, changelog, and docs helper edits to their owner tests instead of silently skipping changed-test coverage.</li>
<li>CI/tooling: route package, release, and install helper edits to their owner tests so changed-test gates cover publish and installer script changes.</li>
<li>CI/tooling: route shared script library edits through their owner tests so lock, process, safety, and scan helpers do not skip changed-test coverage.</li>
<li>CI/tooling: skip expensive import-graph scans once a changed diff already requires broad fallback, keeping local changed-test planning fast while still collecting explicit owner tests.</li>
<li>CI/tooling: route script edits through conventional owner tests when matching <code>test/scripts</code> or <code>src/scripts</code> coverage already exists.</li>
<li>CI/tooling: honor option terminators in the memory FD repro script so follow-on arguments are not reparsed.</li>
<li>Release/CI/E2E: assert plugin lifecycle runtime inspect output instead of only capturing it.</li>
<li>Release/CI/E2E: make gateway-network prove the advertised health RPC and retry early WebSocket closes without burning full open timeouts.</li>
<li>Release/CI/E2E: honor option terminators across release, Parallels smoke, plugin gauntlet, and extension-memory scripts.</li>
<li>Release/CI/E2E: fail plugin gateway gauntlet QA chunks when the requested suite summary is missing or invalid.</li>
<li>Performance: prebuild QA runtime probes with generated plugin assets but without CLI startup metadata.</li>
<li>Performance: skip declaration bundling for runtime-only CLI startup and gateway watch build profiles.</li>
<li>Performance: reuse prepared provider handles, strict tool schemas, gateway runtime metadata, session maintenance config, plugin metadata, bundled skill allowlists, package-local plugin artifacts, single-entry store writes, and validated/serialized session prompt blobs.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.6.1/OpenClaw-2026.6.1.zip" length="55062100" type="application/octet-stream" sparkle:edSignature="PVp8E2HBCvikB/0LCr36lFEyHPAzoFA2ScT6LW27FlzvP+m4r1AEuVN2UrtgWlpkGSsn4Eav0kPJe32u4ObNBw=="/>
</item>
<item>
<title>2026.5.28</title>
<pubDate>Sat, 30 May 2026 21:21:09 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026052890</sparkle:version>
<sparkle:shortVersionString>2026.5.28</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.5.28</h2>
<h3>Highlights</h3>
<ul>
<li>Agent and Codex runtime recovery is steadier: subagents keep cwd/workspace separation, hook context stays prompt-local, session locks release on timeout abort while live OpenClaw locks survive cleanup, stale restart continuations are avoided, and Codex app-server/helper failures no longer tear down shared runtime state. (#87218, #86875, #87409, #87399, #87375, #88129)</li>
<li>Channel delivery and session identity got safer across outbound plugin hooks, Matrix room ids, iMessage reactions/approvals, Slack final replies, Discord recovered tool warnings, runtime-config message actions, WhatsApp profile auth roots, Telegram polling, and Microsoft Teams service URL trust checks. (#73706, #75670, #87366, #87451, #87334, #84535, #82492, #83304, #87160)</li>
<li>Mobile and chat surfaces got a broader refresh: the iOS Pro UI, hosted push relay default, realtime Talk tab playback, Gateway chat transport, onboarding, Talk permissions, WebChat reconnect delivery, and session picker behavior now preserve more state across reconnects and empty searches. (#87367, #87531, #87682, #88096, #88105) Thanks @ngutman.</li>
<li>Browser, channel, and automation inputs are stricter: Browser tool timeouts, viewport/tab indices, Gateway ports, cron retry handling, Discord component ids, schema array refs, Telegram callback pages, and channel progress callbacks now reject malformed values earlier and preserve the intended delivery context. (#82887)</li>
<li>Provider, media, and document coverage expands with Claude Opus 4.8, Fal Krea image schemas, NVIDIA featured models, MiniMax streaming music responses, encrypted PDF extraction, voice model catalogs, GitHub Copilot agent runtime support, and a Codex Supervisor plugin path for delegated Codex workflows. (#87845, #87890, #80775, #84764, #87751, #87794)</li>
<li>CLI, auth, doctor, and provider paths fail faster and recover more clearly: malformed numeric/version options are rejected, workspace dotenv provider credentials are ignored, heartbeat defaults, OAuth/token lifetimes, and local service startup requests are bounded, agent auth health labels are clearer, legacy <code>api_key</code> auth profiles migrate to canonical form, and restart guidance is actionable. (#87398, #86281, #87361, #88133, #83655, #87559, #88088, #85924) Thanks @vincentkoc and @giodl73-repo.</li>
<li>Plugin and Gateway hot paths do less repeated work while preserving cache correctness for install records, config JSON parsing, tool search catalogs, session stores, manifest model rows, auto-enabled plugin config, browser tokens, viewer assets, and release-split external plugin packages. (#86699)</li>
<li>Release, QA, and E2E validation now bound more log, artifact, harness, and cross-OS waits so failing lanes produce proof instead of hanging or false-greening.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Status: show active subagent details in status output.</li>
<li>Diffs: split the default language pack and expand default Diffs language coverage while keeping the host floor aligned. (#87370, #87372) Thanks @RomneyDa.</li>
<li>ClawHub: add plugin display names plus skill verification and trust surfaces. (#87354, #86699) Thanks @thewilloftheshadow and @Patrick-Erichsen.</li>
<li>iOS: refresh the dev app with Pro Command, Chat, Agents, Settings, hosted push relay defaults, and realtime Talk playback wired to gateway sessions, diagnostics, chat, and realtime Talk. (#87367, #88096, #88105) Thanks @Solvely-Colin and @ngutman.</li>
<li>Docs: clarify Codex computer-use setup, paste-token stdin auth setup, macOS gateway sleep troubleshooting, native Codex hook relay recovery, container model auth, install deployment cards, device-token admin gating, CLI setup flow compatibility, Notte cloud browser CDP setup, and backport targets. (#87313, #63050, #87685) Thanks @bdjben, @liaoandi, and @thewilloftheshadow.</li>
<li>PDF/tools: use ClawPDF for PDF extraction, support encrypted PDF extraction, and surface MCP structured content in agent tool results. (#87670, #87751)</li>
<li>Providers: add Claude Opus 4.8 support, Fal Krea image model schemas, NVIDIA featured model catalogs, MiniMax streaming music responses, and provider-backed voice model catalogs. (#87845, #87890, #80775, #84764, #87794) Thanks @eleqtrizit and @vincentkoc.</li>
<li>Codex/GitHub: add the GitHub Copilot agent runtime and the Codex Supervisor plugin package.</li>
<li>Plugins: externalize GitHub Copilot and Tokenjuice as official install-on-demand plugins with npm and ClawHub publish metadata.</li>
<li>Workboard: add agent coordination tools for tracking and handing off active agent work.</li>
<li>Discord: show commentary in progress drafts so live Discord runs expose useful in-progress context. (#85200)</li>
<li>Plugin SDK: add a reply payload sending hook for plugins that need to deliver channel-owned replies and flatten package types for SDK declarations. (#82823, #87165) Thanks @RomneyDa.</li>
<li>Policy: add policy comparison, ingress-channel conformance, and sandbox-posture conformance checks. (#85572, #85744, #86768)</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Agents: fall back to local config pruning when the optional <code>agents delete</code> Gateway probe cannot authenticate, so offline installs can still delete agents without removing shared workspaces.</li>
<li>Tighten phone-control mutation authorization [AI]. (#87150) Thanks @pgondhi987.</li>
<li>Clarify directive persistence authorization policy [AI]. (#86369) Thanks @pgondhi987.</li>
<li>Agents/Codex: keep spawned agent cwd/workspace state separated, forward ACP spawn attachments, keep hook context prompt-local, release session locks on timeout abort and runtime teardown without deleting live OpenClaw-owned locks during cleanup, avoid session event queue self-wait, clean up exec abort listeners, stream assistant deltas incrementally, recover raw missing-thread compaction failures, preserve rotated compaction session identity, keep compaction-timeout snapshots continuable, preserve shared app-server state across startup or helper failures, keep native hook relay alive across restarts and prune stale bridge files, close native hook relay replacement races, keep Claude live tool progress visible for watchdog recovery, suppress abandoned requester completion handoff, route workspace memory through tools, resolve Codex runtime models first, report quarantined dynamic tools, format <code>skills</code> command output, bind node auto-review to prepared plans, retry Claude CLI transcript probes, and bound compaction/steering retries. (#87218, #86875, #86123, #88129, #87399, #87375, #72574, #87383, #87400, #83022, #87671, #87738, #87747, #87706, #87546, #87541, #81048) Thanks @mbelinky, @Alix-007, @luoyanglang, @yetval, @sjf, @joshavant, and @benjamin1492.</li>
<li>Codex Supervisor: keep real-home app-server MCP session listing on the loaded state path, bound stored history scans, and close WebSocket probes cleanly.</li>
<li>Channels: thread canonical session keys into outbound hooks, preserve Matrix room-id case, keep fallback tool warnings mention-inert, retain delivered Slack final replies during late cleanup, continue iMessage polling after denied reactions, suppress duplicate native exec approvals, resolve Gateway message actions against the active runtime config, preserve Telegram SecretRef prompt config and polling keepalives, preserve WhatsApp profile auth roots, QR display, document filenames, and plugin hook config, suppress Discord recovered tool warnings, preserve the Discord voice outbound helper, cap Discord/Signal/Zalo channel request and container timeouts, and block untrusted Teams service URLs while keeping TeamsSDK patterns aligned. (#73706, #75670, #87366, #87451, #87465, #87334, #84535, #76262, #83304, #82492, #87581, #77114, #86426, #85529, #87160) Thanks @zeroaltitude, @lukeboyett, @xiaotian, @funmerlin, @joshavant, @eleqtrizit, @heyitsaamir, @amittell, @liorb-mountapps, @masatohoshino, @bladin, and @giodl73-repo.</li>
<li>CLI/auth/doctor/providers: reject malformed numeric/timeout/subcommand-version inputs, ignore workspace dotenv provider credentials, wait for respawn child shutdown, bound heartbeat defaults plus Codex, GitHub Copilot, OpenAI, Anthropic, Google, Feishu, LM Studio, MiniMax, Xiaomi TTS, and local-provider OAuth/token/model requests, harden Codex auth probes, label auth health by agent, preserve explicit agentRuntime pins during Codex model migration, warm provider auth off the main thread, honor Codex response timeouts, stop migrating current Claude Haiku 4.5 profiles to Sonnet, bound local service startup, resolve GPT-5.5 without cached catalog, migrate legacy memory auto-provider config, rewrite non-canonical <code>api_key</code> auth profiles, and make doctor restart follow-ups actionable. (#87398, #86281, #87361, #88133, #83655, #87559, #87719, #88088, #85924, #84362) Thanks @Patrick-Erichsen, @samzong, @giodl73-repo, @alkor2000, @mmaps, @nxmxbbd, and @vincentkoc.</li>
<li>Gateway/security/session state: expire browser tokens after auth rotation, scope assistant idempotency dedupe, drain probe client closes, avoid stale restart continuation reuse, preserve retry-after fallbacks and stale rate-limit cooldown probes, bound webchat image and artifact transcript scans, include seconds in inbound metadata timestamps, clear completed session active runs, clear stale chat stream buffers, and evict current plugin-state namespaces at row caps. (#87810, #87833, #75089) Thanks @joshavant and @litang9.</li>
<li>Config/parsing/network: reject partial numeric parsing, parse provider/Discord retry headers and dates strictly, honor IPv6 and bare IPv6 <code>no_proxy</code> entries, preserve empty plugin allowlists, canonicalize secret target array indexes, and reject malformed media content lengths, inspected TCP ports, marketplace content lengths, cron epochs, sandbox stat fields, unsafe duration values, empty config path segments, noncanonical schema array refs, unsafe Telegram callback pages, and invalid Teams attachment-fetch DNS targets. (#87883) Thanks @zhangguiping-xydt.</li>
<li>Browser/input hardening: reject invalid tab indexes, excessive viewport resizes, explicit zero CDP ports, malformed geolocation options, unsafe screenshot or permission-grant timeouts, loose response-body limits, invalid cookie expiries, and non-finite Browser tool delays/timeouts.</li>
<li>Cron/automation: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot, and preflight model fallbacks before skipping scheduled work. (#82887)</li>
<li>Auto-reply/directives: respect provider and relayed channel metadata during directive persistence so channel-originated decisions keep their intended context. (#87683)</li>
<li>WhatsApp: resolve the auth directory from the active profile so profile-scoped WhatsApp installs do not drift to the wrong credential root. (#82492)</li>
<li>Gateway/session state: clear completed session active runs, avoid cold-loading providers for MCP inventory, cache single-session child indexes, cap handshake timers, and bound preauth, auth-guard, media, transcript, readiness, and port options.</li>
<li>Channels/replies: preserve channel-owned progress callbacks when verbose output is off, keep group-room progress suppression intact, prefer external session delivery context, escape Discord component id delimiters, force final TUI chat repaints, show Slack reasoning previews, and normalize Discord/Matrix/Mattermost channel numeric options. (#87476, #87423)</li>
<li>Agents/tool args: harden smart-quoted argument repair for edit arrays and exact escaped arguments so model-produced tool calls recover without corrupting valid input. (#86611)</li>
<li>Providers/agents: preserve seeded Anthropic signatures, preserve signed thinking payloads, concatenate signature-delta chunks, preserve DeepSeek <code>reasoning_content</code> replay across tier suffixes, apply OpenRouter strict9 ids to Mistral routes, promote Ollama plain-text tool calls, load NVIDIA featured model catalogs, stream MiniMax music generation responses, and recover empty preflight compaction. (#87593, #87493, #80775, #84764) Thanks @eleqtrizit.</li>
<li>Media/images: skip CLI image cache refs when resolving generated images, allow trusted generated HTML attachments, and bound generated video downloads so stale refs and slow providers fail cleanly. (#87523, #87982)</li>
<li>File transfer: handle late tar stdin pipe errors after archive validation or unpacking has already settled.</li>
<li>Performance: trust install-record caches between reloads, prefer native JSON parsing, reuse unchanged tool-search catalogs, reuse gateway session and plugin metadata paths, skip unchanged store serialization, patch single-entry session writes, add precomputed session patch writers, reduce store clone allocations, cache manifest model catalog rows and auto-enabled plugin config, avoid full session snapshots for entry reads, defer configured Slack full startup, prefer bundled plugin dist entries, and slim current metadata identity caches. (#87760)</li>
<li>Docker/release/QA: package runtime workspace templates, stream cross-OS served artifacts, preserve sparse Crabbox run artifacts, isolate npm plugin installs per package, reject incompatible package plugin API installs, drop the leftover root Sharp dependency from package manifests after the Rastermill migration, bound OpenClaw instance logs, plugin gauntlet relay logs, MCP channel buffers, kitchen-sink scans, agent-turn assertions, QA-Lab credential broker calls, QA Matrix substrate requests, and release scenario logs, and keep release/google live guards current. (#87647, #87477) Thanks @rohitjavvadi and @vincentkoc.</li>
<li>Release/CI: bound manual git fetches, ClawHub verifier responses, ClawHub owner metadata, dependency-guard error bodies, Parallels limits, startup/test/memory budget parsing, and diffs viewer build warnings so release lanes fail with useful proof instead of hanging. (#87839)</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.28/OpenClaw-2026.5.28.zip" length="54750142" type="application/octet-stream" sparkle:edSignature="U4O55uMdPU+OqSx9QR1ApUJ8wg65wxTydzD7iyCn1GHtm1MBK9noEeiA/yoUKkqb/bx0hzi1gNhn+ye19RXnCA=="/>
</item>
<item>
<title>2026.5.27</title>
<pubDate>Thu, 28 May 2026 12:12:19 +0000</pubDate>
@@ -240,5 +49,493 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.27/OpenClaw-2026.5.27.zip" length="54488811" type="application/octet-stream" sparkle:edSignature="c5w2T1UO6vpPs70hyYH93cIyWEOd5sl5z2NkhU53E+XQBSd+jAr+xd0qf3KzWbeX2mfXYMQmnx+VMls3L22EDg=="/>
</item>
<item>
<title>2026.5.26</title>
<pubDate>Wed, 27 May 2026 12:24:26 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026052690</sparkle:version>
<sparkle:shortVersionString>2026.5.26</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.5.26</h2>
<h3>Highlights</h3>
<ul>
<li>Faster Gateway and replies: startup avoids repeated plugin, channel, session, usage-cost, warning, scheduled-service, and filesystem scans; visible replies separate user-facing sends from slower follow-up work; Gateway runtime/session caches churn less under load.</li>
<li>Transcripts are core: transcript-backed meeting summaries, source-provider chunks, cleaned user turns, media provenance, Codex mirrors, WebChat replies, and CLI/TUI replay now use one more reliable transcript path.</li>
<li>More channels are production-ready: Telegram keeps typing/progress context and forum topics, iMessage handles attachment roots, remote media staging, and duplicate local Messages sources, WhatsApp restores group/media behavior, Discord improves voice playback and model picking, and Signal/iMessage/WhatsApp get reaction approvals.</li>
<li>Better voice and Talk: realtime Talk runs can be inspected, steered, cancelled, or followed up from Web UI and Discord voice; wake-name handling is more tolerant without letting ambient speech trigger agents.</li>
<li>Safer content boundaries: Browser snapshot reads honor SSRF policy, system-event text cannot spoof nested prompt markers, fetched file text is wrapped as external content, ClickClack inbound sender allowlists run before agent dispatch, stale device tokens are rejected, and serialized tool-call text is scrubbed from replies.</li>
<li>Providers, Codex, and local models are steadier: named auth profiles, OpenAI sampling params, Codex app-server resume/timeout/usage-limit recovery, dynamic tool-schema guards, xAI usage-limit surfacing, Ollama top-p normalization, and local approval resolution reduce provider-specific dead ends.</li>
<li>More reliable install/update/release paths: Alpine installs, trusted runtime fallback roots, stable update channels, Docker/package timeouts, Windows Scheduled Tasks, Windows/macOS proof lanes, Testbox/Crabbox delegation, plugin publish checks, and macOS runner bootstraps all got hardened.</li>
<li>Better observability: Activity tab, gateway secret-prep traces, tool/model stream progress, explicit fast-mode status, systemd Gateway hygiene, OpenTelemetry LLM spans, release performance evidence, and richer telemetry signals make failures easier to inspect.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Transcripts: add core transcript capture and source-provider support for transcript-backed meeting summaries, including the renamed Transcripts docs, CLI surface, source-provider chunks, and cleaned user-turn persistence.</li>
<li>Auth: add named model login profiles and supported credential migration for Hermes, OpenCode, and Codex auth profiles, with explicit opt-out and non-interactive controls. (#85667) Thanks @fuller-stack-dev.</li>
<li>Diagnostics: trace gateway secret preparation, classify skill/tool usage, surface model stream progress, add OpenTelemetry LLM content spans, and expose alertable telemetry for blocked tools, failover, stale sessions, liveness, oversized payloads, and webhook ingress. (#83019, #80370, #86191)</li>
<li>Channels: add Signal reaction approvals, iMessage thumb approval reactions, and WhatsApp thumb approval reaction support so mobile approval flows work without textual <code>/approve</code> commands. (#85894, #85952, #85477)</li>
<li>Agents/API: forward OpenAI sampling params through the Gateway and expose estimated context-budget status for active agent runs. (#84094)</li>
<li>TUI/status: queue prompts submitted while an agent is busy and show explicit fast-mode state plus richer systemd Gateway hygiene in status output. (#86722, #87115, #86976)</li>
<li>Exec approvals: hide durable approval actions that are unavailable for the current prompt and keep approval runtime tokens local-only so stale prompts cannot offer misleading controls. (#86270, #86359)</li>
<li>Plugin SDK: add reaction approval helpers and keep diagnostic event root exports discoverable across function-name and alias-bound module graphs. (#86735, #87084)</li>
<li>Android/iOS: add the Android pair-new-gateway action and improve mobile Talk mode surfaces, including iOS realtime Talk mode and Android offline voice/gateway recovery. (#86798, #86355) Thanks @ngutman.</li>
<li>Performance: cache plugin metadata snapshots, package realpaths, stable gateway metadata, model cost indexes, channel resolution, usage-cost indexes, and session/auth hot-path facts so common Gateway and reply paths do less rediscovery. (#84649, #85843, #86517, #86678)</li>
<li>Voice: expose shared realtime turn-context tracking through the realtime voice SDK and reuse it for Discord speaker attribution and wake-name context recovery.</li>
<li>Voice: reuse shared realtime output activity tracking in Google Meet command and node audio bridges, including recent-output checks for local barge-in detection.</li>
<li>Voice: expose shared realtime output activity tracking through the realtime voice SDK and reuse it for Discord playback activity and barge-in decisions.</li>
<li>Voice: expose shared realtime consult question matching, speakable-result extraction, and alias-aware forced-consult coordination through the realtime voice SDK, then reuse it in Gateway Talk, Voice Call, and Discord voice paths.</li>
<li>Voice: share activation-name matching and consult-transcript screening through the realtime voice SDK so Discord, browser voice, and meeting surfaces can reuse one implementation.</li>
<li>Cron: default <code>cron.maxConcurrentRuns</code> to 8 so scheduled automations and their isolated agent turns can make progress in parallel without explicit configuration.</li>
<li>QA-Lab: add <code>qa coverage --match <query></code> so focused proof selection can discover matching scenarios from existing metadata before running live or remote lanes.</li>
<li>Discord/model picker: surface an alpha-bucket select (e.g. <code>AG (12) · HN (18) · OZ (5)</code>) when the provider list or a provider's model list exceeds 25 items, so configs with <code>provider/*</code> wildcards stay one click from the right page instead of paginating through prev/next; falls back to numeric chunks when every item shares the same first letter.</li>
<li>Control UI: add an ephemeral Activity tab for sanitized live tool activity summaries without persisting raw telemetry. Fixes #12831. Thanks @BunsDev.</li>
<li>Build: include <code>ui:build</code> in the <code>full</code> and <code>ciArtifacts</code> profiles of <code>scripts/build-all.mjs</code> so <code>pnpm build</code> always rebuilds <code>dist/control-ui</code> after <code>tsdown</code> cleans <code>dist</code>, removing the second-command requirement and the missing-asset failure mode for source/runtime installs and CI artifact uploads. (#85206)</li>
<li>iOS: improve Talk mode with direct realtime voice sessions, compact toolbar status, and responsive voice waveform feedback. (#86355) Thanks @ngutman.</li>
<li>Media: replace the Sharp image backend with Rastermill for metadata, resizing, EXIF orientation, and PNG alpha-preserving optimization so OpenClaw no longer installs Sharp or the WhatsApp Jimp fallback for image processing. (#86437)</li>
<li>Codex: update the bundled Codex CLI to 0.134.0 and keep native compaction disabled for budget-triggered app-server turns so OpenClaw owns the recovery boundary. (#86772)</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Memory/security: reject prompt-like text submitted through the explicit <code>memory_store</code> tool before embedding or storage, matching the existing auto-capture prompt-injection filter. (#87142)</li>
<li>Gateway/security: enable the default auth rate limiter for remote non-browser and HTTP gateway auth failures when <code>gateway.auth.rateLimit</code> is unset, while preserving the loopback exemption. (#87148)</li>
<li>Security/content boundaries: validate Browser snapshot tab URLs against SSRF policy before ChromeMCP or direct CDP reads, sanitize queued system-event text so untrusted plugin/channel labels cannot spoof nested prompt markers, wrap fetched file text and metadata as external content, apply ClickClack <code>allowFrom</code> sender allowlists before agent dispatch, reject RPCs from invalidated device-token clients during rotation, require staged sandbox media refs, and scrub serialized tool-call text from replies. (#78526, #87094, #87062, #83741, #70707, #86924) Thanks @zsxsoft, @ttzero25, and @mmaps.</li>
<li>Transcripts/user turns: persist CLI, WebChat, media, follow-up, hook, and Codex-mirror user turns to the admitted session target; keep cleaned transcript text, inline image routing, provenance metadata, replay hooks, and fallback paths idempotent when runtimes fail or restart.</li>
<li>TUI/status/onboarding/UI: queue busy TUI prompts instead of dropping them, preserve the configured default model during onboarding, show failed tool results as errors, show config-open failures in Control UI, keep status JSON plugin scans healthy, preserve xAI usage-limit errors locally, and expose explicit fast-mode/systemd state. (#86722, #87000, #85786, #87108, #87001, #86614, #87115, #86976)</li>
<li>Plugin commands/SDK: preserve plugin LLM command auth, bind native plugin command dispatch to the host agent's LLM auth, keep <code>onDiagnosticEvent</code> exports discoverable through <code>Function.name</code>, stabilize diagnostic event root aliases, correlate pathless read diagnostics, suppress transient runner failures in channel command paths, and repair local approval resolution. (#85936, #87084, #86977, #87069, #86771)</li>
<li>Codex/providers: keep WebChat delivery hints out of user prompts, avoid false queued-terminal idle timeouts, share the native hook relay registry, quarantine unsupported dynamic tool schemas, preserve Claude resumed-session system prompts, normalize greedy Ollama <code>top_p</code>, preserve per-agent thinking defaults for ingress runs, and avoid native compaction takeover on budget-triggered Codex turns. (#87096, #73950, #87049, #86689, #86772)</li>
<li>Gateway/perf/release: reuse startup-warning metadata and prepared auth stores, avoid cloning live-switch and lifecycle session caches on read paths, defer warning and scheduled-service fallback imports, trim Gateway session/startup/runtime CPU churn, skip duplicate turn session touches, stop chat timeout fallback cascades, drop stale subagent announce history, bound benchmark/watch/kitchen-sink teardown waits, bound macOS/package/onboarding/plugin smoke commands, bound install finalization probes, resolve Parallels npm-update commands from guest <code>PATH</code>, and bootstrap raw AWS macOS Node/pnpm commands through <code>/usr/bin/env</code>. (#86997)</li>
<li>Reply/perf: reduce visible reply delivery latency by preserving Telegram typing/progress context, lazy-loading slash-command startup metadata, avoiding hot-path model hydration, flag-gating Codex profiler timing, deferring context compaction maintenance, and tracking delivery timing. (#86989, #86990, #86991, #86992, #86993, #86994) Thanks @keshavbotagent.</li>
<li>Reply/source delivery: keep TUI, Control UI, media, TTS, transcript, and Codex source-reply finals live without duplicate terminal events or stale replay artifacts.</li>
<li>Agents/replay: repair legacy tool results before replay, preserve <code>sessions_spawn</code> transcript payloads, restore current guard checks, stage sandboxed workspace media, and keep duplicate transcripts tool display metadata from reappearing. (#82203, #86934, #87025) Thanks @martingarramon, @vincentkoc, and @joshavant.</li>
<li>Agents/sessions: handle active-fallback failures in <code>sessions_send</code> so fallback routing reports the real failure and does not leave callers with an ambiguous dropped send. (#86638)</li>
<li>Agents/hooks/subagents: enforce default hook agent allowlists, recover failed subagent lifecycle completions, and keep node task lifecycle cleanup from closing the Gateway listener. (#86101)</li>
<li>Codex: project newer OpenClaw chat history into resumed app-server threads and keep Codex turn timeouts inside the Codex runtime boundary so timeouts do not poison shared app-server clients or fall through to unrelated provider fallback. (#86677, #86476) Thanks @TurboTheTurtle and @pashpashpash.</li>
<li>Config/doctor/update: narrow profiled tool-section doctor repair, keep runtime-injected legacy web-search provider config out of user-authored config validation, and keep prerelease tags excluded from stable updater resolution. (#87030, #86818, #86559) Thanks @joshavant, @luoyanglang, and @stevenepalmer.</li>
<li>CLI/Windows: add a Windows-only stack-size respawn for stack-heavy startup paths, default CLI logs to local timestamps, and validate timeout/banner TTY state more strictly. (#87031, #85387) Thanks @giodl73-repo and @vincentkoc.</li>
<li>Locking/security: require owner identity proof before stale plugin lock removal, memoize session lock owner arguments, and avoid writing default exec approval stores unless policy state actually changed. (#86814, #86964) Thanks @Alix-007 and @vincentkoc.</li>
<li>Install/release: bound Docker package build, inventory, pack, and tarball preparation with process-group timeouts; pin shrinkwrap patch drift to the pnpm lock; harden macOS restart and dSYM packaging; and run release Docker/live timeout wrappers in the foreground so child processes cannot wedge gates.</li>
<li>Telegram/network: treat <code>ENETDOWN</code> as a transient pre-connect network failure so Telegram sends, gateway unhandled-rejection handling, and cron network retries follow the same recovery path as sibling network outages. (#86762) Thanks @TurboTheTurtle.</li>
<li>Telegram: preserve inbound text entities, overlapping DM replies, account topic cache sidecars, outbound reply context, targeted bot-command mentions, durable group retry targets, forum topic names, and native progress callbacks. (#83873, #85361, #85555, #85656, #85709, #86299, #86553) Thanks @SebTardif, @luoyanglang, and @neeravmakwana.</li>
<li>iMessage: read image attachments from local Messages attachment roots, dedupe duplicate local Messages-source accounts, seed direct DM history, fix image/group media attachment commands, advance catchup cursors after live handling, and keep slash-command acknowledgements in the source conversation. (#82642, #85475, #86569, #86705, #86706, #86770) Thanks @homer-byte, @TurboTheTurtle, @swang430, and @OmarShahine.</li>
<li>WhatsApp/QQ/Twitch/IRC/Slack: restore WhatsApp ack identity and group-drop warnings, make QQ Bot media respect <code>OPENCLAW_HOME</code>, serialize Twitch auth disconnects, store IRC channel routes canonically, and keep Slack downloaded files out of reply media. (#83833, #85309, #85777, #85794, #85906, #86318, #86697) Thanks @sliverp, @neeravmakwana, and @Kailigithub.</li>
<li>Discord/voice: improve voice playback and wake replies, bucket large model picker menus, merge media captions into one message, route metadata through configured proxies, restore numeric channel sends, suppress self-reply echoes, and tighten wake matching without breaking fuzzy wake phrases. (#80227, #86238, #86487, #86571, #86595, #86601)</li>
<li>Codex: preserve native web-search metadata, keep oversized native thread reuse, bridge CLI API-key auth into the app server, preserve sandbox bootstrap path style, recover context-window prompt errors, honor yolo approval policy, disable native thread personality, and route compaction through Codex auth. (#85378, #85542, #85891, #85909, #86408)</li>
<li>Agents/runtime: enforce session lock max-hold reclaim, release embedded-attempt locks on all exits, treat aborted subagent runs as terminal, avoid runtime model hydration on hot paths, disclose scoped session list counts, derive overflow budgets from provider errors, and keep fallback errors scoped to the active model candidate. (#70473, #85764, #86014, #86134, #86427, #86944) Thanks @openperf, @fuller-stack-dev, @zhangguiping-xydt, and @ferminquant.</li>
<li>Config/update/doctor: retry config recovery after failed backup restore, skip shell env fallback on Windows, exclude prerelease tags from the stable git channel, support deep config edits, warn instead of aborting on unreadable cron stores, prune stale bundled plugin paths, and avoid duplicate restart prompts when the Gateway is already healthy. (#85739, #85787, #86060, #86260, #86384, #86533) Thanks @liaoyl830.</li>
<li>Install/release: support Alpine CLI installs and runtime floors, prefer trusted startup argv runtime fallback roots, reject stale CLI node runtimes, avoid npm <code>min-release-age</code> installer failures, bound npm/package/Docker install phases, restore config parent ownership in Docker, seed Docker lockfile package tarballs before prune, make release/plugin prerelease checks fail closed instead of hanging or false-greening, and use host-visible Crabbox local work roots for Docker-backed proof. (#85491)</li>
<li>Windows daemon: keep Scheduled Task gateway launches running on battery power and avoid workgroup-machine prompts for a domain user during task installation. (#59299)</li>
<li>Security: avoid printing Gateway tokens in Docker, validate plugin model-pattern regexes safely, escape transcript metadata field names, harden session allowlist glob matching, audit Claude permission overrides under YOLO, and require explicit allow for ACP auto approvals. (#85849, #85934, #86046, #86557)</li>
<li>Media/images: replace Sharp with Rastermill, keep EXIF normalization best-effort, normalize HEIC/HEIF before image descriptions, route Codex image API keys through OpenAI, preserve image compression metadata, and auto-scale live tool result caps. (#85776, #86037, #86437, #86857, #86923)</li>
<li>Memory: prevent semantic vector indexes from silently degrading when embeddings are unavailable, stop doctor OOMs on large session stores, preserve sidecar hooks/artifacts, write fallback dream diaries, use CJK-aware dreaming dedupe, and avoid per-file watcher FD fan-out. (#80613, #82928, #85060, #85704, #85967, #86701) Thanks @brokemac79, @openperf, and @yaaboo-gif.</li>
<li>Agents/sessions: include visibility metadata on restricted <code>sessions_list</code> results so scoped counts are clearly reported without widening access or exposing hidden-session counts. (#86944) Thanks @ferminquant.</li>
<li>Gateway/DNS: validate wide-area discovery domains before deriving zone paths or writing zone files, so invalid <code>discovery.wideArea.domain</code> and <code>dns setup --domain</code> values fail with a DNS-name diagnostic instead of falling through to unrelated configuration errors. Thanks @mmaps.</li>
<li>Agents/BTW: route fallback side-question streams through the embedded stream resolver so Anthropic-compatible MiniMax requests use the same capped transport as normal chat. (#86312) Thanks @neeravmakwana.</li>
<li>Telegram: treat <code>/command@TargetBot</code> bot-command entities as explicit mentions for the addressed bot so <code>requireMention</code> groups no longer drop targeted commands or captions. Fixes #84462. (#86553) Thanks @luoyanglang.</li>
<li>CI: bound Docker/Bash E2E tarball npm installs with <code>OPENCLAW_E2E_NPM_INSTALL_TIMEOUT</code> so package, onboarding, plugin, and upgrade lanes fail instead of hanging on a stuck npm install.</li>
<li>CI: fail Parallels npm-update smoke jobs after the guest command timeout and cleanup backstop instead of only logging a timeout line.</li>
<li>CI: bound kitchen-sink RPC HTTP probes so stalled gateway readiness or response bodies fail and retry instead of wedging the walker.</li>
<li>CI: keep <code>OPENCLAW_TESTBOX=1 pnpm check:changed</code> delegating to Blacksmith Testbox through Crabbox without forwarding local Testbox or worker env into the remote command.</li>
<li>CI: send KILL after the TERM grace period for manual checkout fetch timeouts so stuck Testbox and workflow checkout retries cannot hang behind a wedged <code>git fetch</code>.</li>
<li>CI: send KILL after the TERM grace period for Bun global install smoke command timeouts so trapped <code>openclaw</code> child processes cannot wedge the scheduled install smoke.</li>
<li>iMessage: thread current channel/account inbound attachment roots into the image tool so iMessage-saved attachments under <code>~/Library/Messages/Attachments</code> (including the wildcard <code>/Users/*/Library/Messages/Attachments</code> root) are read through the existing inbound path policy instead of being rejected as <code>path-not-allowed</code>. Literal <code>localRoots</code> stays workspace-scoped. Fixes #30170. (#86569)</li>
<li>QQ Bot: respect <code>OPENCLAW_HOME</code> for outbound media path resolution so <code><qqmedia></code> sends no longer silently fail when <code>HOME</code> and <code>OPENCLAW_HOME</code> differ (Docker / multi-user hosts). Persisted QQ Bot data (sessions, known users, refs) stays anchored on the OS home for upgrade compatibility. Fixes #83562. Thanks @sliverp.</li>
<li>Update: report the primary malformed <code>openclaw.extensions</code> payload error without adding a duplicate missing-main diagnostic. (#86596) Thanks @ferminquant.</li>
<li>Control UI: keep host-local Markdown file paths inert while preserving app-relative links. (#86620) Thanks @BryanTegomoh.</li>
<li>Gateway: dampen repeated unauthenticated device-required probes per URL while preserving explicit-auth and paired recovery paths. (#86575) Thanks @ferminquant.</li>
<li>IRC: store inbound channel routes with the canonical <code>channel:#name</code> target and join transient channel sends before writing. (#85906) Thanks @Kailigithub.</li>
<li>Usage: surface unknown all-zero model pricing as missing cost entries instead of a confident <code>$0</code> total. (#85882) Thanks @MichaelZelbel.</li>
<li>Agents/Codex: honor yolo app-server approval policy only for the full <code>never</code> plus <code>danger-full-access</code> case. (#85909) Thanks @earlvanze.</li>
<li>Gateway/Gmail: clear Gmail watcher renewal intervals on re-entry so hot reloads do not leak lifecycle timers. (#82947) Thanks @SebTardif.</li>
<li>Logging: exit cleanly on broken stdout/stderr pipes without masking existing failure exit codes. (#80059) Thanks @pavelzak.</li>
<li>Gateway/security: escape transcript metadata field names while extracting oversized session line prefixes. (#85934) Thanks @SebTardif.</li>
<li>Plugins/security: validate manifest model pattern regexes with the safe-regex compiler so unsafe patterns are ignored before matching. (#86046) Thanks @SebTardif.</li>
<li>Discord: route gateway metadata REST lookups through the configured Discord proxy so proxied accounts do not fall back to direct <code>discord.com</code> connections before opening the WebSocket. Fixes #80227. Thanks @Clivilwalker.</li>
<li>Agents/media: hydrate current-turn image attachments from filename-derived MIME types so active vision can see generated or forwarded images whose source omitted an image content type. (#84812) Thanks @marchpure.</li>
<li>Agents/fs: point workspace-only scratch-path guidance at in-workspace temp directories while keeping host-root writes rejected by the tool guard. (#86501) Thanks @tianxiaochannel-oss88.</li>
<li>Agents/media: keep async cron media completions scoped to their run session while preserving direct delivery for stale generated-media success and failure notifications. (#86529) Thanks @ai-hpc.</li>
<li>Gateway: emit plugin <code>session_end</code>/<code>session_start</code> hooks when <code>agent.send</code> rotates or replaces a session id, keeping hook lifecycle state aligned with <code>sessions.changed</code> notifications. Fixes #83507. (#85875) Thanks @brokemac79.</li>
<li>OpenShell/SSH: reject malformed generated exec commands before sandbox/session setup so unresolved workflow placeholders fail fast instead of reaching the remote shell. Fixes #72373. Thanks @brokemac79.</li>
<li>Google: stop normalizing <code>gemini-3.1-flash-lite</code> to the retired preview endpoint and update Flash Lite alias guidance to the GA model id. Fixes #86151. (#86240) Thanks @SebTardif.</li>
<li>Installer: make Alpine apk installs cover Git, verify the Node runtime floor, try <code>nodejs-current</code>, and report Alpine version guidance when repositories only provide older Node packages.</li>
<li>Agents/status: prefer the active Claude CLI OAuth auth label over an unused Anthropic env API-key label for equivalent runtime aliases. Fixes #80184. (#86570) Thanks @brokemac79.</li>
<li>Agents/media: send direct fallback for generated media still missing after an active requester wake fails. (#85489) Thanks @fuller-stack-dev.</li>
<li>Agents: derive overflow compaction budgets from provider-reported and synthetic over-budget token counts so confirmed context overflows compact before retrying. (#70473) Thanks @fuller-stack-dev.</li>
<li>Agents/Codex: recover Codex context-window prompt errors through overflow compaction and surface reset guidance when recovery is exhausted. (#85542) Thanks @fuller-stack-dev.</li>
<li>Agents/Codex: allow Codex app-server runs to bootstrap from <code>CODEX_API_KEY</code> or <code>OPENAI_API_KEY</code> when no Codex auth profile is configured.</li>
<li>Agents/Codex: keep selected Codex runtime routing on OpenAI-Codex while preserving direct OpenAI API-key compaction fallback. (#86408) Thanks @funmerlin and @VACInc.</li>
<li>Agent transcript: include OpenClaw agent session logs when finding local transcript candidates.</li>
<li>Crabbox: bootstrap raw AWS macOS shell commands wrapped in absolute <code>time</code> paths so RSS probes can run Node and pnpm on fresh macOS runners.</li>
<li>Crabbox: bootstrap raw AWS macOS shell commands even when setup statements precede Node or pnpm usage.</li>
<li>TUI/local: skip unnecessary secret resolution, gateway model catalog loading, bootstrap, and skill scans in explicit local-model runs so startup reaches the model request faster.</li>
<li>Sessions/doctor: load large session stores without clone amplification during read-only doctor checks and reclaim stale <code>sessions.json.*.tmp</code> sidecars. Fixes #56827. Thanks @openperf.</li>
<li>Tests: clean successful plugin gateway gauntlet isolated temp roots while keeping an explicit preservation switch for failed/debug runs.</li>
<li>Plugins/perf: reuse derived plugin metadata snapshots for the lifetime of the process so reply-time skill setup no longer rescans plugin metadata on every turn.</li>
<li>Discord/OpenAI voice: keep wake-name master consults using the current speaker context after ignored ambient transcripts and shorten the default capture silence grace.</li>
<li>Doctor: skip redundant Gateway restart prompts when a recent supervisor restart leaves the Gateway healthy. Fixes #86518. (#86533) Thanks @liaoyl830.</li>
<li>Cron: restore suspended cron lanes to the configured/default concurrency instead of falling back to one after quota or circuit-breaker auto-resume.</li>
<li>Gateway: keep session-only Control UI tool-start mirrors flowing during diagnostic queue pressure instead of silently dropping non-terminal tool updates.</li>
<li>Agents/memory: return optional not-found context for missing date-only daily memory reads instead of logging benign first-run <code>ENOENT</code> failures. Fixes #82928. Thanks @galiniliev.</li>
<li>Discord: merge streamed text captions into following media block replies so captions and attachments send as one message. (#86487) Thanks @neeravmakwana.</li>
<li>Gateway: avoid sending duplicate tool-event frames to Control UI connections that are subscribed by both run and session.</li>
<li>Discord/OpenAI voice: accept broader edge-position fuzzy wake-name transcripts while keeping ambient speech gated.</li>
<li>Discord/OpenAI voice: accept longer leading wake-name mistranscripts such as "Open Club" for OpenClaw.</li>
<li>Agents/OpenAI-compatible: stop ModelStudio-compatible chat requests before sending system/tool-only payloads that have no usable user or assistant turn. (#86177) Thanks @TurboTheTurtle.</li>
<li>Gateway/plugins: reuse plugin package realpath checks while building installed plugin indexes so startup avoids repeated filesystem resolution work.</li>
<li>Kilo Gateway: send string <code>stop</code> sequences as arrays so Kilo accepts OpenAI-compatible chat completions. (#86461) Thanks @SebTardif.</li>
<li>Discord/OpenAI voice: accept leading fuzzy wake-name transcripts such as "Monty" or "Moti" for a Molty agent while keeping ambient speech gated.</li>
<li>Media understanding: convert HEIC and HEIF images to JPEG before image description providers run so iPhone photos work in direct and configured image-description flows. (#86037)</li>
<li>Agents: release embedded-attempt session locks from outer teardown so post-prompt exceptions cannot wedge later requests behind <code>SessionWriteLockTimeoutError</code>. Fixes #86014. Thanks @openperf.</li>
<li>Discord/OpenAI voice: rotate Realtime sessions at provider max duration without logging the expected session-expiry event as an error.</li>
<li>Sessions: skip metadata-only entries during QMD-slugified session lookup so one incomplete row does not block transcript hit resolution. (#86327) Thanks @abnershang.</li>
<li>Agents/media: derive bundled plugin local-media trust from plugin tool metadata instead of importing the full plugin registry on subscription paths. (#84409) Thanks @samzong.</li>
<li>Image tool: keep config-backed custom-provider API keys usable for auto-discovered vision models, including deferred image-tool execution without env keys or auth profiles. (#85733)</li>
<li>Memory/local embeddings: run local GGUF embeddings in an isolated worker sidecar and degrade to configured fallback or keyword search on worker failure so native embedding crashes do not take down the Gateway. (#85348) Thanks @osolmaz.</li>
<li>Gateway: clear the runtime config snapshot before <code>SIGUSR1</code> in-process restarts so config changes survive the next gateway loop. (#86388) Thanks @XuZehan-iCenter.</li>
<li>Models: show OAuth delegation markers as configured <code>models.json</code> auth while keeping runtime route usability checks strict. (#86378) Thanks @rohitjavvadi.</li>
<li>Cron: seed active scheduled and manual cron task rows with a progress summary so status surfaces do not look blank while jobs run. (#86313) Thanks @ferminquant.</li>
<li>Cron: preserve unsupported persisted cron payload rows during routine store writes while keeping those rows non-runnable. Fixes #84922. (#86415) Thanks @IWhatsskill.</li>
<li>Updater: exclude prerelease git tags from stable channel resolution so source updates do not check out newer alpha/rc/preview/canary tags. (#86260) Thanks @stevenepalmer.</li>
<li>Security/Audit: flag webhook <code>hooks.token</code> reuse of active Gateway password auth in <code>openclaw security audit</code> while keeping password-mode startup compatibility. (#84338) Thanks @coygeek.</li>
<li>QQBot: derive the outbound reply watchdog from configured agent and provider timeouts so slow local model replies are not cut off at five minutes. Fixes #85267. (#85271) Thanks @SymbolStar.</li>
<li>Agents/heartbeat: stop heartbeat turns after the first valid <code>heartbeat_respond</code> so repeated response loops do not burn tokens. (#86357) Thanks @udaymanish6.</li>
<li>Tasks: keep retained lost tasks out of default status health counts, explain their cleanup window during maintenance, and prune lost task records after 24 hours instead of the general 7-day terminal retention.</li>
<li>Memory-core: keep REM dreaming focused on live light-staged memories and mark staged entries as considered so old recall history no longer dominates fresh candidates. (#86302) Thanks @SebTardif.</li>
<li>Memory: abort sync instead of downgrading an existing semantic vector index to FTS-only when the configured embedding provider is temporarily unavailable. (#85704) Thanks @yaaboo-gif.</li>
<li>Telegram: propagate forum topic names through the account-scoped topic cache for native command context and topic create/edit actions. (#86299) Thanks @SebTardif.</li>
<li>Slack: keep downloaded read-only files out of reply media so Slack file reads do not echo files back to the conversation. (#86318) Thanks @neeravmakwana.</li>
<li>Cron: accept leading-plus relative durations such as <code>+5m</code> for one-shot <code>--at</code> schedules. (#86341) Thanks @mushuiyu886.</li>
<li>Agents/media: preserve async-started media tool metadata so background generation starts no longer surface generic incomplete-turn warnings while replay stays unsafe. (#85933) Thanks @fuller-stack-dev.</li>
<li>Docker E2E: dedupe scheduler lane resources so npm/service package lanes are not over-counted and serialized unnecessarily.</li>
<li>QA/diagnostics: add a collector-backed OpenTelemetry smoke lane, make the OTLP payload leak check scenario-aware, and keep source QA builds from failing on optional dependency imports resolved through pnpm's temp module path.</li>
<li>Crabbox: bootstrap Git metadata for sparse remote changed gates so raw synced workspaces can run <code>pnpm check:changed</code> from the intended diff.</li>
<li>xAI/LM Studio: avoid buffering ordinary bracketed or <code>final</code> prose until stream completion while watching for plain-text tool-call fallbacks.</li>
<li>Doctor: warn and continue when the cron job store exists but cannot be read so later health checks still run. Fixes #86102. (#86384) Thanks @1052326311.</li>
<li>Discord: suppress a bot's previous reply body and referenced media from prompt context when a user replies to that bot message, while keeping reply metadata for routing. (#86238) Thanks @fuller-stack-dev.</li>
<li>Discord: restore bare numeric channel IDs for outbound message-tool sends while keeping explicit DM targets unambiguous. (#86571) Thanks @joshavant.</li>
<li>Docker E2E: avoid rebuilding the Control UI twice while preparing the shared OpenClaw package tarball for package-backed scenario runs.</li>
<li>Tests: avoid rebuilding the Control UI twice during the installer Docker smoke now that <code>pnpm build</code> includes <code>ui:build</code>.</li>
<li>Tests: give QA config mutation RPCs enough native Windows budget to finish gateway config writes and restart settle after hot scenario runs.</li>
<li>Tests: keep the gateway restart-inflight QA scenario focused on restart recovery on native Windows by allowing expected embedded prompt handoff errors and using the Windows-safe timeout budget.</li>
<li>QA-Lab: make the synthetic OpenAI provider honor generic <code>reply exactly:</code> directives after required kickoff reads so restart-recovery scenarios do not fall through to generic repo-summary prose.</li>
<li>Gateway: abort active <code>agent</code> RPC runs during forced restart shutdown so stale in-process turns cannot keep writing a session after the Gateway lifecycle restarts.</li>
<li>Crabbox: sync clean sparse worktrees through a temporary full checkout even when reusing an existing lease so tracked build-time files are not omitted.</li>
<li>Build: route <code>scripts/ui.js</code> through the shared pnpm runner and keep Control UI chunking helpers in sparse-included source so native Windows Corepack builds can produce <code>dist/control-ui</code>.</li>
<li>Tests: give the memory fallback QA scenario enough turn budget to exercise native Windows gateway runs instead of failing on the client timeout while the mock agent is still dispatching.</li>
<li>Tests: collect QA gateway CPU/RSS metrics on native Windows and give the channel baseline enough turn budget to report slow gateway runs instead of timing out before proof.</li>
<li>Install/update: bypass npm <code>min-release-age</code> policies with <code>--min-release-age=0</code> instead of <code>--before</code> so hosted installers keep working on npm versions that reject the combined config. (#84749) Thanks @TeodoroRodrigo.</li>
<li>Diagnostics: reclaim wedged session lanes when stale active-run bookkeeping blocks queued work despite no forward progress. Fixes #85639. Thanks @openperf.</li>
<li>WebChat: keep message-tool replies visible in the chat while still summarizing internal tool results for the model. Fixes #86347. Thanks @shakkernerd.</li>
<li>Gateway/perf: fail startup benchmark samples when the Gateway process exits before benchmark teardown, including signal deaths after readiness probes.</li>
<li>Gateway/perf: fail restart benchmark samples when the Gateway exits before benchmark teardown, including clean exits and signal deaths after successful restart probes.</li>
<li>Agents/tests: keep model catalog visibility on static selection helpers so catalog visibility checks avoid the broad model-selection barrel import.</li>
<li>Agents/commitments: serialize commitment store load-modify-save writes so concurrent heartbeat and CLI updates no longer lose dismissal, sent, or attempt state. (#81153) Thanks @ai-hpc.</li>
<li>xAI/LM Studio: promote plain-text tool-call fallbacks into structured tool calls and strip leaked internal tool syntax before user-facing delivery. (#86222) Thanks @fuller-stack-dev.</li>
<li>CLI: suppress benign self-update version-skew warnings during package post-update finalization.</li>
<li>Gateway/perf: tighten restart and startup benchmark failure handling so long profiling runs, failed probes, and fresh Linux runners no longer produce false passing or <code>n/a</code> results.</li>
<li>Checks: keep intentional Knip unused-file findings optional so full CI and sparse proof workspaces stay aligned.</li>
<li>Docker: restore writable <code>~/.config</code> in runtime images. Fixes #85968. Thanks @hkoessler and @Bartok9.</li>
<li>Plugin SDK: keep legacy root diagnostic subscriptions connected when built plugin SDK aliases resolve diagnostic helpers through a separate module graph.</li>
<li>Diagnostics: export alertable OTel and Prometheus signals for blocked tools, model failover, stale sessions, liveness warnings, oversized payloads, and webhook ingress while fixing shared OTLP endpoints with query strings.</li>
<li>Tests: normalize macOS canonical temp paths in exec allowlists, fs-safe trash assertions, installed plugin matching, Telegram topic-name stores, and built ACPX MCP server expectations so native macOS proof runners cover the intended behavior.</li>
<li>Codex/app-server: preserve message-tool-only source reply delivery mode on active runs so sub-agent completion wakeups can steer the active Codex turn instead of being rejected. (#86287) Thanks @ferminquant.</li>
<li>Tests: sample the Windows kitchen-sink RPC gateway directly and serialize RSS probes so native runs keep the memory guard active.</li>
<li>Tests: normalize bundled plugin lifecycle probe paths and state-root lookup so native Windows release sweeps accept valid packaged plugin installs.</li>
<li>Agents/Claude CLI: route live native Bash permission requests through OpenClaw exec policy so Claude turns no longer stall on <code>control_request</code>, and document that OpenClaw exec policy is authoritative. Fixes #80819. (#86330, from #81971) Thanks @guthirry and @sallyom.</li>
<li>Security audit: warn when YOLO OpenClaw exec policy overrides a restrictive raw Claude <code>--permission-mode</code> for managed live sessions. (#86557) Thanks @sallyom.</li>
<li>Config: keep benign legacy metadata write anomalies out of default doctor and config command output while preserving explicit anomaly logging for diagnostics.</li>
<li>Codex: log when implicit app-server <code>never</code> approvals are promoted for OpenClaw tool policy, including whether the trigger was a <code>before_tool_call</code> hook or trusted tool policy.</li>
<li>Codex harness: make subscription usage-limit errors without reset times explain that OpenClaw cannot determine the reset and point users to wait until Codex is available, use another Codex account, or switch to another configured model/provider. Thanks @amknight.</li>
<li>Google Vertex: support production ADC modes such as Workload Identity Federation, service-account credentials, and metadata-server ADC for the native Vertex transport. (#83971) Thanks @damianFelixPago.</li>
<li>Telegram: route normal <code>[telegram][diag]</code> polling diagnostics through <code>runtime.log</code> while keeping non-diag warnings and persistence failures on <code>runtime.error</code>, so healthy polling startup no longer looks like an error. Fixes #82957. (#82958) Thanks @galiniliev.</li>
<li>Providers/Ollama: strip inline Kimi cloud reasoning prefixes from streamed and final visible replies while keeping ordinary Kimi answers append-only. (#86286) Thanks @jason-allen-oneal.</li>
</ul>
<ul>
<li>Gateway: require Talk secret authority before setup-code handoff can include Talk secrets. (#85690) Thanks @ngutman.</li>
<li>Agents: keep fallback error reporting scoped to the active model candidate so stale prior-provider quota/auth text is not reported for later fallback attempts. (#86134) Thanks @zhangguiping-xydt.</li>
<li>iMessage: dedupe watcher startup when <code>channels.imessage.accounts</code> lists both <code>default</code> and a named account that point at the same local Messages source, so the gateway no longer spawns two <code>imsg rpc</code> processes or doubles inbound replies; the dedupe is scoped to watcher startup, leaving duplicate accounts addressable for outbound sends, status, and capability listings, and <code>openclaw doctor</code> flags the redundant account with a rebinding hint. Fixes #65141. (#86705) Thanks @swang430.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.26/OpenClaw-2026.5.26.zip" length="54484748" type="application/octet-stream" sparkle:edSignature="y4WXG7JT8ktJ+K7YDgllY7u5Z9BSKR/SwGiwEh0gikOJ/SWqwcQd+z2tWa2zgwvCJKWsAUFwJs1ATor880SUBg=="/>
</item>
<item>
<title>2026.5.22</title>
<pubDate>Sun, 24 May 2026 01:41:27 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026052290</sparkle:version>
<sparkle:shortVersionString>2026.5.22</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.5.22</h2>
<h3>Changes</h3>
<ul>
<li>Gateway/perf: reuse process-stable channel catalog reads, avoid repeated bundled-channel boundary checks, and rotate gateway watch CPU profiles so benchmark runs do not accumulate unbounded artifacts.</li>
<li>Gateway/perf: reuse immutable plugin metadata snapshots across startup, config, model, channel, setup, and secret metadata readers so hot paths avoid repeated plugin file stats and manifest registry reloads.</li>
<li>Gateway/perf: lazy-load startup-idle plugin work, core gateway method handlers, and the embedded ACPX runtime so Gateway health and ready signals no longer wait on unused handler trees or ACPX probes.</li>
<li>Gateway/perf: cache plugin SDK public-surface alias maps and skip irrelevant macOS Linuxbrew PATH probes so Gateway startup avoids repeated filesystem walks and slow missing-directory stats.</li>
<li>Meeting Notes: add a source-only external meeting-notes plugin and SDK source-provider contract outside the core npm package, with auto-start capture config, manual transcript imports, read-only <code>openclaw meeting-notes</code> CLI access, and Discord voice as the first live source.</li>
<li>Docs/channels/config: add Signal <code>configPath</code>, Telegram wildcard topic defaults, local-time backup archive names, Termux home fallback, include-path validation, secret-scanner-safe placeholder guidance, Gemini CLI/Antigravity media guidance, and macOS VM auto-login guidance. Thanks @NorseGaud, @yudistiraashadi, @huangqian8, @VibhorGautam, @maweibin, @tianxingleo, @IgnacioPro, and @xzcxzcyy-claw.</li>
<li>Docs: clarify model-usage portability, Codex migration prerequisites, status bootstrap wording, thread-bound subagent limits, hook ownership, and config-preserving safety guidance. Thanks @aniruddhaadak80, @leno23, @TomDjerry, @matthewxmurphy, @vincentkoc, and @stablegenius49.</li>
<li>Docs: clarify README onboarding and Gateway startup paths, WhatsApp QR/408 recovery, cron output language prompts, skill advanced features, gateway upstream 403 troubleshooting, and plugin fallback override guidance. Thanks @deepujain, @Zacxxx, @Jah-yee, @neyric, @usimic, @Renu-Cybe, @BigUncle, and @SeashoreShi.</li>
<li>Docs: clarify context-pruning ratio bounds, local dashboard recovery, CLI env markers, remote onboarding token behavior, and Peekaboo Bridge permissions for subprocess agents. Thanks @ayesha-aziz123, @dishraters, @hougangdev, and @brandonlipman.</li>
<li>Docs: clarify browser CDP diagnostics, Plugin SDK allowlist imports, status-reaction timing defaults, queue steering behavior, limited-tool troubleshooting, cron HEARTBEAT handling, Telegram multi-agent groups, Bitwarden SecretRef setup, and EasyRunner deployments. Thanks @Quratulain-bilal, @mbelinky, @Mickey-, @vancece, @xenouzik, @posigit, @surlymochan, @janaka, and @choiking.</li>
<li>Crabbox/Testbox: run clean sparse-checkout Testbox syncs from a temporary full checkout and route remote changed gates through Corepack pnpm.</li>
<li>Docs: clarify IPv4-only Gateway BYOH binding, trusted-proxy scope clearing, Android pairing approval, macOS Accessibility grants, Zalo profile env vars, password-store SecretRef setup, and Chinese memory navigation. Thanks @itskai-dev, @gwh7078, @longstoryscott, @MoeJaberr, and @yuaiccc.</li>
<li>Docs: consolidate GLM under Z.AI, add the Upstash Box install guide and Gateway exposure runbook, clarify MEDIA directives, Copilot and Voyage setup, config path quoting, real behavior proof, and memory-file write guidance. Thanks @BobDu, @alitariksahin, @Jefsky, @musaabhasan, @OmerZeyveli, @leno23, @WuKongAI-CMU, @luoyanglang, and @majin1102.</li>
<li>Docs: clarify media provider credentials, Codex/OpenClaw code-mode boundaries, Slack and Telegram ack reactions, Feishu dynamic agents, secrets plaintext boundaries, memory guidance, and Chinese glossary terms. Thanks @nielskaspers, @cosmopolitan033, @drclaw-iq, @alexgduarte, @zccyman, @chengoak, and @cassthebandit.</li>
<li>Packaging: exclude documentation images and assets from the npm tarball, reducing published package size without affecting runtime docs search or CLI behavior. Thanks @SebTardif.</li>
<li>Media understanding: stop auto-probing Gemini CLI and use Antigravity CLI only as a lower-priority image/video fallback after configured provider APIs.</li>
<li>Agents/subagents: limit default sub-agent bootstrap context to <code>AGENTS.md</code> and <code>TOOLS.md</code>, keeping persona, identity, user, memory, heartbeat, and setup files out of delegated workers by default. (#85283) Thanks @100yenadmin.</li>
<li>Maintainer skills: exclude plugin SDK/API boundary work from <code>openclaw-landable-bug-sweep</code> so bugbash sweeps stay focused on small paper-cut fixes.</li>
<li>QA-Lab/diagnostics: extend the OpenTelemetry smoke harness to prove trace, metric, and log export, and add first-class Prometheus and observability smoke aliases.</li>
<li>Plugin SDK: add a generic channel-message poll sender so channel plugins can expose poll delivery without depending on channel-specific SDK facades.</li>
<li>Crabbox: keep the local wrapper's provider validation synced with the installed Crabbox binary while preserving supported aliases such as <code>docker</code> and <code>blacksmith</code>. (#85302) Thanks @hxy91819.</li>
<li>Maintainer skills: add <code>openclaw-landable-bug-sweep</code> for producing five small, reviewed, CI-green OpenClaw bugfix PRs from issue/PR sweeps.</li>
<li>Control UI/chat: add search and Load More pagination to the chat session picker, keeping initial session loads bounded while making older conversations reachable. (#85237) Thanks @amknight.</li>
<li>CLI/onboarding: start classic onboarding when bare <code>openclaw</code> runs before an authored config exists, while keeping configured installs on Crestodian. (#72343) Thanks @fuller-stack-dev.</li>
<li>Discord: allow configuring a bounded <code>agentComponents.ttlMs</code> callback registry lifetime for long-running component workflows, with per-account overrides and a 24-hour cap. (#84189) Thanks @100menotu001.</li>
<li>xAI/Grok: reuse xAI OAuth auth profiles for Grok <code>web_search</code>, thread active-agent auth through web search, add Grok model aliases, and let media providers declare default operation timeouts. (#85182) Thanks @fuller-stack-dev.</li>
<li>Plugin SDK: add row-level session workflow helpers and deprecate <code>loadSessionStore</code> so plugins can read and patch sessions without depending on the legacy whole-store shape. (#84693) Thanks @efpiva.</li>
<li>Gateway/plugins: reuse a compatible Gateway startup plugin registry during dispatch so safe plugin dispatches avoid redundant registry loading. (#84324) Thanks @ai-hpc.</li>
<li>Plugins/SDK: add a general <code>embeddingProviders</code> capability contract and registration API so embeddings can become a reusable provider surface outside memory-specific adapters.</li>
<li>Dependencies: refresh provider, plugin, UI, and tooling packages, update <code>protobufjs</code> to 8.4.0 to clear the current npm advisory, and carry the Claude ACP completion patch forward to <code>@agentclientprotocol/claude-agent-acp</code> 0.36.1.</li>
<li>Agents/tools: remove the old sender-owner tool gating path so configured tools stay visible for trusted sessions while command and channel-action auth still carry real sender identity.</li>
<li>QA-Lab: add curated mock JSONL replay fixtures and first-drift reporting for runtime-parity audits. (#80323, refs #80176) Thanks @100yenadmin.</li>
<li>QA-Lab: add a QA bus tool-trace visibility scenario for sanitized tool-call assertions.</li>
<li>QA-Lab: replace generic evidence framing in seeded scenario prompts with concrete observed QA behavior.</li>
<li>QA-Lab: list named scenario packs in the coverage report so personal-agent privacy coverage stays visible in audits.</li>
<li>QA-Lab: list live transport lane membership in the coverage report so real transport checks stay separate from seeded qa-channel scenarios.</li>
<li>Release/package: run package integrity checks before package acceptance lanes so public install/update validation fails before private QA assets can leak into the package.</li>
<li>QA-Lab: include the optional 100-turn runtime parity soak in release-soak artifacts so long-run Codex/Pi transcript drift stays visible outside the default gate. (#80395) Thanks @100yenadmin.</li>
<li>QA-Lab: add a live-only long-context progress watchdog scenario for Codex app-server timeout and stalled-run sentinels. (#80323) Thanks @100yenadmin.</li>
<li>QA-Lab: tag gateway restart recovery and streaming final-integrity scenarios as live-only runtime parity lanes. (#80323) Thanks @100yenadmin.</li>
<li>QA-Lab: add a personal-agent failure recovery scenario that checks honest partial status, retry boundaries, and local recovery artifacts. (#83872) Thanks @iFiras-Max1.</li>
<li>QA-Lab: include an opt-in <code>update.run</code> package self-upgrade sentinel for destructive latest-package recovery checks.</li>
<li>QA-Lab: add Codex plugin lifecycle and auth-profile fixture coverage for missing installs, pinned-version drift, first-turn install ordering, and doctor migration safety. (#80323, refs #80174) Thanks @100yenadmin.</li>
<li>Models/perf: pre-warm the provider auth-state map at gateway startup so <code>/models</code> and every model-listing call short-circuits the per-provider plugin / external-CLI discovery on the hot path. Per-call cost drops from ~20 s to ~5 ms (~4,100×); the one-time startup warm resets and re-warms after hot reloads. (#84816) Thanks @sjf.</li>
<li>Release/security: ship the root npm package and OpenClaw-owned npm plugins with generated shrinkwrap, support bundled plugin runtime dependencies for suitable plugin tarballs, and require review for lockfile/shrinkwrap changes so published installs use locked dependency graphs.</li>
<li>Tests/perf: isolate doctor core health check unit coverage from real skills/workspace discovery so <code>doctor-core-checks</code> no longer dominates unit perf while keeping one real skills-readiness smoke. (#84493) Thanks @frankekn.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>WebChat: summarize internal message-tool source replies so tool cards no longer duplicate the visible reply body. (#84773) Thanks @jason-allen-oneal.</li>
<li>Gateway: preserve deferred lifecycle-error cleanup across later non-terminal events so provider timeouts can persist failed session state instead of leaving sessions stuck running. (#85256, fixes #63819) Thanks @samzong.</li>
<li>Agents/subagents: report tool-only child progress during timeout summaries instead of showing no visible output.</li>
<li>Telegram/ACP: preserve explicit <code>:topic:</code> conversation suffixes when inbound ACP targets do not carry a separate thread id.</li>
<li>Browser/proxy: bypass the managed proxy for the exact local managed Chrome CDP readiness and DevTools WebSocket endpoints, so <code>openclaw browser start</code> works when the operator proxy blocks loopback egress. (#83255) Thanks @lightcap.</li>
<li>Ollama: bypass the managed proxy for configured local embedding origins while keeping SSRF guardrails on unconfigured targets. Thanks @Kaspre.</li>
<li>OpenAI/images: route Codex API-key image generation through the native OpenAI Images API instead of the Codex OAuth streaming backend, avoiding 401s from valid API keys.</li>
<li>Agents/OpenAI completions: omit empty tool payload fields for proxy-like OpenAI-compatible endpoints so strict vLLM-style servers accept tool-free turns. (#85835) Thanks @rendrag-git.</li>
<li>Checks/Windows: route full <code>pnpm check</code> stage commands through the managed child runner so Windows avoids Node shell-argv deprecation warnings there too.</li>
<li>Checks/Windows: run managed child commands through explicit <code>cmd.exe</code> wrapping instead of Node shell mode with argv, avoiding Node 24 subprocess deprecation warnings during changed checks.</li>
<li>Gateway: omit internal stream-error placeholder entries from agent prompt history so failed assistant turns are not replayed as model-authored text. (#85652) Thanks @anyech.</li>
<li>Sessions: enforce the session write-lock max-hold policy during lock acquisition so long-held locks can be reclaimed before the stale-lock window. (#85764) Thanks @njuboy11.</li>
<li>Models: prune retired Groq, GitHub Copilot, OpenAI, xAI, and old Claude catalog entries, with doctor migration to upgrade existing configs to current provider refs.</li>
<li>Doctor/update: recognize junction-backed source checkouts as git installs by comparing canonical paths before showing package-manager update guidance. Fixes #82215. Thanks @igormf.</li>
<li>Channels: honor <code>/verbose on</code> for tool/progress summaries across direct chats, groups, channels, and forum topics while preserving quiet default behavior. (#85488) Thanks @kurplunkin.</li>
<li>CLI/skills: show an all-ready note with next-step commands when skill setup has no missing dependencies to install. (#85032) Thanks @aniruddhaadak80.</li>
<li>Microsoft Foundry: route DeepSeek V4 Pro and Flash models through the Foundry Responses API while keeping older DeepSeek models on their existing path. (#85549) Thanks @roslinmahmud.</li>
<li>Status/usage: show configured cost estimates for AWS SDK models in full usage output while keeping token-only usage replies cost-free. (#85619) Thanks @ItsOtherMauridian.</li>
<li>Agents/OpenAI Responses: retry non-visible reasoning-only turns for OpenAI Responses API families instead of treating them as empty failed turns. (#85603) Thanks @SebTardif.</li>
<li>Directive tags: preserve message and content-part object identity when display stripping makes no directive-tag changes. (#85682) Thanks @willamhou.</li>
<li>Telegram: send local <code>path</code>/<code>filePath</code> and structured attachment media from <code>sendMessage</code> actions instead of dropping them or sending text-only messages. (#85219) Thanks @keshavbotagent.</li>
<li>Sessions/status: show the estimated context budget when fresh provider usage is unavailable and clear stale estimates across session resets and compaction boundaries. (#84830) Thanks @giodl73-repo.</li>
<li>Gateway/config: pin relative <code>OPENCLAW_STATE_DIR</code> overrides to an absolute path at startup so later working-directory changes cannot retarget gateway state. (#52264) Thanks @PerfectPan.</li>
<li>Release/package: run npm release, prepublish, and postpublish verification through Windows-safe npm command shims so native Windows checks can execute <code>npm.cmd</code> instead of treating it as a binary.</li>
<li>Agents/harness: pass CLI runtime aliases through harness selection so provider-owned CLI aliases no longer get rejected before reaching the right runtime. (#85631) Thanks @potterdigital.</li>
<li>Secrets: show the irreversible apply warning after interactive <code>secrets configure</code> confirmation so confirmed migrations still get the final safety prompt. (#85638) Thanks @alkor2000.</li>
<li>Agents/CLI output: ignore cumulative Claude <code>stream-json</code> result usage when assistant usage events are present, preventing inflated cache-read accounting. (#85625) Thanks @zhouhe-xydt.</li>
<li>CLI: keep <code>waitForever()</code> alive by leaving its keep-alive interval ref'd so the public helper no longer exits immediately with Node's unsettled-await code. (#85694) Thanks @m1qaweb.</li>
<li>Agents/bootstrap: guard bootstrap name checks against missing file names so malformed bootstrap entries warn and truncate instead of crashing. Fixes #85523. (#85615) Thanks @zhouhe-xydt.</li>
<li>CLI/tasks: reject partially numeric <code>openclaw tasks audit --limit</code> values so audit limits must be real positive integers instead of accepting strings like <code>5abc</code>. (#84901) Thanks @jbetala7.</li>
<li>Status/diagnostics: bound deep Docker audit probes so <code>openclaw status --deep</code> reports slow container checks instead of hanging behind unbounded inspection. (#85476) Thanks @giodl73-repo.</li>
<li>Providers/Anthropic: migrate 1M context handling to GA-capable Claude 4.x models by sizing eligible models at 1M without the retired <code>context-1m-2025-08-07</code> beta, ignoring that retired beta in older configs, and preserving OAuth-required Anthropic beta headers. (#45613) Thanks @haoyu-haoyu.</li>
<li>Cron/Telegram: parse forum-topic delivery targets through the Telegram plugin instead of cron core, including <code>:topic:</code> and <code>:topicId</code> forms for announce delivery. Thanks @etticat.</li>
<li>Twitch: keep stale message-handler cleanup callbacks from removing newer handler registrations for the same account, preserving inbound message delivery after reconnects. Fixes #83888. (#85425) Thanks @alkor2000.</li>
<li>Memory/LanceDB: expose public memory artifacts through the active memory provider bridge so memory-wiki imports durable memory files, daily notes, dream reports, and event logs without depending on memory-core internals. Fixes #83604. (#85060) Thanks @brokemac79.</li>
<li>Crabbox: keep AWS hydration compatible with local Actions replay by inlining the hydrate workflow's Node/pnpm setup instead of invoking repo-local composite actions.</li>
<li>Agents/subagents: simplify native sub-agent completion handoff so children report their latest visible assistant result to the requester without using <code>message</code>, while keeping parent-owned message-tool delivery policy intact. Fixes #85070. (#85089) Thanks @brokemac79.</li>
<li>Docker setup: stop printing the Gateway bearer token in setup logs and printed follow-up commands.</li>
<li>Agents: let embedded compaction fallback retries proceed when PI-compatible candidates do not need agent harness plugin preparation.</li>
<li>Agents/tools: honor configured custom provider API keys when deciding whether media, image-generation, video-generation, music-generation, and PDF tools are available. (#85570)</li>
<li>StepFun: stop advertising stale generic API key auth choices so onboarding only offers runtime-backed Standard and Step Plan choices.</li>
<li>Diagnostics: keep OpenTelemetry log bodies behind explicit content capture and scrub scoped agent-session keys from OpenTelemetry and Prometheus labels while preserving bounded queue-lane prefixes.</li>
<li>Windows installer: fail Git checkout installs when <code>pnpm install</code> or <code>pnpm build</code> fails instead of writing a wrapper to a missing CLI build.</li>
<li>Sessions: surface previous-transcript archive failures during <code>/new</code> rotation so disk rename errors are logged instead of silently hiding stranded transcript files. Fixes #81984. (#85586, from #82081) Thanks @0xghost42.</li>
<li>TUI/agents: mirror internal-ui message-tool replies into final chat output so message-tool-only agents remain visible in <code>openclaw tui</code>. Fixes #85538. Thanks @danpolasek.</li>
<li>Agents: keep parallel OpenAI-compatible tool-call deltas in separate argument buffers so interleaved tool calls no longer corrupt streamed arguments. (#82263) Thanks @luna-system.</li>
<li>Memory/doctor: report missing or unusable QMD workspace directories as workspace failures instead of generic binary failures. (#63167) Thanks @sercada.</li>
<li>Debug proxy: record CONNECT client-socket errors and destroy the paired upstream socket so abrupt client disconnects no longer leak tunnel resources. (#82444) Thanks @SebTardif.</li>
<li>Diffs: continue hydrating later diff cards when one card fails so a single broken card no longer blanks the whole diff viewer. (#84775) Thanks @cosmopolitan033.</li>
<li>Mac app: use the native settings sidebar window chrome so the sidebar toggle stays on the left and content no longer clips under oversized titlebar padding.</li>
<li>QA-Lab/Codex: bundle auth/plugin fixture imports for flow scenarios and let terminal async media tools end Codex app-server turns without timing out. (#80397, refs #80323) Thanks @100yenadmin.</li>
<li>Gateway/agents: preserve fresh session overrides and metadata when stale cached agent-session entries race with store updates, so subagent model/provider overrides and routing policy survive concurrent writes. (#19328) Thanks @CodeReclaimers.</li>
<li>Control UI/chat: keep chat session search inline with the session selector so the header no longer shows a duplicate standalone search row.</li>
<li>Control UI/chat: collapse focused-mode header chrome and suppress hidden-header scroll updates so focus mode no longer jumps while scrolling. Thanks @amknight.</li>
<li>Codex app-server: restart the native app-server and retry once when server-side compaction times out, so preflight compaction stalls recover instead of failing every dispatch. (#85500)</li>
<li>Restore Control UI gateway token pairing [AI]. (#85459) Thanks @pgondhi987.</li>
<li>OpenAI video: honor configured provider request private-network opt-in for local/custom video endpoints so explicitly trusted mock and self-hosted providers are not blocked. Thanks @shakkernerd.</li>
<li>OpenAI video: send uploaded video edit requests to the documented <code>/videos/edits</code> endpoint with a <code>video</code> file instead of posting MP4 references to <code>/videos</code>. Thanks @shakkernerd.</li>
<li>Agents/channels: preserve message-tool delivery evidence through gateway agent completion handoffs so successful generated media sends are not followed by false failure messages. Thanks @shakkernerd.</li>
<li>CLI/update: repair managed npm plugin <code>openclaw</code> peer links during post-core convergence and reject stale or wrong-target peer links before restart. (#83794) Thanks @fuller-stack-dev.</li>
<li>CLI/agents: default new omitted-account bindings to all accounts when the channel has multiple configured accounts, and clarify account-scope docs. (#49769) Thanks @Gcaufy.</li>
<li>Codex app-server: let authorized <code>/codex</code> control commands such as <code>/codex detach</code> escape plugin-owned conversation bindings while keeping unknown or unauthorized slash text routed to the bound plugin. Fixes #85157. (#85188) Thanks @TurboTheTurtle.</li>
<li>Auto-reply/models: keep <code>/models</code> browse replies fast by sharing the bounded read-only catalog path with Gateway model listing. (#84735) Thanks @safrano9999.</li>
<li>Codex app-server: disable native Code Mode when the effective exec host is <code>node</code> and keep OpenClaw <code>exec</code>/<code>process</code> available, so <code>/exec host=node</code> routes shell commands through the selected node instead of the gateway. Fixes #85012. (#85090) Thanks @sahilsatralkar.</li>
<li>Agents: bound embedded auto-compaction session write-lock watchdogs to the compaction timeout instead of the full run timeout, so stuck compaction cannot hold the live session lock for the whole run window. (#84949) Thanks @luoyanglang.</li>
<li>Gateway/agents: return phase-aware <code>agent.wait</code> timeout attribution and only cool auth profiles on provider-started timeouts. Refs #65504. Thanks @100yenadmin.</li>
<li>Gateway: defer provider auth-state prewarm until after startup readiness so early gateway tool/session requests are not blocked by provider auth discovery. (#85272) Thanks @dutifulbob.</li>
<li>Gateway/models: coalesce provider auth-state rewarms after auth-profile failures and log event-loop delay for warm/rewarm work, so provider auth bursts no longer stack full auth sweeps behind channel replies.</li>
<li>Gateway/models: stop cancelled provider auth-state prewarms from continuing full provider sweeps, so reload and auth-failure bursts no longer keep startup busy.</li>
<li>Agents/Codex: show the first plan update as a transient chat status notice without counting it as final assistant content.</li>
<li>CLI/update: walk the macOS process ancestry and honor the inherited Gateway runtime PID before package updates stop the managed Gateway service, so nested in-band updater children can refuse instead of killing the LaunchAgent-supervised Gateway that owns them. Fixes #85120.</li>
<li>Gateway/LaunchAgent: wait for launchd reload bootout to finish and fall back to kickstart when bootstrap races, so reload handoff does not leave the service deregistered. Fixes #84630. (#84641) Thanks @NianJiuZst.</li>
<li>Gateway/LaunchAgent: treat a concurrent launchd bootstrap as a successful restart when the service is already loaded, avoiding false macOS Gateway restart failures. Fixes #84721. (#84722) Thanks @googlerest.</li>
<li>Gateway/service: include the active <code>openclaw</code> command bin directory in managed service PATH generation and doctor audit expectations for npm-global macOS installs. Fixes #84201. (#84475) Thanks @jbetala7.</li>
<li>Control UI/chat: disable the thinking selector for known non-reasoning models instead of showing duplicate Off choices. Fixes #84069. Thanks @DrippingMellow.</li>
<li>Memory: expand <code>~</code> in configured extra memory paths before resolving them, so home-relative folders are not treated as workspace-relative. Fixes #58026. Thanks @stadman.</li>
<li>Skills: treat <code>openclaw.os: macos</code> as Darwin when checking skill requirements, so macOS-only skills no longer report as missing on macOS hosts. Fixes #61338. Thanks @Jessecq1995.</li>
<li>Control UI/logs: strip ANSI escape sequences from displayed Gateway log messages so color codes no longer appear as raw text. Fixes #64399. Thanks @guguangxin-eng.</li>
<li>Docker: pre-create the workspace and auth-profile config mount points with <code>node</code> ownership so first-run named volumes do not start root-owned. Fixes #85076. Thanks @Noerr.</li>
<li>Telegram: pass configured markdown table mode through outbound markdown chunking so chunked sends render tables consistently. Fixes #85085. Thanks @ShuaiHui.</li>
<li>CLI/update: preserve managed Gateway service environment during package cutovers so macOS LaunchAgent repair/restart reads the pre-update service state instead of caller shell state. (#83026)</li>
<li>Agents/providers: honor per-model <code>api</code> and <code>baseUrl</code> overrides in custom provider auth hooks and transport selection. Fixes #80487. (#80488) Thanks @huveewomg.</li>
<li>Gateway/restart: eager-load the lifecycle runtime before in-place upgrade signal handling so package replacement does not deadlock restart imports. (#84890) Thanks @myps6415.</li>
<li>CLI/update: start managed Gateway update handoff helpers from a stable existing directory and tolerate deleted cwd/package roots during macOS LaunchAgent handoff. Fixes #83808. (#83875) Thanks @jason-allen-oneal.</li>
<li>Skills: watch each shared skill directory once across agent workspaces instead of once per agent, preventing file-descriptor exhaustion (<code>EMFILE</code>) that disposed bundle-mcp processes and stalled sessions on multi-agent gateways. Fixes #84968. (#85130) Thanks @openperf.</li>
<li>Release/security: keep generated npm shrinkwrap package versions inside the pnpm lock graph so published package locks cannot bypass pnpm dependency age and override policy.</li>
<li>Cron: honor <code>cron.retry.retryOn: ["network"]</code> for common network error codes such as <code>EAI_AGAIN</code>, <code>EHOSTUNREACH</code>, and <code>ENETUNREACH</code>.</li>
<li>Gateway chat: broadcast returned agent-run error payloads after an agent starts so ACP/WebChat clients receive terminal idle-timeout errors. Fixes #84945.</li>
<li>Gateway chat display: preserve OpenAI-compatible <code>prompt_tokens</code>, <code>completion_tokens</code>, and <code>total_tokens</code> usage fields in sanitized chat history so llama.cpp sessions keep context counts. Fixes #77992. Thanks @MarTT79.</li>
<li>Dashboard/CLI: allow macOS browser launching through <code>open</code> even when SSH environment variables are present, while preserving Linux SSH no-display protection. Fixes #67088. Thanks @theglove44.</li>
<li>Codex app-server: keep native web search observations out of mirrored chat transcripts while preserving tool progress telemetry. Fixes #85109. Thanks @ugitmebaby.</li>
<li>OpenCode Go: strip unsupported Kimi reasoning replay fields before provider requests so repeated <code>kimi-k2.6</code> turns do not fail schema validation. Fixes #83812. Thanks @Sleeck.</li>
<li>Browser/CDP: add a WSL2 portproxy self-loop hint when Chrome DevTools endpoints accept connections but return an empty HTTP reply. Fixes #59209. Thanks @Owlock.</li>
<li>Agents/OpenAI: preserve structured provider error code, type, and redacted body metadata on boundary-aware transport failures.</li>
<li>Doctor/Codex: point native Codex asset warnings at the canonical <code>openclaw migrate plan codex</code> preview command. Fixes #84948. Thanks @markoa.</li>
<li>CLI/models: make <code>capability model auth logout --agent</code> remove auth profiles from the selected non-default agent store. Fixes #85092. Thanks @islandpreneur007.</li>
<li>Gateway/models: reuse prepared provider auth metadata during model-listing auth checks so repeated lookups avoid broad plugin discovery while preserving synthetic local auth.</li>
<li>CLI/status: suppress systemd user-service setup hints when <code>openclaw status --deep</code> can already reach a running Gateway RPC service. Fixes #85094. Thanks @islandpreneur007.</li>
<li>CLI/devices: recover local approval when a same-device repair request replaces the request ID being approved.</li>
<li>CLI/agents: retry transient normal-close Gateway handshakes before falling back to embedded <code>openclaw agent</code> execution.</li>
<li>CLI/update: keep managed Gateway service stop/restart status lines out of <code>openclaw update --json</code> stdout so package-update automation can parse the JSON payload.</li>
<li>Plugins: resolve OpenClaw plugin SDK subpaths for native external plugin runtimes without mutating package installs or broadening process-wide module resolution.</li>
<li>Agents/OpenAI: preserve Responses and Chat Completions <code>reasoning_tokens</code> usage metadata without double-counting it in aggregate output tokens. (#85319)</li>
<li>Control UI/chat: convert pasted <code>data:image/...;base64,...</code> clipboard text into an image attachment instead of dumping the payload into the composer. Fixes #62604. Thanks @cpwilhelmi.</li>
<li>Providers/Gemini: strip fractional seconds from web-search time range filters so Gemini accepts freshness-bound search requests. (#85071) Thanks @Noerr.</li>
<li>OpenAI Codex: preserve image input support for sparse <code>openai-codex/gpt-5.5</code> catalog rows. (#85095) Thanks @sercada.</li>
<li>CLI/models: add a piped or pasted API-key path for OpenAI Codex auth and warn when API keys are pasted into token-mode auth. (#85533) Thanks @joshavant.</li>
<li>Telegram: dead-letter missing-harness isolated ingress failures so a poisoned spooled update no longer blocks later same-lane messages. Fixes #85470. (#85605) Thanks @joshavant.</li>
<li>Plugins/discovery: strip <code>-plugin</code> package suffixes when deriving plugin id hints so package names line up with manifest ids. (#85170) Thanks @JulyanXu.</li>
<li>Tlon: stop advertising a non-existent agent tool contract in the plugin manifest.</li>
<li>Telegram: preserve fenced code block languages through Markdown rendering so Telegram receives <code>language-*</code> code classes. (#85209) Thanks @leno23.</li>
<li>Windows installer: run npm and Corepack command shims from a Windows-local directory so installs launched from WSL2 UNC paths do not fail before OpenClaw is installed.</li>
<li>Windows updates: roll back git-backed updates to the previous checkout when dependency install, build, UI build, or doctor repair fails.</li>
<li>Windows installer: persist user-local portable Git on PATH and activate the repo-pinned pnpm version for git-backed installs and updates.</li>
<li>Windows installer: bootstrap a user-local portable Node.js when native Windows has no Node and no winget, Chocolatey, or Scoop, so first-run installs can continue on raw hosts.</li>
<li>Windows installer: extract the downloaded portable Node.js directory with native <code>tar</code> before falling back to .NET zip extraction, avoiding PowerShell 5.1 archive and path-length failures.</li>
<li>fix(integrations): enforce channel read target allowlists [AI]. (#84982) Thanks @pgondhi987.</li>
<li>Agents/heartbeat: route single-owner <code>session.dmScope=main</code> direct-message exec and cron event wakes back to the agent main session so async completions no longer strand context in orphan direct-DM queues. Fixes #71581. (#83743) Thanks @Kaspre.</li>
<li>Agents/code-mode: expose outer code-mode <code>exec</code> source through the <code>command</code> hook alias with <code>toolKind</code>/<code>toolInputKind</code> discriminators so exec-shaped policies can distinguish code-mode cells. (#83483) Thanks @Kaspre.</li>
<li>Agents/code mode: return structured timeout and runtime-unavailable error codes for known worker failures. Fixes #83389. (#83444) Thanks @Kaspre.</li>
<li>QA-Lab: isolate multi-scenario suite workers when scenarios need startup config patches, preventing message-routing config from leaking into unrelated scenarios.</li>
<li>QA-Lab: make the commitments heartbeat-target-none scenario request an immediate heartbeat instead of waiting for the next scheduled heartbeat.</li>
<li>Codex/Plugin SDK: deliver Codex-native subagent completions through a generic harness task runtime so harness-backed plugins can mirror durable task lifecycle and completion delivery without Codex-specific SDK imports. (#83445) Thanks @bryanpearson.</li>
<li>Gateway CLI: surface local post-challenge connect assembly failures immediately instead of waiting for the wrapper timeout. Fixes #68944. (#85253) Thanks @samzong.</li>
<li>Messages: strip unsupported web-search citation control markers from outbound replies before they reach WebChat or external channels. Fixes #85193. (#85204) Thanks @neeravmakwana.</li>
<li>Agents/exec: treat denied exec approvals as terminal instead of feeding them back into agent follow-up work, and recognize Chinese stop phrases in abort handling. Fixes #69386. (#85194) Thanks @samzong.</li>
<li>CLI/agents: abort accepted Gateway-backed <code>openclaw agent</code> runs on SIGINT/SIGTERM so cron and supervisor timeouts do not leave remote agent work alive. Fixes #71710. (#84381) Thanks @Kaspre.</li>
<li>Codex app-server: retry replay-safe stdio client-close turns once using structured failure metadata, while surfacing idle <code>turn/completed</code> timeouts instead of blindly replaying active shared-server turns. Thanks @VACInc.</li>
<li>Codex app-server: reject command overrides that embed Node or package-manager arguments and point users to <code>appServer.args</code>, so Windows startup avoids shell parsing failures. (#84417) Thanks @TurboTheTurtle.</li>
<li>Agents/Copilot: drop unsafe GitHub Copilot Responses reasoning replay items before send so Telegram direct sessions no longer fail on overlong replay IDs. Fixes #85197. (#85198) Thanks @galiniliev.</li>
<li>UI: add accessible tooltips to the topbar color-mode buttons so System, Light, and Dark choices are labeled on hover and focus. (#85227) Thanks @amknight.</li>
<li>fix: constrain Windows task script names [AI]. (#85064) Thanks @pgondhi987.</li>
<li>Control UI: keep the chat session picker from hiding older or cross-agent configured conversations while preserving the bounded configured-agent refresh. (#85211) Thanks @amknight.</li>
<li>Agents/Anthropic: preserve unsafe integer tool-call input values in streamed Anthropic tool-use JSON, preventing Discord-style IDs from being rounded before dispatch. Fixes #47229. (#83063) Thanks @leno23.</li>
<li>Agents/Codex: estimate tool-heavy prompt pressure at the LLM boundary before provider submission, so persistent sessions compact before overflowing context windows. (#85541) Thanks @fuller-stack-dev and @joshavant.</li>
<li>Agents/hooks: wait for local one-shot CLI and Codex <code>agent_end</code> plugin hooks before process cleanup so terminal observability flushes reliably. (#85007)</li>
<li>Providers/Google: preserve Gemini 3 cron <code>thinkingDefault: "low"</code> when stale catalog metadata says <code>reasoning:false</code>, so scheduled runs keep provider-supported thinking instead of downgrading to off. (#85185) Thanks @neeravmakwana.</li>
<li>CLI/agents: allow <code>openclaw agent --session-key</code> to target explicit session keys, including agent-scoped legacy keys. (#85121) Thanks @Kaspre.</li>
<li>Auto-reply/ACP: wait for same-channel block reply delivery before starting tool work, while still honoring ACP dispatch aborts so stopped turns do not wait on slow channel sends. (#83722) Thanks @IWhatsskill.</li>
<li>Codex/ACP: mark required child-run completions that only report progress, omit a final deliverable, or fail requester delivery as blocked while preserving real final reports. (#85110) Thanks @IWhatsskill.</li>
<li>Channels: treat bare abort messages such as <code>stop</code>, <code>abort</code>, and <code>wait</code> as immediate control commands in inbound debounce paths so stop requests are not delayed behind pending message coalescing. (#83348) Thanks @IWhatsskill.</li>
<li>Channels/message tool: resolve configured external channel plugins during in-agent channel selection, so <code>openclaw agent --local</code> message-tool sends no longer report an available channel as unavailable. (#85022) Thanks @Kaspre.</li>
<li>Agents/heartbeat: honor group/channel <code>message_tool</code> visible-reply policy and model-specific Codex runtime config for scheduled heartbeat runs, so failed internal tool output stays private. Fixes #85310. (#85357) Thanks @neeravmakwana.</li>
<li>Gateway/ACP: close child ACP sessions spawned via <code>sessions_spawn</code> when their parent session is reset or deleted, instead of leaving orphaned <code>claude-agent-acp</code> processes that accumulate and exhaust memory. Fixes #68916. (#85190) Thanks @openperf.</li>
<li>Codex app-server: block native execution paths when OpenClaw exec resolves to a node host while preserving the first-party CLI node binding path. Fixes #85012. (#85534) Thanks @joshavant.</li>
<li>Diagnostics: bound cleanup timeout detail logs, emit drop summaries when async diagnostic bursts exceed the queue cap, and surface async queue drops through diagnostic telemetry.</li>
<li>Agents/subagents: surface blocked child-run completions as errors instead of successful subagent finishes. (#80886) Thanks @TurboTheTurtle.</li>
<li>Context engines: fail closed with a descriptive error when the selected agent runtime cannot satisfy declared context-engine host requirements.</li>
<li>Agents/Pi: treat accepted embedded <code>sessions_spawn</code> child-session handoffs as terminal progress so parent turns no longer report false non-deliverable failures. (#85054) Thanks @samzong.</li>
<li>CLI/models: resolve <code>openclaw models set</code> aliases from the runtime config while keeping authored aliases ahead of runtime-only defaults. (#83262) Thanks @IWhatsskill.</li>
<li>Doctor: show personal Codex CLI asset notices as info instead of warnings. Fixes #84859.</li>
<li>WhatsApp: update Baileys to <code>7.0.0-rc13</code> and drop the obsolete logger type patch.</li>
<li>CLI/update: pre-pack GitHub/git package update targets before the staged npm install, restoring <code>openclaw update --tag main</code> for one-off package updates. (#81296) Thanks @fuller-stack-dev.</li>
<li>Gateway: mirror successful same-source message-tool sends into session transcripts so delivered replies stay in later history/context. (#84837) Thanks @iFiras-Max1.</li>
<li>Media generation: keep image, music, and video completion delivery from duplicating or losing task ownership when generated media finishes through active session replies. (#84006) Thanks @fuller-stack-dev.</li>
<li>Infra/json: retry transient <code>File changed during read</code> races while loading JSON state so config and state reads recover instead of failing the turn. (#84285)</li>
<li>Plugins/providers: fail closed for workspace provider plugins during setup-mode discovery unless explicitly trusted, preventing untrusted workspace plugin code from running during provider setup. (#81069) Thanks @mmaps.</li>
<li>Providers/Ollama: resolve configured Ollama Cloud <code>OLLAMA_API_KEY</code> markers to the real discovery key so cloud provider entries keep authenticated model catalog access. (#85037)</li>
<li>Discord: keep persistent component registry fallback warnings actionable by forwarding structured error and cause metadata through the runtime logger. Fixes #84185. (#84190) Thanks @100menotu001.</li>
<li>Gateway/sessions: preserve compatible session auth profile overrides when switching models within the same provider, including provider-auth aliases. Fixes #81837. (#81886) Thanks @TurboTheTurtle.</li>
<li>Gateway/status: surface inbound delivery telemetry counters and transport-liveness warnings in <code>openclaw status --all</code>. Fixes #49577. (#72724)</li>
<li>Docker: prune package-excluded plugin source workspaces and dependency closures so runtime images do not keep packages for plugins that were not opted in.</li>
<li>Providers/Ollama: treat Docker/OrbStack host aliases as local Ollama endpoints so <code>ollama-local</code> marker auth works when OpenClaw runs inside a VM/container and Ollama runs on the host. Fixes #84875.</li>
<li>QA-Lab: keep explicitly searchable/deferred OpenClaw dynamic tool rows report-only by default so tool-coverage gates do not treat mock discovery gaps as hard product failures. (#80319) Thanks @100yenadmin.</li>
<li>Agents/config: keep non-Google provider model refs from being rewritten by Google Gemini preview-id normalization. (#84762) Thanks @zhangguiping-xydt.</li>
<li>Installer: require a real controlling terminal before launching onboarding so headless <code>curl | bash</code> installs finish cleanly after installing the CLI.</li>
<li>Agents/Codex: promote a completed final assistant response when a prompt timeout races Codex app-server completion instead of returning an empty timeout envelope. Refs #84516.</li>
<li>Codex app-server: keep interrupted turn statuses from being treated as OpenClaw aborts by themselves, so tool-only turns remain eligible for no-visible-answer recovery. Fixes #84492.</li>
<li>Agents: cap heartbeat model bleed context hints by the stored session window when runtime model metadata is unavailable, so overflow recovery advice does not suggest a larger window than the active session actually has.</li>
<li>Control UI/Web Push: use <code>https://openclaw.ai</code> as the generated default VAPID subject instead of the old localhost mailbox so iOS PWA push setup uses an Apple-acceptable subject when <code>OPENCLAW_VAPID_SUBJECT</code> is unset. Fixes #83134. (#83317) Thanks @IWhatsskill.</li>
<li>Control UI: distinguish inherited thinking-off settings from explicit Off selections so the thinking selector no longer shows two identical Off rows. (#85223) Thanks @amknight.</li>
<li>Agents/Pi: keep embedded session transcript writes from tripping false takeover detection after packaged npm onboarding agent turns.</li>
<li>Codex/TUI: surface Codex-native post-turn compaction failures instead of continuing uncompacted, and keep successful native compaction serialized before local idle/next-turn handling. Fixes #84305. (#85160) Thanks @joshavant.</li>
<li>Memory/search: stop recall tracking from writing dreaming side-effect artifacts when <code>dreaming.enabled=false</code>, while preserving normal search results. Fixes #84436. (#84444) Thanks @NianJiuZst.</li>
<li>Diffs: render viewer toolbar icons from a closed icon-name map instead of HTML strings, removing the toolbar icon XSS sink. (#83955) Thanks @tanshanshan.</li>
<li>QA: keep <code>pnpm qa:e2e</code> self-check runs inside the private QA runtime envelope even when inherited shell env disables bundled plugins.</li>
<li>fix(config): validate browser sandbox bind sources [AI]. (#84799) Thanks @pgondhi987.</li>
<li>doctor: constrain legacy plugin cleanup paths [AI]. (#84801) Thanks @pgondhi987.</li>
<li>Update/doctor: prune stale local bundled plugin install records that point at old compiled bundled output so current bundled plugin schemas win after upgrade. (#84863) Thanks @fuller-stack-dev.</li>
<li>Providers/Ollama: preserve native Ollama tool-call IDs across assistant replay so Gemini over Ollama Cloud can keep its hidden function-call thought-signature handle.</li>
<li>Discord: keep session recovery and <code>/stop</code> abort ownership on the source dispatch lane while bound ACP turns continue routing to their target session, so stalled pre-run work and late replies are cleared instead of leaking after stop. Fixes #84477. (#85100) Thanks @joshavant.</li>
<li>Codex app-server: mark missing turn completion after observed execution as replay-unsafe and release the session so follow-up turns can run. Fixes #84076. (#85107) Thanks @joshavant.</li>
<li>Codex app-server: give visible <code>message</code> dynamic tool sends a longer timeout budget so slow channel delivery can return its own result or error instead of hitting the 30-second Codex wrapper. (#85216) Thanks @amknight.</li>
<li>Codex app-server: add a dedicated post-tool raw assistant completion idle timeout config so trusted heavy turns can wait longer after tool handoff without weakening final assistant release.</li>
<li>Matrix: keep explicitly configured two-person rooms on the room route before stale <code>m.direct</code> or strict two-member DM fallback can bypass mention gating. Fixes #85017. (#85137) Thanks @joshavant.</li>
<li>Agents/subagents: require explicit subagent allowlist targets to be configured agents so stale deleted-agent ids are omitted from <code>agents_list</code> and rejected by <code>sessions_spawn</code>. Fixes #84811. (#85154) Thanks @joshavant.</li>
<li>PDF tool: time out idle remote PDF body reads after 120 seconds so stalled remote documents return an error instead of wedging the session. Fixes #68649. (#84768) Thanks @luoyanglang.</li>
<li>Diagnostics/OpenTelemetry plugin: suppress handled OTLP exporter promise rejections so collector shutdowns no longer crash the Gateway. (#81085) Thanks @luoyanglang.</li>
<li>Agents/exec: omit raw command text and env values from denied exec failure logs while keeping safe correlation metadata. Fixes #85049. (#85140) Thanks @joshavant.</li>
<li>Media/audio: skip empty structured sherpa-onnx transcripts instead of treating the raw JSON payload as spoken text. (#84667) Thanks @TurboTheTurtle.</li>
<li>Agents/exec: preserve inherited XDG base-directory environment values for subprocesses while still rejecting agent-supplied XDG overrides. Fixes #84854. (#85139) Thanks @joshavant.</li>
<li>Node/Linux: keep <code>OPENCLAW_GATEWAY_TOKEN</code> out of generated systemd unit files by writing node service token values to a node-specific env file. (#84408)</li>
<li>Memory-core/dreaming: reuse stable narrative subagent session keys per workspace and phase while keeping per-run idempotency and bounded cleanup, so stale <code>dreaming-narrative-*</code> sessions do not accumulate. Fixes #68252, #69187, and #70402. (#70464) Thanks @chiyouYCH.</li>
<li>Trajectory/support: tolerate partial skill snapshot entries when building support metadata so rejected skill path scans no longer abort trajectory capture. (#71185) Thanks @lukeboyett.</li>
<li>TUI: coalesce repeated idle Esc abort notices into a single <code>no active run xN</code> system row instead of appending duplicate rows.</li>
<li>Telegram: honor <code>channels.telegram.pollingStallThresholdMs</code> in the default isolated polling path, restarting silent workers instead of leaving inbound updates wedged. Fixes #83950. (#84861) Thanks @joshavant.</li>
<li>Telegram: dedupe replayed message dispatches by Telegram chat/message identity so isolated-ingress replays do not trigger duplicate model dispatches. Fixes #84886. (#85208) Thanks @joshavant.</li>
<li>Slack: suppress reasoning payloads before reply delivery and dispatch accounting, so Slack monitor, slash-command, fallback, and direct reply paths do not leak model reasoning. Fixes #84319. (#84322) Thanks @ffluk3 and @joshavant.</li>
<li>Slack: deliver native plugin approval prompts and updates when Slack native approvals are enabled, while keeping plugin approval authorization separate from exec approvers.</li>
<li>Slack: keep native plugin approval prompts in the originating app conversation thread when the live Slack turn source is a <code>D...</code> conversation.</li>
<li>Agents/Pi: disable the embedded pi-coding-agent runtime auto-retry so OpenClaw's own retry and failover loop does not replay failed tool calls through a nested SDK retry. Fixes #73781. (#74434) Thanks @yelog.</li>
<li>CLI/perf: keep <code>setup --help</code>, <code>onboard --help</code>, and <code>configure --help</code> out of the full wizard runtime while preserving the existing help output. (#84488) Thanks @frankekn.</li>
<li>CLI/perf: keep <code>agents --help</code> out of agents action/runtime imports so help, completion, and command discovery paths avoid loading the full agents runtime. (#84483) Thanks @frankekn.</li>
<li>CLI/perf: keep <code>secrets --help</code> and <code>nodes --help</code> on the precomputed help path so parent help avoids loading action-heavy command runtime modules. (#84818) Thanks @frankekn.</li>
<li>CLI/perf: serve <code>doctor</code>, <code>gateway</code>, <code>models</code>, and <code>plugins</code> parent help from startup metadata so common subcommand help avoids full CLI program construction. (#84786) Thanks @frankekn.</li>
<li>Codex/Lossless: keep context-engine history on the canonical run session when Telegram DMs use per-peer runtime policy keys. Fixes #84936. (#84954) Thanks @neeravmakwana.</li>
<li>Codex: keep heartbeat response tool schemas durable without exposing dynamic tools disabled by turn policy, so heartbeat wakeups can reuse threads while scoped tool allowlists stay enforced. (#84681) Thanks @jalehman.</li>
<li>Auth/OAuth: skip the refresh adapter when a stored OAuth credential has no refresh token so agent turns fail fast on missing-key instead of waiting on the 120s refresh timeout. Thanks @romneyda.</li>
<li>Auth/Codex: load legacy OAuth sidecar credentials in the embedded runner's secrets-runtime auth loaders so Telegram replies, cron-triggered turns, and other isolated sub-agent lanes can reach the existing #83312 refresh-and-rewrite migration instead of failing with <code>No API key found for provider "openai-codex"</code> until the user runs <code>openclaw doctor</code>. Thanks @Totalsolutionsync and @romneyda.</li>
<li>Codex/failover: classify <code>deactivated_workspace</code> as a permanent auth failure so configured fallback models can advance when a Codex workspace is deactivated. (#55893) Thanks @litang9.</li>
<li>Exec: keep configured <code>tools.exec.pathPrepend</code> entries ahead of user shell startup PATH changes on POSIX gateway runs. (#81403) Thanks @medns.</li>
<li>Gateway/sessions: allow shared-secret bearer callers to read and stream session history without an explicit scope header. (#81815) Thanks @medns.</li>
<li>Agents/embedded runner: classify HTML auth provider responses as <code>auth_html</code> and return a re-authentication hint instead of the CDN-blocked copy that <code>upstream_html</code> returns. Cloudflare Access login pages, nginx basic-auth challenges, and gateway login walls all produce HTML auth bodies that were previously misdiagnosed as transient CDN blocks. (#79900) Thanks @martingarramon.</li>
<li>TUI/streaming watchdog: dismiss the <code>This response is taking longer than expected</code> notice as soon as a chat event for the same run arrives, so the message no longer sits next to the recovered response when the run was only briefly silent. Refs #67052, #69081 (closed), prior attempt #69026. Thanks @jpruit20 and @romneyda.</li>
<li>Agents/Pi: tolerate OpenClaw-owned transcript writes while embedded prompts are released for model I/O, keeping long-running Feishu, Slack, Telegram, and cron turns from failing with false session-takeover errors. Fixes #84059. (#84250) Thanks @tianxiaochannel-oss88.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.22/OpenClaw-2026.5.22.zip" length="54409357" type="application/octet-stream" sparkle:edSignature="am1mwLOmUHor9QuQWtxSsKoBOCySUBo4fB+0Qdcrz0E3wf6ESIMTfOC0k+dKJSh9gtLZw5jzpWVqTBzEdU36Aw=="/>
</item>
</channel>
</rss>

View File

@@ -218,7 +218,6 @@ Current OpenClaw Android implication:
- Google Play build excludes SMS send/search, Call Log search, and recent-photo access unless the product is intentionally positioned and approved under the relevant policy exception.
- The repo now ships this split as Android product flavors:
- `play`: removes `READ_SMS`, `SEND_SMS`, `READ_CALL_LOG`, `READ_MEDIA_IMAGES`, `READ_MEDIA_VISUAL_USER_SELECTED`, and `READ_EXTERNAL_STORAGE`; hides SMS, Call Log, and Photos surfaces in onboarding, settings, and advertised node capabilities.
- Installed-app listing is user controlled. `device.apps` is advertised only after the user enables **Settings > Phone Capabilities > Installed Apps**. The command defaults to launcher-visible apps and does not require `QUERY_ALL_PACKAGES`.
- `thirdParty`: keeps the full permission set and the existing SMS / Call Log / Photos functionality.
Policy links:
@@ -253,9 +252,9 @@ Pre-req checklist:
4) Open the app **Screen** tab and keep it active during the run (canvas/A2UI commands require the canvas WebView attached there).
5) Grant runtime permissions for capabilities you expect to pass (camera/mic/location/notification listener/location, etc.).
6) No interactive system dialogs should be pending before test start.
7) Canvas host is enabled and reachable from the device for remote Canvas checks (do not run gateway with `OPENCLAW_SKIP_CANVAS_HOST=1`; startup logs should include `canvas host mounted at .../__openclaw__/`).
7) Canvas host is enabled and reachable from the device (do not run gateway with `OPENCLAW_SKIP_CANVAS_HOST=1`; startup logs should include `canvas host mounted at .../__openclaw__/`).
8) Local operator test client pairing is approved. If first run fails with `pairing required`, preview the latest pending request, approve the printed request ID, then rerun:
9) For A2UI checks, keep the app on **Screen** tab; the node uses its bundled app-owned A2UI page for message application.
9) For A2UI checks, keep the app on **Screen** tab; the node now auto-refreshes canvas capability once on first A2UI reachability failure (TTL-safe retry).
```bash
openclaw devices list
@@ -287,8 +286,8 @@ Common failure quick-fixes:
- `pairing required` before tests start:
- list pending requests (`openclaw devices list`), then approve with the exact ID (`openclaw devices approve <requestId>`) and rerun.
- `A2UI host not reachable` / `A2UI_HOST_UNAVAILABLE`:
- keep the app foregrounded on the **Screen** tab and rerun. A2UI commands use the bundled app-owned A2UI page; the Gateway Canvas host is still needed for remote Canvas checks, but not for A2UI message application.
- `A2UI host not reachable` / `A2UI_HOST_NOT_CONFIGURED`:
- ensure the Canvas plugin host is running and reachable, keep the app on the **Screen** tab. The app refreshes the Canvas plugin surface URL once before failing; if it still fails, reconnect app and rerun.
- `NODE_BACKGROUND_UNAVAILABLE: canvas unavailable`:
- app is not effectively ready for canvas commands; keep app foregrounded and **Screen** tab active.

View File

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

View File

@@ -2,18 +2,10 @@ package ai.openclaw.app
import android.content.Intent
/** Android Assistant entry point used by manifest-declared app actions. */
const val actionAskOpenClaw = "ai.openclaw.app.action.ASK_OPENCLAW"
/** Debug action that opens the Voice tab directly for Android E2E automation. */
const val actionOpenVoiceE2e = "ai.openclaw.app.debug.OPEN_VOICE_E2E"
/** Intent extra that carries an optional assistant prompt for app actions. */
const val extraAssistantPrompt = "prompt"
/**
* Top-level home destinations that external actions may request.
*/
enum class HomeDestination {
Connect,
Chat,
@@ -22,30 +14,20 @@ enum class HomeDestination {
Settings,
}
/**
* Normalized launch request from Android Assistant or explicit app actions.
*/
data class AssistantLaunchRequest(
val source: String,
val prompt: String?,
val autoSend: Boolean,
)
/**
* Parses app-owned navigation actions that should open a specific home tab.
*/
fun parseHomeDestinationIntent(intent: Intent?): HomeDestination? {
val action = intent?.action ?: return null
return when {
// Debug-only shortcut keeps E2E navigation out of release builds.
BuildConfig.DEBUG && action == actionOpenVoiceE2e -> HomeDestination.Voice
else -> null
}
}
/**
* Parse external assistant entry points without starting any UI side effects.
*/
fun parseAssistantLaunchIntent(intent: Intent?): AssistantLaunchRequest? {
val action = intent?.action ?: return null
return when (action) {

View File

@@ -1,6 +1,5 @@
package ai.openclaw.app
/** Camera HUD state categories shown over the Android UI during capture. */
enum class CameraHudKind {
Photo,
Recording,
@@ -8,7 +7,6 @@ enum class CameraHudKind {
Error,
}
/** One-shot camera HUD message keyed by token so repeated text still replays. */
data class CameraHudState(
val token: Long,
val kind: CameraHudKind,

View File

@@ -5,7 +5,6 @@ import android.os.Build
import android.provider.Settings
object DeviceNames {
/** Prefers the user-visible Android device name, then falls back to manufacturer/model text. */
fun bestDefaultNodeName(context: Context): String {
val deviceName =
runCatching {
@@ -16,8 +15,6 @@ object DeviceNames {
if (deviceName.isNotEmpty()) return deviceName
// Manufacturer/model are best-effort platform fields; keep the final
// fallback stable so stored default names do not become blank.
val model =
listOfNotNull(Build.MANUFACTURER?.takeIf { it.isNotBlank() }, Build.MODEL?.takeIf { it.isNotBlank() })
.joinToString(" ")

View File

@@ -1,8 +1,5 @@
package ai.openclaw.app
/**
* Persisted location capture mode advertised to the gateway.
*/
enum class LocationMode(
val rawValue: String,
) {
@@ -11,10 +8,8 @@ enum class LocationMode(
;
companion object {
/** Parses persisted location mode text while migrating old always-on configs to while-using. */
fun fromRawValue(raw: String?): LocationMode {
val normalized = raw?.trim()?.lowercase()
// Older configs used "always"; Android node currently exposes while-using location only.
if (normalized == "always") return WhileUsing
return entries.firstOrNull { it.rawValue.lowercase() == normalized } ?: Off
}

View File

@@ -2,117 +2,34 @@ package ai.openclaw.app
import ai.openclaw.app.ui.OpenClawTheme
import ai.openclaw.app.ui.RootScreen
import android.content.Intent
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Main Android activity that owns Compose UI attachment and runtime UI wiring.
*/
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
private lateinit var permissionRequester: PermissionRequester
private var initializedViewModel: MainViewModel? = null
private var didAttachRuntimeUi = false
private var didStartNodeService = false
private var didStartViewModelCollectors = false
private var foreground = false
private var pendingIntent: Intent? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pendingIntent = intent
handleAssistantIntent(intent)
WindowCompat.setDecorFitsSystemWindows(window, false)
permissionRequester = PermissionRequester(this)
setContent {
var activeViewModel by remember { mutableStateOf<MainViewModel?>(null) }
LaunchedEffect(Unit) {
withFrameNanos { }
withContext(Dispatchers.Default) {
(application as NodeApp).prefs
}
val readyViewModel = viewModel
activateViewModel(readyViewModel)
activeViewModel = readyViewModel
}
OpenClawTheme {
activeViewModel?.let { RootScreen(viewModel = it) } ?: StartupSurface()
}
}
}
override fun onStart() {
super.onStart()
foreground = true
initializedViewModel?.setForeground(true)
}
override fun onStop() {
foreground = false
initializedViewModel?.setForeground(false)
super.onStop()
}
override fun onNewIntent(intent: android.content.Intent) {
super.onNewIntent(intent)
setIntent(intent)
pendingIntent = intent
initializedViewModel?.let { handleAssistantIntent(viewModel = it, intent = intent) }
}
/**
* Wires MainViewModel only after Activity first draw and background prefs warm-up.
*/
private fun activateViewModel(readyViewModel: MainViewModel) {
if (initializedViewModel != null) return
initializedViewModel = readyViewModel
readyViewModel.setForeground(foreground)
startViewModelCollectors(readyViewModel)
pendingIntent?.let { initialIntent ->
handleAssistantIntent(viewModel = readyViewModel, intent = initialIntent)
pendingIntent = null
}
}
/**
* Starts lifecycle collectors after ViewModel construction so they cannot force early startup.
*/
private fun startViewModelCollectors(readyViewModel: MainViewModel) {
if (didStartViewModelCollectors) return
didStartViewModelCollectors = true
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
readyViewModel.preventSleep.collect { enabled ->
viewModel.preventSleep.collect { enabled ->
if (enabled) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
@@ -124,10 +41,9 @@ class MainActivity : ComponentActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
readyViewModel.runtimeInitialized.collect { ready ->
viewModel.runtimeInitialized.collect { ready ->
if (!ready || didAttachRuntimeUi) return@collect
// Runtime UI helpers need an Activity owner, so attach once after NodeRuntime is ready.
readyViewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
viewModel.attachRuntimeUi(owner = this@MainActivity, permissionRequester = permissionRequester)
didAttachRuntimeUi = true
if (!didStartNodeService) {
NodeForegroundService.start(this@MainActivity)
@@ -136,15 +52,33 @@ class MainActivity : ComponentActivity() {
}
}
}
setContent {
OpenClawTheme {
Surface(modifier = Modifier) {
RootScreen(viewModel = viewModel)
}
}
}
}
/**
* Routes assistant/app-action intents into ViewModel state without recreating the activity.
*/
private fun handleAssistantIntent(
viewModel: MainViewModel,
intent: Intent?,
) {
override fun onStart() {
super.onStart()
viewModel.setForeground(true)
}
override fun onStop() {
viewModel.setForeground(false)
super.onStop()
}
override fun onNewIntent(intent: android.content.Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleAssistantIntent(intent)
}
private fun handleAssistantIntent(intent: android.content.Intent?) {
parseHomeDestinationIntent(intent)?.let { destination ->
viewModel.requestHomeDestination(destination)
return
@@ -153,23 +87,3 @@ class MainActivity : ComponentActivity() {
viewModel.handleAssistantLaunch(request)
}
}
@Composable
private fun StartupSurface() {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color.Black,
contentColor = Color.White,
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = "OPENCLAW",
fontSize = 22.sp,
fontWeight = FontWeight.Medium,
)
}
}
}

View File

@@ -4,8 +4,6 @@ import ai.openclaw.app.chat.ChatMessage
import ai.openclaw.app.chat.ChatPendingToolCall
import ai.openclaw.app.chat.ChatSessionEntry
import ai.openclaw.app.chat.OutgoingAttachment
import ai.openclaw.app.gateway.DeviceAuthStore
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.GatewayUpdateAvailableSummary
import ai.openclaw.app.node.CameraCaptureManager
@@ -16,7 +14,6 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -24,11 +21,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
* UI-facing bridge that exposes NodeRuntime and preference state as Compose-friendly StateFlows.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel(
app: Application,
@@ -36,11 +29,7 @@ class MainViewModel(
private val nodeApp = app as NodeApp
private val prefs = nodeApp.prefs
private val runtimeRef = MutableStateFlow<NodeRuntime?>(null)
@Volatile private var foreground = false
@Volatile private var runtimeStartupQueued = false
private var foreground = true
private val _requestedHomeDestination = MutableStateFlow<HomeDestination?>(null)
val requestedHomeDestination: StateFlow<HomeDestination?> = _requestedHomeDestination
private val _startOnboardingAtGatewaySetup = MutableStateFlow(false)
@@ -50,9 +39,6 @@ class MainViewModel(
private val _pendingAssistantAutoSend = MutableStateFlow<String?>(null)
val pendingAssistantAutoSend: StateFlow<String?> = _pendingAssistantAutoSend
/**
* Lazily starts NodeRuntime and preserves the current foreground bit across startup.
*/
private fun ensureRuntime(): NodeRuntime {
runtimeRef.value?.let { return it }
val runtime = nodeApp.ensureRuntime()
@@ -61,22 +47,6 @@ class MainViewModel(
return runtime
}
/**
* Starts the node runtime off the main thread so fresh installs can render
* the shell before encrypted prefs, device identity, and gateway setup warm up.
*/
private fun queueRuntimeStartup() {
if (runtimeRef.value != null || runtimeStartupQueued) return
runtimeStartupQueued = true
viewModelScope.launch(Dispatchers.Default) {
runCatching { ensureRuntime() }
runtimeStartupQueued = false
}
}
/**
* Adapts a runtime StateFlow to a stable ViewModel StateFlow before runtime startup.
*/
private fun <T> runtimeState(
initial: T,
selector: (NodeRuntime) -> StateFlow<T>,
@@ -112,7 +82,6 @@ class MainViewModel(
val isConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.isConnected }
val isNodeConnected: StateFlow<Boolean> = runtimeState(initial = false) { it.nodeConnected }
val statusText: StateFlow<String> = runtimeState(initial = "Offline") { it.statusText }
val gatewayConnectionProblem: StateFlow<GatewayConnectionProblem?> = runtimeState(initial = null) { it.gatewayConnectionProblem }
val serverName: StateFlow<String?> = runtimeState(initial = null) { it.serverName }
val remoteAddress: StateFlow<String?> = runtimeState(initial = null) { it.remoteAddress }
val gatewayVersion: StateFlow<String?> = runtimeState(initial = null) { it.gatewayVersion }
@@ -170,7 +139,6 @@ class MainViewModel(
val gatewayBootstrapToken: StateFlow<String> = prefs.gatewayBootstrapToken
val onboardingCompleted: StateFlow<Boolean> = prefs.onboardingCompleted
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
val installedAppsSharingEnabled: StateFlow<Boolean> = prefs.installedAppsSharingEnabled
val speakerEnabled: StateFlow<Boolean> = prefs.speakerEnabled
val voiceCaptureMode: StateFlow<VoiceCaptureMode> = runtimeState(initial = VoiceCaptureMode.Off) { it.voiceCaptureMode }
val micEnabled: StateFlow<Boolean> = runtimeState(initial = false) { it.micEnabled }
@@ -202,6 +170,12 @@ class MainViewModel(
val chatSessions: StateFlow<List<ChatSessionEntry>> = runtimeState(initial = emptyList()) { it.chatSessions }
val pendingRunCount: StateFlow<Int> = runtimeState(initial = 0) { it.pendingRunCount }
init {
if (prefs.onboardingCompleted.value) {
ensureRuntime()
}
}
val canvas: CanvasController
get() = ensureRuntime().canvas
@@ -211,9 +185,6 @@ class MainViewModel(
val sms: SmsManager
get() = ensureRuntime().sms
/**
* Attaches Activity-owned permission and lifecycle seams after runtime initialization.
*/
fun attachRuntimeUi(
owner: LifecycleOwner,
permissionRequester: PermissionRequester,
@@ -224,15 +195,15 @@ class MainViewModel(
runtime.sms.attachPermissionRequester(permissionRequester)
}
/**
* Starts runtime on foreground entry only after onboarding has completed.
*/
fun setForeground(value: Boolean) {
foreground = value
if (value && prefs.onboardingCompleted.value) {
queueRuntimeStartup()
}
runtimeRef.value?.setForeground(value)
val runtime =
if (value && prefs.onboardingCompleted.value) {
ensureRuntime()
} else {
runtimeRef.value
}
runtime?.setForeground(value)
}
fun setDisplayName(value: String) {
@@ -283,54 +254,10 @@ class MainViewModel(
prefs.setGatewayPassword(value)
}
/** Clears setup credentials without starting the runtime just to discard first-run pairing auth. */
private fun resetGatewaySetupAuth() {
runtimeRef.value?.resetGatewaySetupAuth() ?: resetGatewaySetupAuthWithoutRuntime()
fun resetGatewaySetupAuth() {
ensureRuntime().resetGatewaySetupAuth()
}
private fun resetGatewaySetupAuthWithoutRuntime() {
prefs.clearGatewaySetupAuth()
val deviceId = DeviceIdentityStore(nodeApp).loadOrCreate().deviceId
val deviceAuthStore = DeviceAuthStore(prefs)
deviceAuthStore.clearToken(deviceId, "node")
deviceAuthStore.clearToken(deviceId, "operator")
}
fun saveGatewayConfigAndConnect(
host: String,
port: Int,
tls: Boolean,
token: String,
bootstrapToken: String,
password: String,
resetSetupAuth: Boolean,
) {
// Gateway pairing touches encrypted prefs, identity files, and sockets; keep
// the whole sequence off the Compose thread so retries cannot trigger ANRs.
viewModelScope.launch(Dispatchers.Default) {
if (resetSetupAuth) {
resetGatewaySetupAuth()
}
prefs.setManualEnabled(true)
prefs.setManualHost(host)
prefs.setManualPort(port)
prefs.setManualTls(tls)
prefs.setGatewayBootstrapToken(bootstrapToken)
prefs.setGatewayToken(token)
prefs.setGatewayPassword(password)
ensureRuntime()
.connect(
GatewayEndpoint.manual(host = host, port = port),
NodeRuntime.GatewayConnectAuth(
token = token.ifEmpty { null },
bootstrapToken = bootstrapToken.ifEmpty { null },
password = password.ifEmpty { null },
),
)
}
}
/** Marks onboarding complete and starts the runtime before UI observes connected-state flows. */
fun setOnboardingCompleted(value: Boolean) {
if (value) {
ensureRuntime()
@@ -338,17 +265,13 @@ class MainViewModel(
prefs.setOnboardingCompleted(value)
}
/** Re-enters gateway setup after disconnecting and clearing one-time setup credentials. */
fun pairNewGateway() {
viewModelScope.launch(Dispatchers.Default) {
runtimeRef.value?.disconnect()
resetGatewaySetupAuth()
prefs.setOnboardingCompleted(false)
_startOnboardingAtGatewaySetup.value = true
}
runtimeRef.value?.disconnect()
resetGatewaySetupAuth()
_startOnboardingAtGatewaySetup.value = true
prefs.setOnboardingCompleted(false)
}
/** Acknowledges the one-shot request that opens onboarding at the gateway setup step. */
fun clearGatewaySetupStartRequest() {
_startOnboardingAtGatewaySetup.value = false
}
@@ -357,10 +280,6 @@ class MainViewModel(
prefs.setCanvasDebugStatusEnabled(value)
}
fun setInstalledAppsSharingEnabled(value: Boolean) {
ensureRuntime().setInstalledAppsSharingEnabled(value)
}
fun setNotificationForwardingEnabled(value: Boolean) {
ensureRuntime().setNotificationForwardingEnabled(value)
}
@@ -396,7 +315,6 @@ class MainViewModel(
ensureRuntime().setVoiceScreenActive(active)
}
/** Routes assistant intents into chat, either as a draft or queued auto-send prompt. */
fun handleAssistantLaunch(request: AssistantLaunchRequest) {
_requestedHomeDestination.value = HomeDestination.Chat
if (request.autoSend) {
@@ -441,25 +359,13 @@ class MainViewModel(
}
fun refreshGatewayConnection() {
viewModelScope.launch(Dispatchers.Default) {
ensureRuntime().refreshGatewayConnection()
}
}
fun startGatewayDiscovery() {
queueRuntimeStartup()
ensureRuntime().refreshGatewayConnection()
}
fun connect(endpoint: GatewayEndpoint) {
ensureRuntime().connect(endpoint)
}
fun connectInBackground(endpoint: GatewayEndpoint) {
viewModelScope.launch(Dispatchers.Default) {
ensureRuntime().connect(endpoint)
}
}
fun connect(
endpoint: GatewayEndpoint,
token: String?,

View File

@@ -3,17 +3,11 @@ package ai.openclaw.app
import android.app.Application
import android.os.StrictMode
/**
* Android Application singleton that owns process-wide secure prefs and lazy NodeRuntime startup.
*/
class NodeApp : Application() {
val prefs: SecurePrefs by lazy { SecurePrefs(this) }
@Volatile private var runtimeInstance: NodeRuntime? = null
/**
* Returns the single NodeRuntime for this process, creating it on first use.
*/
fun ensureRuntime(): NodeRuntime {
runtimeInstance?.let { return it }
return synchronized(this) {
@@ -21,9 +15,6 @@ class NodeApp : Application() {
}
}
/**
* Reads the runtime without forcing startup, used by lifecycle probes and services.
*/
fun peekRuntime(): NodeRuntime? = runtimeInstance
override fun onCreate() {

View File

@@ -19,7 +19,6 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
/** Foreground service that keeps the Android node connection and voice capture visible to the OS. */
class NodeForegroundService : Service() {
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var notificationJob: Job? = null
@@ -37,8 +36,6 @@ class NodeForegroundService : Service() {
stopSelf()
return
}
// Split connection and capture flows before combining so notification text
// can update without restarting runtime-owned connection work.
notificationJob =
scope.launch {
combine(
@@ -184,7 +181,6 @@ class NodeForegroundService : Service() {
private fun startForegroundWithTypes(notification: Notification) {
val serviceTypes = foregroundServiceTypesForVoiceMode(voiceCaptureMode)
if (didStartForeground) {
// Re-issue startForeground when Talk mode toggles so Android sees the microphone service type.
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceTypes)
return
}
@@ -200,19 +196,16 @@ class NodeForegroundService : Service() {
private const val ACTION_SET_VOICE_CAPTURE_MODE = "ai.openclaw.app.action.SET_VOICE_CAPTURE_MODE"
private const val EXTRA_VOICE_CAPTURE_MODE = "ai.openclaw.app.extra.VOICE_CAPTURE_MODE"
/** Starts the persistent node foreground service from UI lifecycle code. */
fun start(context: Context) {
val intent = Intent(context, NodeForegroundService::class.java)
context.startForegroundService(intent)
}
/** Requests disconnect through the service action path so notification actions and UI share behavior. */
fun stop(context: Context) {
val intent = Intent(context, NodeForegroundService::class.java).setAction(ACTION_STOP)
context.startService(intent)
}
/** Updates Android's foreground-service type before voice capture mode changes require microphone access. */
fun setVoiceCaptureMode(
context: Context,
mode: VoiceCaptureMode,
@@ -222,7 +215,6 @@ class NodeForegroundService : Service() {
.setAction(ACTION_SET_VOICE_CAPTURE_MODE)
.putExtra(EXTRA_VOICE_CAPTURE_MODE, mode.name)
if (mode == VoiceCaptureMode.TalkMode) {
// Microphone foreground service type must be declared before Talk capture starts.
ContextCompat.startForegroundService(context, intent)
} else {
context.startService(intent)
@@ -231,9 +223,6 @@ class NodeForegroundService : Service() {
}
}
/**
* Foreground-service type mask required by Android for the current voice capture mode.
*/
internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
val base = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
return if (mode == VoiceCaptureMode.TalkMode) {
@@ -243,9 +232,6 @@ internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
}
}
/**
* Compact notification suffix for voice state; kept pure for service-notification tests.
*/
internal fun voiceNotificationSuffix(
mode: VoiceCaptureMode,
manualMicEnabled: Boolean,
@@ -274,7 +260,6 @@ private fun String?.toVoiceCaptureMode(): VoiceCaptureMode =
it.name == this
} ?: VoiceCaptureMode.Off
/** Connection fields that drive foreground notification title/body text. */
private data class VoiceNotificationBase(
val status: String,
val server: String?,
@@ -282,7 +267,6 @@ private data class VoiceNotificationBase(
val mode: VoiceCaptureMode,
)
/** Voice capture fields that affect foreground-service type and suffix. */
private data class VoiceNotificationCapture(
val micEnabled: Boolean,
val micListening: Boolean,
@@ -290,7 +274,6 @@ private data class VoiceNotificationCapture(
val talkSpeaking: Boolean,
)
/** Aggregated notification state from runtime flows. */
private data class VoiceNotificationState(
val base: VoiceNotificationBase,
val capture: VoiceNotificationCapture,

View File

@@ -75,36 +75,11 @@ import kotlinx.serialization.json.buildJsonObject
import java.util.UUID
import java.util.concurrent.atomic.AtomicLong
/**
* Process runtime that owns gateway sessions, node command handlers, capture managers, and UI-facing state.
*/
data class GatewayConnectionProblem(
val code: String?,
val message: String,
val reason: String?,
val requestId: String?,
val recommendedNextStep: String?,
val pauseReconnect: Boolean,
val retryable: Boolean,
) {
val isPairingRequired: Boolean = code == "PAIRING_REQUIRED"
val canAutoRetry: Boolean =
isPairingRequired &&
(
retryable ||
!pauseReconnect ||
recommendedNextStep == "wait_then_retry"
)
}
class NodeRuntime(
context: Context,
val prefs: SecurePrefs = SecurePrefs(context.applicationContext),
private val tlsFingerprintProbe: suspend (String, Int) -> GatewayTlsProbeResult = ::probeGatewayTlsFingerprint,
) {
/**
* Authentication material supplied by setup/manual connect flows before gateway session routing.
*/
data class GatewayConnectAuth(
val token: String?,
val bootstrapToken: String?,
@@ -208,6 +183,8 @@ class NodeRuntime(
A2UIHandler(
canvas = canvas,
json = json,
getNodeCanvasHostUrl = { nodeSession.currentCanvasHostUrl() },
getOperatorCanvasHostUrl = { operatorSession.currentCanvasHostUrl() },
)
private val connectionManager: ConnectionManager =
@@ -224,7 +201,6 @@ class NodeRuntime(
callLogAvailable = { SensitiveFeatureConfig.callLogEnabled },
photosAvailable = { SensitiveFeatureConfig.photosEnabled },
hasRecordAudioPermission = { hasRecordAudioPermission() },
installedAppsSharingEnabled = { installedAppsSharingEnabled.value },
manualTls = { manualTls.value },
)
@@ -263,7 +239,6 @@ class NodeRuntime(
smsTelephonyAvailable = { sms.hasTelephonyFeature() },
callLogAvailable = { SensitiveFeatureConfig.callLogEnabled },
photosAvailable = { SensitiveFeatureConfig.photosEnabled },
installedAppsSharingEnabled = { installedAppsSharingEnabled.value },
debugBuild = { BuildConfig.DEBUG },
onCanvasA2uiPush = {
_canvasA2uiHydrated.value = true
@@ -271,13 +246,11 @@ class NodeRuntime(
_canvasRehydrateErrorText.value = null
},
onCanvasA2uiReset = { _canvasA2uiHydrated.value = false },
refreshCanvasHostUrl = { nodeSession.refreshCanvasHostUrl() },
motionActivityAvailable = { motionHandler.isActivityAvailable() },
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
)
/**
* Pending TLS trust decision when a gateway certificate is new or has changed.
*/
data class GatewayTrustPrompt(
val endpoint: GatewayEndpoint,
val fingerprintSha256: String,
@@ -304,16 +277,11 @@ class NodeRuntime(
private val _statusText = MutableStateFlow("Offline")
val statusText: StateFlow<String> = _statusText.asStateFlow()
private val _gatewayConnectionProblem = MutableStateFlow<GatewayConnectionProblem?>(null)
val gatewayConnectionProblem: StateFlow<GatewayConnectionProblem?> = _gatewayConnectionProblem.asStateFlow()
private val _pendingGatewayTrust = MutableStateFlow<GatewayTrustPrompt?>(null)
val pendingGatewayTrust: StateFlow<GatewayTrustPrompt?> = _pendingGatewayTrust.asStateFlow()
private val connectAttemptSeq = AtomicLong(0)
/**
* Builds the node-owned session key from stable device identity plus optional active agent.
*/
private fun resolveNodeMainSessionKey(agentId: String? = null): String {
val deviceId = identityStore.loadOrCreate().deviceId
return buildNodeMainSessionKey(deviceId, agentId)
@@ -431,7 +399,6 @@ class NodeRuntime(
identityStore = identityStore,
deviceAuthStore = deviceAuthStore,
onConnected = { hello ->
_gatewayConnectionProblem.value = null
operatorConnected = true
operatorStatusText = "Connected"
_serverName.value = hello.serverName
@@ -479,7 +446,6 @@ class NodeRuntime(
updateStatus()
micCapture.onGatewayConnectionChanged(false)
},
onConnectFailure = ::handleGatewayConnectFailure,
onEvent = { event, payloadJson ->
handleGatewayEvent(event, payloadJson)
},
@@ -491,7 +457,6 @@ class NodeRuntime(
identityStore = identityStore,
deviceAuthStore = deviceAuthStore,
onConnected = {
_gatewayConnectionProblem.value = null
_nodeConnected.value = true
nodeStatusText = "Connected"
didAutoRequestCanvasRehydrate = false
@@ -517,7 +482,6 @@ class NodeRuntime(
updateStatus()
showLocalCanvasOnDisconnect()
},
onConnectFailure = ::handleGatewayConnectFailure,
onEvent = { _, _ -> },
onInvoke = { req ->
invokeDispatcher.handleInvoke(req.command, req.paramsJson)
@@ -712,23 +676,6 @@ class NodeRuntime(
updateHomeCanvasState()
}
private fun handleGatewayConnectFailure(
error: GatewaySession.ErrorShape,
pauseReconnect: Boolean,
) {
val details = error.details
_gatewayConnectionProblem.value =
GatewayConnectionProblem(
code = details?.code ?: error.code,
message = error.message,
reason = details?.reason,
requestId = details?.requestId,
recommendedNextStep = details?.recommendedNextStep,
pauseReconnect = pauseReconnect || details?.pauseReconnect == true,
retryable = details?.retryable == true,
)
}
private fun resolveMainSessionKey(): String {
val trimmed = _mainSessionKey.value.trim()
return if (trimmed.isEmpty()) "main" else trimmed
@@ -894,7 +841,6 @@ class NodeRuntime(
fun setGatewayPassword(value: String) = prefs.setGatewayPassword(value)
/** Clears setup credentials plus paired device tokens for both Android gateway roles. */
fun resetGatewaySetupAuth() {
prefs.clearGatewaySetupAuth()
val deviceId = identityStore.loadOrCreate().deviceId
@@ -902,12 +848,10 @@ class NodeRuntime(
deviceAuthStore.clearToken(deviceId, "operator")
}
/** Persists onboarding state; callers decide whether runtime startup is needed first. */
fun setOnboardingCompleted(value: Boolean) = prefs.setOnboardingCompleted(value)
val lastDiscoveredStableId: StateFlow<String> = prefs.lastDiscoveredStableId
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
val installedAppsSharingEnabled: StateFlow<Boolean> = prefs.installedAppsSharingEnabled
val notificationForwardingEnabled: StateFlow<Boolean> = prefs.notificationForwardingEnabled
val notificationForwardingMode: StateFlow<NotificationPackageFilterMode> =
prefs.notificationForwardingMode
@@ -973,7 +917,6 @@ class NodeRuntime(
updateHomeCanvasState()
}
/** Updates foreground state and triggers reconnect/presence behavior on app visibility changes. */
fun setForeground(value: Boolean) {
_isForeground.value = value
if (value) {
@@ -1063,8 +1006,6 @@ class NodeRuntime(
if (didAutoConnect) return
if (_isConnected.value) return
val endpoint = resolvePreferredGatewayEndpoint() ?: return
// Only attempt the stored preferred gateway once per runtime lifetime; users
// can still reconnect explicitly from the UI after a failed auto attempt.
didAutoConnect = true
connect(endpoint)
}
@@ -1119,12 +1060,6 @@ class NodeRuntime(
prefs.setCanvasDebugStatusEnabled(value)
}
fun setInstalledAppsSharingEnabled(value: Boolean) {
if (prefs.installedAppsSharingEnabled.value == value) return
prefs.setInstalledAppsSharingEnabled(value)
refreshNodeSurfaceAfterSharingChange()
}
fun setNotificationForwardingEnabled(value: Boolean) {
prefs.setNotificationForwardingEnabled(value)
}
@@ -1452,24 +1387,16 @@ class NodeRuntime(
}
fun refreshGatewayConnection() {
val endpoint = connectedEndpoint
if (endpoint == null) {
resolvePreferredGatewayEndpoint()?.let(::connect)
?: run {
_statusText.value = "Failed: no saved gateway endpoint"
}
return
}
val endpoint =
connectedEndpoint ?: run {
_statusText.value = "Failed: no cached gateway endpoint"
return
}
operatorStatusText = "Connecting…"
updateStatus()
connectWithAuth(endpoint = endpoint, auth = resolveGatewayConnectAuth(), reconnect = true)
}
private fun refreshNodeSurfaceAfterSharingChange() {
val endpoint = connectedEndpoint ?: return
connectWithAuth(endpoint = endpoint, auth = resolveGatewayConnectAuth(), reconnect = true)
}
private fun connectWithAuth(
endpoint: GatewayEndpoint,
auth: GatewayConnectAuth,
@@ -1569,7 +1496,6 @@ class NodeRuntime(
connectAttemptId: Long,
) {
if (!isCurrentConnectAttempt(connectAttemptId)) return
_gatewayConnectionProblem.value = null
connectedEndpoint = endpoint
operatorStatusText = "Connecting…"
nodeStatusText = "Connecting…"
@@ -1666,7 +1592,6 @@ class NodeRuntime(
stopActiveVoiceSession()
connectedEndpoint = null
activeGatewayAuth = null
_gatewayConnectionProblem.value = null
_pendingGatewayTrust.value = null
operatorSession.disconnect()
nodeSession.disconnect()
@@ -1905,7 +1830,7 @@ class NodeRuntime(
return
}
try {
val modelsRes = operatorSession.request("models.list", "{}")
val modelsRes = operatorSession.request("models.list", """{"view":"all"}""")
val modelsRoot = json.parseToJsonElement(modelsRes).asObjectOrNull()
_modelCatalog.value = parseGatewayModels(modelsRoot?.get("models") as? JsonArray)
@@ -2132,7 +2057,6 @@ class NodeRuntime(
id = id,
name = obj["name"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() } ?: id,
provider = provider,
available = obj.optionalBoolean("available"),
supportsVision = "image" in inputTypes,
supportsAudio = "audio" in inputTypes,
supportsDocuments = "document" in inputTypes,
@@ -2749,7 +2673,6 @@ data class GatewayModelSummary(
val id: String,
val name: String,
val provider: String,
val available: Boolean?,
val supportsVision: Boolean,
val supportsAudio: Boolean,
val supportsDocuments: Boolean,
@@ -2932,15 +2855,6 @@ private fun JsonObject?.double(key: String): Double? = (this?.get(key) as? JsonP
private fun JsonObject?.boolean(key: String): Boolean = (this?.get(key) as? JsonPrimitive)?.content?.trim() == "true"
private fun JsonObject?.optionalBoolean(key: String): Boolean? =
(this?.get(key) as? JsonPrimitive)?.content?.trim()?.lowercase()?.let { value ->
when (value) {
"true" -> true
"false" -> false
else -> null
}
}
internal fun cronJobLastRunStatus(state: JsonObject?): String? =
state
.cronStatus("lastStatus")
@@ -2957,7 +2871,7 @@ fun providerDisplayName(provider: String): String =
when (provider.trim().lowercase()) {
"openai" -> "OpenAI"
"openrouter" -> "OpenRouter"
"codex" -> "Codex"
"openai-codex", "codex" -> "Codex"
"ollama", "ollama-local" -> "Ollama Local"
else ->
provider

View File

@@ -3,7 +3,6 @@ package ai.openclaw.app
import java.time.Instant
import java.time.ZoneId
/** Package-filter mode used before notification events are forwarded to the gateway. */
enum class NotificationPackageFilterMode(
val rawValue: String,
) {
@@ -12,12 +11,10 @@ enum class NotificationPackageFilterMode(
;
companion object {
/** Parses persisted filter mode text, defaulting to blocklist for safer forwarding. */
fun fromRawValue(raw: String?): NotificationPackageFilterMode = entries.firstOrNull { it.rawValue == raw?.trim()?.lowercase() } ?: Blocklist
}
}
/** Runtime policy used before forwarding notification events to a node session. */
internal data class NotificationForwardingPolicy(
val enabled: Boolean,
val mode: NotificationPackageFilterMode,
@@ -29,7 +26,6 @@ internal data class NotificationForwardingPolicy(
val sessionKey: String?,
)
/** Applies the operator-configured package allow/block list after trimming input. */
internal fun NotificationForwardingPolicy.allowsPackage(packageName: String): Boolean {
val normalized = packageName.trim()
if (normalized.isEmpty()) {
@@ -41,7 +37,6 @@ internal fun NotificationForwardingPolicy.allowsPackage(packageName: String): Bo
}
}
/** Returns true for both same-day and overnight quiet-hour windows. */
internal fun NotificationForwardingPolicy.isWithinQuietHours(
nowEpochMs: Long,
zoneId: ZoneId = ZoneId.systemDefault(),
@@ -69,14 +64,12 @@ internal fun NotificationForwardingPolicy.isWithinQuietHours(
private val localHourMinuteRegex = Regex("""^([01]\d|2[0-3]):([0-5]\d)$""")
/** Normalizes persisted or user-entered local times to strict HH:mm form. */
internal fun normalizeLocalHourMinute(raw: String): String? {
val trimmed = raw.trim()
val match = localHourMinuteRegex.matchEntire(trimmed) ?: return null
return "${match.groupValues[1]}:${match.groupValues[2]}"
}
/** Converts strict local HH:mm text to minutes since midnight for window checks. */
internal fun parseLocalHourMinute(raw: String): Int? {
val normalized = normalizeLocalHourMinute(raw) ?: return null
val parts = normalized.split(':')
@@ -85,13 +78,11 @@ internal fun parseLocalHourMinute(raw: String): Int? {
return hour * 60 + minute
}
/** Fixed-window limiter that bounds notification bursts per wall-clock minute. */
internal class NotificationBurstLimiter {
private val lock = Any()
private var windowStartMs: Long = -1L
private var eventsInWindow: Int = 0
/** Returns true when the current minute bucket still has forwarding capacity. */
fun allow(
nowEpochMs: Long,
maxEventsPerMinute: Int,
@@ -99,8 +90,6 @@ internal class NotificationBurstLimiter {
if (maxEventsPerMinute <= 0) {
return false
}
// Align all callers to the same minute bucket so concurrent notifications
// share the quota even when they arrive with slightly different timestamps.
val currentWindow = nowEpochMs - (nowEpochMs % 60_000L)
synchronized(lock) {
if (currentWindow != windowStartMs) {

View File

@@ -26,9 +26,6 @@ import kotlinx.coroutines.withTimeout
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
/**
* Serializes Android runtime-permission prompts behind coroutine-friendly request calls.
*/
class PermissionRequester internal constructor(
private val activity: ComponentActivity,
launcherFactory: ((Map<String, Boolean>) -> Unit) -> ActivityResultLauncher<Array<String>>,
@@ -53,13 +50,8 @@ class PermissionRequester internal constructor(
private val mutex = Mutex()
private val requestSlotsLock = Any()
private val mainHandler = Handler(Looper.getMainLooper())
// ActivityResult launchers cannot be registered after start; pre-register a small pool for nested UI flows.
private val launchers = List(4) { createPermissionRequestSlot(launcherFactory) }
/**
* Request missing Android runtime permissions and return the final grant state for every requested permission.
*/
suspend fun requestIfMissing(
permissions: List<String>,
timeoutMs: Long = 20_000,
@@ -101,7 +93,6 @@ class PermissionRequester internal constructor(
try {
withTimeout(timeoutMs) { deferred.await() }
} catch (err: TimeoutCancellationException) {
// Late ActivityResult callbacks are ignored by completePermissionRequest.
request.timedOut = true
throw err
}
@@ -139,7 +130,6 @@ class PermissionRequester internal constructor(
private fun reservePermissionRequestSlot(request: PendingPermissionRequest): PermissionRequestSlot =
synchronized(requestSlotsLock) {
// The outer mutex serializes normal callers; this guard catches accidental concurrent launchers in tests.
val slot = launchers.firstOrNull { it.request == null } ?: error("permission request launcher busy")
slot.request = request
slot
@@ -155,7 +145,6 @@ class PermissionRequester internal constructor(
slot.request = null
}
} ?: return
// Timed-out requests have already resumed callers with failure; ignore any late platform callback.
if (request.timedOut) return
request.deferred.complete(result)
}
@@ -197,7 +186,6 @@ class PermissionRequester internal constructor(
val actualObserver =
LifecycleEventObserver { _, event ->
if (event != Lifecycle.Event.ON_DESTROY) return@LifecycleEventObserver
// Do not resume a destroyed Activity with a positive result.
finish(false)
}
observer = actualObserver

View File

@@ -15,9 +15,6 @@ import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import java.util.UUID
/**
* Reactive settings facade for Android node preferences and encrypted gateway credentials.
*/
class SecurePrefs(
context: Context,
private val securePrefsOverride: SharedPreferences? = null,
@@ -40,18 +37,14 @@ class SecurePrefs(
private const val notificationsForwardingMaxEventsPerMinuteKey =
"notifications.forwarding.maxEventsPerMinute"
private const val notificationsForwardingSessionKeyKey = "notifications.forwarding.sessionKey"
private const val installedAppsSharingEnabledKey = "device.apps.sharing.enabled"
private const val voiceMicEnabledKey = "voice.micEnabled"
}
private val appContext = context.applicationContext
private val json = Json { ignoreUnknownKeys = true }
// Non-secret UI/runtime preferences stay readable for migration and backup behavior.
private val plainPrefs: SharedPreferences =
appContext.getSharedPreferences(plainPrefsName, Context.MODE_PRIVATE)
// Gateway credentials and arbitrary secret strings are isolated behind EncryptedSharedPreferences.
private val masterKey by lazy {
MasterKey
.Builder(appContext)
@@ -116,10 +109,6 @@ class SecurePrefs(
MutableStateFlow(plainPrefs.getBoolean("canvas.debugStatusEnabled", false))
val canvasDebugStatusEnabled: StateFlow<Boolean> = _canvasDebugStatusEnabled
private val _installedAppsSharingEnabled =
MutableStateFlow(plainPrefs.getBoolean(installedAppsSharingEnabledKey, false))
val installedAppsSharingEnabled: StateFlow<Boolean> = _installedAppsSharingEnabled
private val _notificationForwardingEnabled =
MutableStateFlow(plainPrefs.getBoolean(notificationsForwardingEnabledKey, defaultNotificationForwardingEnabled))
val notificationForwardingEnabled: StateFlow<Boolean> = _notificationForwardingEnabled
@@ -258,18 +247,12 @@ class SecurePrefs(
_canvasDebugStatusEnabled.value = value
}
fun setInstalledAppsSharingEnabled(value: Boolean) {
plainPrefs.edit { putBoolean(installedAppsSharingEnabledKey, value) }
_installedAppsSharingEnabled.value = value
}
internal fun getNotificationForwardingPolicy(appPackageName: String): NotificationForwardingPolicy {
val modeRaw = plainPrefs.getString(notificationsForwardingModeKey, null)
val mode = NotificationPackageFilterMode.fromRawValue(modeRaw)
val configuredPackages = loadNotificationForwardingPackages()
val normalizedAppPackage = appPackageName.trim()
// Always block OpenClaw's own notifications in blocklist mode to prevent forwarding loops.
val defaultBlockedPackages =
if (normalizedAppPackage.isNotEmpty()) setOf(normalizedAppPackage) else emptySet()
@@ -328,7 +311,6 @@ class SecurePrefs(
.toSet()
.toList()
.sorted()
// Persist deterministic JSON so settings diffs and state restoration are stable.
val encoded = JsonArray(sanitized.map { JsonPrimitive(it) }).toString()
plainPrefs.edit { putString(notificationsForwardingPackagesKey, encoded) }
_notificationForwardingPackages.value = sanitized.toSet()
@@ -373,7 +355,6 @@ class SecurePrefs(
_notificationForwardingSessionKey.value = normalized
}
/** Loads manual or instance-scoped gateway token material from encrypted preferences. */
fun loadGatewayToken(): String? {
val manual =
_gatewayToken.value.trim().ifEmpty {
@@ -382,19 +363,16 @@ class SecurePrefs(
stored
}
if (manual.isNotEmpty()) return manual
// Per-instance tokens keep reused Android installs from sharing stale gateway auth.
val key = "gateway.token.${_instanceId.value}"
val stored = securePrefs.getString(key, null)?.trim()
return stored?.takeIf { it.isNotEmpty() }
}
/** Saves the paired gateway token under the current Android instance id. */
fun saveGatewayToken(token: String) {
val key = "gateway.token.${_instanceId.value}"
securePrefs.edit { putString(key, token.trim()) }
}
/** Loads the bootstrap token used during gateway setup and device-token handoff. */
fun loadGatewayBootstrapToken(): String? {
val key = "gateway.bootstrapToken.${_instanceId.value}"
val stored =
@@ -426,11 +404,9 @@ class SecurePrefs(
securePrefs.edit { putString(key, password.trim()) }
}
/** Clears manual/setup credentials without removing persisted role-specific device tokens. */
fun clearGatewaySetupAuth() {
val instanceId = _instanceId.value
securePrefs.edit {
// Clear both current manual credentials and instance-scoped setup credentials after pairing/reset.
remove("gateway.manual.token")
remove("gateway.token.$instanceId")
remove("gateway.bootstrapToken.$instanceId")
@@ -440,13 +416,11 @@ class SecurePrefs(
_gatewayBootstrapToken.value = ""
}
/** Loads the pinned gateway TLS fingerprint for a discovered/manual stable endpoint id. */
fun loadGatewayTlsFingerprint(stableId: String): String? {
val key = "gateway.tls.$stableId"
return plainPrefs.getString(key, null)?.trim()?.takeIf { it.isNotEmpty() }
}
/** Persists the gateway TLS fingerprint captured through TOFU or explicit trust. */
fun saveGatewayTlsFingerprint(
stableId: String,
fingerprint: String,
@@ -483,7 +457,6 @@ class SecurePrefs(
private fun loadOrCreateInstanceId(): String {
val existing = plainPrefs.getString("node.instanceId", null)?.trim()
if (!existing.isNullOrBlank()) return existing
// Instance id is not secret; it scopes local credentials and survives display-name changes.
val fresh = UUID.randomUUID().toString()
plainPrefs.edit { putString("node.instanceId", fresh) }
return fresh
@@ -493,7 +466,6 @@ class SecurePrefs(
val existing = plainPrefs.getString(displayNameKey, null)?.trim().orEmpty()
if (existing.isNotEmpty() && existing != "Android Node") return existing
// Replace the historical generic name with a device-specific default once.
val candidate = DeviceNames.bestDefaultNodeName(context).trim()
val resolved = candidate.ifEmpty { "Android Node" }
@@ -501,7 +473,6 @@ class SecurePrefs(
return resolved
}
/** Persists sanitized voice wake triggers and updates the reactive settings flow. */
fun setWakeWords(words: List<String>) {
val sanitized = WakeWords.sanitize(words, defaultWakeWords)
val encoded =
@@ -550,7 +521,7 @@ class SecurePrefs(
val raw = plainPrefs.getString(voiceWakeModeKey, null)
val resolved = VoiceWakeMode.fromRawValue(raw)
// Default ON (foreground) when unset, but keep "always" opt-in through explicit settings.
// Default ON (foreground) when unset.
if (raw.isNullOrBlank()) {
plainPrefs.edit { putString(voiceWakeModeKey, resolved.rawValue) }
}
@@ -562,7 +533,6 @@ class SecurePrefs(
val raw = plainPrefs.getString(locationModeKey, "off")
val resolved = LocationMode.fromRawValue(raw)
if (raw?.trim()?.lowercase() == "always") {
// Migrate old "always" configs to the current while-using contract.
plainPrefs.edit { putString(locationModeKey, resolved.rawValue) }
}
return resolved

View File

@@ -1,12 +1,10 @@
package ai.openclaw.app
/** Normalizes blank gateway session keys to the legacy main session alias. */
internal fun normalizeMainKey(raw: String?): String {
val trimmed = raw?.trim()
return if (!trimmed.isNullOrEmpty()) trimmed else "main"
}
/** Accepts only gateway session keys that can represent the main chat stream. */
internal fun isCanonicalMainSessionKey(raw: String?): Boolean {
val trimmed = raw?.trim().orEmpty()
if (trimmed.isEmpty()) return false
@@ -14,7 +12,6 @@ internal fun isCanonicalMainSessionKey(raw: String?): Boolean {
return trimmed.startsWith("agent:")
}
/** Extracts the agent id from canonical agent-scoped main session keys. */
internal fun resolveAgentIdFromMainSessionKey(raw: String?): String? {
val trimmed = raw?.trim().orEmpty()
if (!trimmed.startsWith("agent:")) return null
@@ -25,7 +22,6 @@ internal fun resolveAgentIdFromMainSessionKey(raw: String?): String? {
.ifEmpty { null }
}
/** Builds the node session key shape consumed by gateway chat and presence APIs. */
internal fun buildNodeMainSessionKey(
deviceId: String,
agentId: String?,

View File

@@ -1,8 +1,5 @@
package ai.openclaw.app
/**
* Persisted voice capture mode that controls foreground-service microphone requirements.
*/
enum class VoiceCaptureMode {
Off,
ManualMic,

View File

@@ -1,8 +1,5 @@
package ai.openclaw.app
/**
* Persisted wake-word mode; raw values are stored in secure preferences.
*/
enum class VoiceWakeMode(
val rawValue: String,
) {
@@ -12,9 +9,6 @@ enum class VoiceWakeMode(
;
companion object {
/**
* Invalid stored values fall back to foreground wake so hands-free behavior stays opt-in.
*/
fun fromRawValue(raw: String?): VoiceWakeMode = entries.firstOrNull { it.rawValue == raw?.trim()?.lowercase() } ?: Foreground
}
}

View File

@@ -1,16 +1,11 @@
package ai.openclaw.app
/**
* Wake-word parsing limits and sanitizers shared by settings and voice runtime paths.
*/
object WakeWords {
const val maxWords: Int = 32
const val maxWordLength: Int = 64
/** Splits comma-separated user input into non-empty wake-word entries. */
fun parseCommaSeparated(input: String): List<String> = input.split(",").map { it.trim() }.filter { it.isNotEmpty() }
/** Returns null when edited text normalizes to the current wake-word list. */
fun parseIfChanged(
input: String,
current: List<String>,
@@ -19,7 +14,6 @@ object WakeWords {
return if (parsed == current) null else parsed
}
/** Applies persisted-list bounds and falls back to defaults when all entries are empty. */
fun sanitize(
words: List<String>,
defaults: List<String>,

View File

@@ -61,17 +61,12 @@ class ChatController(
private val pendingRuns = mutableSetOf<String>()
private val pendingRunTimeoutJobs = ConcurrentHashMap<String, Job>()
// Preserve sent messages locally until chat.history includes the gateway-confirmed copy.
private val optimisticMessagesByRunId = LinkedHashMap<String, ChatMessage>()
private val pendingRunTimeoutMs = 120_000L
// Drops stale history responses after session switches or refresh races.
private val historyLoadGeneration = AtomicLong(0)
private var lastHealthPollAtMs: Long? = null
/** Clears transient chat state when the operator gateway session disconnects. */
fun onDisconnected(message: String) {
_healthOk.value = false
_errorText.value = null
@@ -83,7 +78,6 @@ class ChatController(
_sessionId.value = null
}
/** Loads a chat session, normalizing "main" to the current gateway-provided main session key. */
fun load(sessionKey: String) {
val key = normalizeRequestedSessionKey(sessionKey)
val generation = beginHistoryLoad(key, clearMessages = key != _sessionKey.value)
@@ -92,7 +86,6 @@ class ChatController(
}
}
/** Rebinds chat to a new canonical main session key after gateway hello/agent changes. */
fun applyMainSessionKey(mainSessionKey: String) {
val trimmed = mainSessionKey.trim()
if (trimmed.isEmpty()) return
@@ -115,7 +108,6 @@ class ChatController(
}
}
/** Refreshes current chat history and session list without clearing optimistic messages first. */
fun refresh() {
val key = normalizeRequestedSessionKey(_sessionKey.value)
val generation = beginHistoryLoad(key, clearMessages = false)
@@ -128,14 +120,12 @@ class ChatController(
scope.launch { fetchSessions(limit = limit) }
}
/** Persists the normalized thinking level used for subsequent chat sends. */
fun setThinkingLevel(thinkingLevel: String) {
val normalized = normalizeThinking(thinkingLevel)
if (normalized == _thinkingLevel.value) return
_thinkingLevel.value = normalized
}
/** Switches to another gateway chat session and starts a fresh history load. */
fun switchSession(sessionKey: String) {
val key = normalizeRequestedSessionKey(sessionKey)
if (key.isEmpty()) return
@@ -173,7 +163,6 @@ class ChatController(
return key
}
/** Queues a chat send without waiting for gateway acceptance. */
fun sendMessage(
message: String,
thinkingLevel: String,
@@ -188,7 +177,6 @@ class ChatController(
}
}
/** Sends a chat message and returns once the gateway accepts or rejects the request. */
suspend fun sendMessageAwaitAcceptance(
message: String,
thinkingLevel: String,
@@ -206,7 +194,7 @@ class ChatController(
val sessionKey = _sessionKey.value
val thinking = normalizeThinking(thinkingLevel)
// Optimistic user message keeps the composer responsive while chat.send and history refresh complete.
// Optimistic user message.
val userContent =
buildList {
add(ChatMessageContent(type = "text", text = text))
@@ -227,7 +215,6 @@ class ChatController(
role = "user",
content = userContent,
timestampMs = System.currentTimeMillis(),
idempotencyKey = "$runId:user",
)
optimisticMessagesByRunId[runId] = optimisticMessage
_messages.value = _messages.value + optimisticMessage
@@ -270,7 +257,6 @@ class ChatController(
val res = session.request("chat.send", params.toString())
val actualRunId = parseRunId(res) ?: runId
if (actualRunId != runId) {
// Gateway may return a canonical run id; move all pending bookkeeping to that id.
optimisticMessagesByRunId[actualRunId] = optimisticMessagesByRunId.remove(runId) ?: optimisticMessage
clearPendingRun(runId)
armPendingRunTimeout(actualRunId)
@@ -288,7 +274,6 @@ class ChatController(
}
}
/** Sends best-effort abort requests for every currently pending gateway run. */
fun abort() {
val runIds =
synchronized(pendingRuns) {
@@ -311,7 +296,6 @@ class ChatController(
}
}
/** Applies gateway chat/agent stream events to local transcript and pending-run state. */
fun handleGatewayEvent(
event: String,
payloadJson: String?,
@@ -353,7 +337,6 @@ class ChatController(
)
if (!isCurrentHistoryLoad(sessionKey, _sessionKey.value, generation, historyLoadGeneration.get())) return
val history = parseHistory(historyJson, sessionKey = sessionKey, previousMessages = _messages.value)
prunePersistedOptimisticMessages(history.messages)
_messages.value = mergeOptimisticMessages(incoming = history.messages, optimistic = optimisticMessagesByRunId.values)
_sessionId.value = history.sessionId
_historyLoading.value = false
@@ -413,7 +396,7 @@ class ChatController(
val state = payload["state"].asStringOrNull()
when (state) {
"delta" -> {
// Only show streaming text for runs we initiated in this controller.
// Only show streaming text for runs we initiated
if (!isPending) return
val text = parseAssistantDeltaText(payload)
if (!text.isNullOrEmpty()) {
@@ -426,8 +409,10 @@ class ChatController(
}
if (runId != null) {
clearPendingRun(runId)
optimisticMessagesByRunId.remove(runId)
} else {
clearPendingRuns(clearOptimisticMessages = false)
clearPendingRuns()
optimisticMessagesByRunId.clear()
}
pendingToolCallsById.clear()
publishPendingToolCalls()
@@ -457,7 +442,6 @@ class ChatController(
sessionKey = currentSessionKey,
previousMessages = _messages.value,
)
prunePersistedOptimisticMessages(history.messages)
_messages.value = mergeOptimisticMessages(incoming = history.messages, optimistic = optimisticMessagesByRunId.values)
_sessionId.value = history.sessionId
history.thinkingLevel
@@ -564,14 +548,12 @@ class ChatController(
}
}
private fun clearPendingRuns(clearOptimisticMessages: Boolean = true) {
private fun clearPendingRuns() {
for ((_, job) in pendingRunTimeoutJobs) {
job.cancel()
}
pendingRunTimeoutJobs.clear()
if (clearOptimisticMessages) {
optimisticMessagesByRunId.clear()
}
optimisticMessagesByRunId.clear()
synchronized(pendingRuns) {
pendingRuns.clear()
_pendingRunCount.value = 0
@@ -583,15 +565,6 @@ class ChatController(
_messages.value = _messages.value.filterNot { it.id == message.id }
}
private fun prunePersistedOptimisticMessages(incoming: List<ChatMessage>) {
val retained =
retainUnmatchedOptimisticMessages(
incoming = incoming,
optimistic = optimisticMessagesByRunId.values,
).toSet()
optimisticMessagesByRunId.entries.removeAll { entry -> entry.value !in retained }
}
private fun parseHistory(
historyJson: String,
sessionKey: String,
@@ -606,14 +579,13 @@ class ChatController(
array.mapNotNull { item ->
val obj = item.asObjectOrNull() ?: return@mapNotNull null
val role = obj["role"].asStringOrNull() ?: return@mapNotNull null
val content = parseChatMessageContents(obj)
val content = obj["content"].asArrayOrNull()?.mapNotNull(::parseChatMessageContent) ?: emptyList()
val ts = obj["timestamp"].asLongOrNull()
ChatMessage(
id = UUID.randomUUID().toString(),
role = role,
content = content,
timestampMs = ts,
idempotencyKey = obj["idempotencyKey"].asStringOrNull(),
)
}
@@ -665,9 +637,6 @@ internal fun isCurrentHistoryLoad(
activeGeneration: Long,
): Boolean = requestedSessionKey == currentSessionKey && requestGeneration == activeGeneration
/**
* Convert gateway chat content parts into Android UI content parts.
*/
internal fun parseChatMessageContent(el: JsonElement): ChatMessageContent? {
val obj = el.asObjectOrNull() ?: return null
return when (obj["type"].asStringOrNull() ?: "text") {
@@ -689,27 +658,11 @@ internal fun parseChatMessageContent(el: JsonElement): ChatMessageContent? {
}
}
internal fun parseChatMessageContents(obj: JsonObject): List<ChatMessageContent> {
obj["content"].asArrayOrNull()?.let { content ->
return content.mapNotNull(::parseChatMessageContent)
}
obj["content"].asStringOrNull()?.let { text ->
return listOf(ChatMessageContent(type = "text", text = text))
}
obj["text"].asStringOrNull()?.let { text ->
return listOf(ChatMessageContent(type = "text", text = text))
}
return emptyList()
}
internal data class MainSessionState(
val currentSessionKey: String,
val appliedMainSessionKey: String,
)
/**
* Rewrite only the active "main" alias when the gateway publishes a new canonical main session key.
*/
internal fun applyMainSessionKey(
currentSessionKey: String,
appliedMainSessionKey: String,
@@ -727,9 +680,6 @@ internal fun applyMainSessionKey(
)
}
/**
* Keep Compose item identity stable across history refreshes by matching existing messages to incoming copies.
*/
internal fun reconcileMessageIds(
previous: List<ChatMessage>,
incoming: List<ChatMessage>,
@@ -760,41 +710,26 @@ internal fun mergeOptimisticMessages(
): List<ChatMessage> {
if (optimistic.isEmpty()) return incoming
val missingOptimistic = retainUnmatchedOptimisticMessages(incoming = incoming, optimistic = optimistic)
val unmatchedIncoming = incoming.toMutableList()
val missingOptimistic =
optimistic.filter { message ->
val matchIndex =
unmatchedIncoming.indexOfFirst { incomingMessage ->
incomingMessageConsumesOptimistic(incomingMessage, message)
}
if (matchIndex >= 0) {
unmatchedIncoming.removeAt(matchIndex)
false
} else {
true
}
}
if (missingOptimistic.isEmpty()) return incoming
return (incoming + missingOptimistic).sortedWith(compareBy<ChatMessage> { it.timestampMs ?: Long.MAX_VALUE }.thenBy { it.id })
}
internal fun retainUnmatchedOptimisticMessages(
incoming: List<ChatMessage>,
optimistic: Collection<ChatMessage>,
): List<ChatMessage> {
if (optimistic.isEmpty()) return emptyList()
val unmatchedIncoming = incoming.toMutableList()
return optimistic.filter { message ->
val matchIndex =
unmatchedIncoming.indexOfFirst { incomingMessage ->
incomingMessageConsumesOptimistic(incomingMessage, message)
}
if (matchIndex >= 0) {
unmatchedIncoming.removeAt(matchIndex)
false
} else {
true
}
}
}
/**
* Message identity used only for refresh reconciliation; it avoids exposing gateway ids as UI keys.
*/
internal fun messageIdentityKey(message: ChatMessage): String? {
val idempotencyKey = message.idempotencyKey?.trim().orEmpty()
if (idempotencyKey.isNotEmpty()) {
return listOf(message.role.trim().lowercase(), idempotencyKey).joinToString(separator = "|")
}
val contentKey = messageContentIdentityKey(message) ?: return null
val timestamp = message.timestampMs?.toString().orEmpty()
if (timestamp.isEmpty() && contentKey.isEmpty()) return null
@@ -807,10 +742,6 @@ private fun incomingMessageConsumesOptimistic(
incoming: ChatMessage,
optimistic: ChatMessage,
): Boolean {
val optimisticIdempotencyKey = optimistic.idempotencyKey?.trim().orEmpty()
if (optimisticIdempotencyKey.isNotEmpty()) {
return incoming.idempotencyKey?.trim() == optimisticIdempotencyKey
}
if (optimisticMessageIdentityKey(incoming) != optimisticMessageIdentityKey(optimistic)) return false
val incomingTimestamp = incoming.timestampMs ?: return false
val optimisticTimestamp = optimistic.timestampMs ?: return true

View File

@@ -1,19 +1,12 @@
package ai.openclaw.app.chat
/**
* Chat transcript item as delivered by gateway chat history and live chat events.
*/
data class ChatMessage(
val id: String,
val role: String,
val content: List<ChatMessageContent>,
val timestampMs: Long?,
val idempotencyKey: String? = null,
)
/**
* One content part in a chat message; binary parts carry base64 plus their MIME metadata.
*/
data class ChatMessageContent(
val type: String = "text",
val text: String? = null,
@@ -22,9 +15,6 @@ data class ChatMessageContent(
val base64: String? = null,
)
/**
* Tool call placeholder shown while a gateway run is still streaming.
*/
data class ChatPendingToolCall(
val toolCallId: String,
val name: String,
@@ -33,18 +23,12 @@ data class ChatPendingToolCall(
val isError: Boolean? = null,
)
/**
* Stable session selector row; [key] is the gateway session key used in chat requests.
*/
data class ChatSessionEntry(
val key: String,
val updatedAtMs: Long?,
val displayName: String? = null,
)
/**
* Snapshot of one chat session, including optional thinking level selected on the gateway.
*/
data class ChatHistory(
val sessionKey: String,
val sessionId: String?,
@@ -52,9 +36,6 @@ data class ChatHistory(
val messages: List<ChatMessage>,
)
/**
* User-selected attachment payload sent to the gateway as inline base64.
*/
data class OutgoingAttachment(
val type: String,
val mimeType: String,

View File

@@ -1,10 +1,6 @@
package ai.openclaw.app.gateway
/**
* Decoder for Bonjour DNS-SD service names returned with decimal byte escapes.
*/
object BonjourEscapes {
/** Decodes Bonjour DNS-SD decimal escapes while preserving ordinary UTF-8. */
fun decode(input: String): String {
if (input.isEmpty()) return input
@@ -19,7 +15,6 @@ object BonjourEscapes {
val value =
((d0.code - '0'.code) * 100) + ((d1.code - '0'.code) * 10) + (d2.code - '0'.code)
if (value in 0..255) {
// Bonjour escape bytes are decimal octets, not Unicode code points.
bytes.add(value.toByte())
i += 4
continue

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