Compare commits

..

1 Commits

Author SHA1 Message Date
Ethan Chen 4c146f8511
Merge 569eb093d6 into 8410d5a050 2026-03-14 10:26:07 +08:00
1319 changed files with 9705 additions and 81210 deletions

View File

@ -1,8 +1,8 @@
---
description: Update OpenClaw from upstream when branch has diverged (ahead/behind)
description: Update Clawdbot from upstream when branch has diverged (ahead/behind)
---
# OpenClaw Upstream Sync Workflow
# Clawdbot Upstream Sync Workflow
Use this workflow when your fork has diverged from upstream (e.g., "18 commits ahead, 29 commits behind").
@ -132,16 +132,16 @@ pnpm mac:package
```bash
# Kill running app
pkill -x "OpenClaw" || true
pkill -x "Clawdbot" || true
# Move old version
mv /Applications/OpenClaw.app /tmp/OpenClaw-backup.app
mv /Applications/Clawdbot.app /tmp/Clawdbot-backup.app
# Install new build
cp -R dist/OpenClaw.app /Applications/
cp -R dist/Clawdbot.app /Applications/
# Launch
open /Applications/OpenClaw.app
open /Applications/Clawdbot.app
```
---
@ -235,7 +235,7 @@ If upstream introduced new model configurations:
# Check for OpenRouter API key requirements
grep -r "openrouter\|OPENROUTER" src/ --include="*.ts" --include="*.js"
# Update openclaw.json with fallback chains
# Update clawdbot.json with fallback chains
# Add model fallback configurations as needed
```

54
.github/CODEOWNERS vendored
View File

@ -1,54 +0,0 @@
# Protect the ownership rules themselves.
/.github/CODEOWNERS @steipete
# WARNING: GitHub CODEOWNERS uses last-match-wins semantics.
# If you add overlapping rules below the secops block, include @openclaw/secops
# on those entries too or you can silently remove required secops review.
# Security-sensitive code, config, and docs require secops review.
/SECURITY.md @openclaw/secops
/.github/dependabot.yml @openclaw/secops
/.github/codeql/ @openclaw/secops
/.github/workflows/codeql.yml @openclaw/secops
/src/security/ @openclaw/secops
/src/secrets/ @openclaw/secops
/src/config/*secret*.ts @openclaw/secops
/src/config/**/*secret*.ts @openclaw/secops
/src/gateway/*auth*.ts @openclaw/secops
/src/gateway/**/*auth*.ts @openclaw/secops
/src/gateway/*secret*.ts @openclaw/secops
/src/gateway/**/*secret*.ts @openclaw/secops
/src/gateway/security-path*.ts @openclaw/secops
/src/gateway/resolve-configured-secret-input-string*.ts @openclaw/secops
/src/gateway/protocol/**/*secret*.ts @openclaw/secops
/src/gateway/server-methods/secrets*.ts @openclaw/secops
/src/agents/*auth*.ts @openclaw/secops
/src/agents/**/*auth*.ts @openclaw/secops
/src/agents/auth-profiles*.ts @openclaw/secops
/src/agents/auth-health*.ts @openclaw/secops
/src/agents/auth-profiles/ @openclaw/secops
/src/agents/sandbox.ts @openclaw/secops
/src/agents/sandbox-*.ts @openclaw/secops
/src/agents/sandbox/ @openclaw/secops
/src/infra/secret-file*.ts @openclaw/secops
/src/cron/stagger.ts @openclaw/secops
/src/cron/service/jobs.ts @openclaw/secops
/docs/security/ @openclaw/secops
/docs/gateway/authentication.md @openclaw/secops
/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @openclaw/secops
/docs/gateway/sandboxing.md @openclaw/secops
/docs/gateway/secrets-plan-contract.md @openclaw/secops
/docs/gateway/secrets.md @openclaw/secops
/docs/gateway/security/ @openclaw/secops
/docs/cli/approvals.md @openclaw/secops
/docs/cli/sandbox.md @openclaw/secops
/docs/cli/security.md @openclaw/secops
/docs/cli/secrets.md @openclaw/secops
/docs/reference/secretref-credential-surface.md @openclaw/secops
/docs/reference/secretref-user-supplied-credentials-matrix.json @openclaw/secops
# Release workflow and its supporting release-path checks.
/.github/workflows/openclaw-npm-release.yml @openclaw/openclaw-release-managers
/docs/reference/RELEASING.md @openclaw/openclaw-release-managers
/scripts/openclaw-npm-publish.sh @openclaw/openclaw-release-managers
/scripts/openclaw-npm-release-check.ts @openclaw/openclaw-release-managers
/scripts/release-check.ts @openclaw/openclaw-release-managers

6
.github/labeler.yml vendored
View File

@ -6,6 +6,7 @@
"channel: discord":
- changed-files:
- any-glob-to-any-file:
- "src/discord/**"
- "extensions/discord/**"
- "docs/channels/discord.md"
"channel: irc":
@ -27,6 +28,7 @@
"channel: imessage":
- changed-files:
- any-glob-to-any-file:
- "src/imessage/**"
- "extensions/imessage/**"
- "docs/channels/imessage.md"
"channel: line":
@ -62,16 +64,19 @@
"channel: signal":
- changed-files:
- any-glob-to-any-file:
- "src/signal/**"
- "extensions/signal/**"
- "docs/channels/signal.md"
"channel: slack":
- changed-files:
- any-glob-to-any-file:
- "src/slack/**"
- "extensions/slack/**"
- "docs/channels/slack.md"
"channel: telegram":
- changed-files:
- any-glob-to-any-file:
- "src/telegram/**"
- "extensions/telegram/**"
- "docs/channels/telegram.md"
"channel: tlon":
@ -91,6 +96,7 @@
"channel: whatsapp-web":
- changed-files:
- any-glob-to-any-file:
- "src/web/**"
- "extensions/whatsapp/**"
- "docs/channels/whatsapp.md"
"channel: zalo":

View File

@ -159,9 +159,6 @@ jobs:
- runtime: node
task: extensions
command: pnpm test:extensions
- runtime: node
task: channels
command: pnpm test:channels
- runtime: node
task: protocol
command: pnpm protocol:check

View File

@ -12,15 +12,9 @@ on:
- "**/*.mdx"
- ".agents/**"
- "skills/**"
workflow_dispatch:
inputs:
tag:
description: Existing release tag to backfill (for example v2026.3.13)
required: true
type: string
concurrency:
group: docker-release-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
group: docker-release-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
@ -29,48 +23,9 @@ env:
IMAGE_NAME: ${{ github.repository }}
jobs:
validate_manual_backfill:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Validate tag input format
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-beta\.[1-9][0-9]*)?$ ]]; then
echo "Invalid release tag: ${RELEASE_TAG}"
exit 1
fi
- name: Checkout selected tag
uses: actions/checkout@v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
approve_manual_backfill:
if: github.event_name == 'workflow_dispatch'
needs: validate_manual_backfill
# WARNING: KEEP MANUAL BACKFILLS GATED BY THE docker-release ENVIRONMENT.
runs-on: ubuntu-24.04
environment: docker-release
steps:
- name: Approve Docker backfill
env:
RELEASE_TAG: ${{ inputs.tag }}
run: echo "Approved Docker backfill for $RELEASE_TAG"
# KEEP THIS WORKFLOW ON GITHUB-HOSTED RUNNERS.
# DO NOT MOVE IT BACK TO BLACKSMITH WITHOUT RE-VALIDATING TAG BUILDS AND BACKFILLS.
# Build amd64 images (default + slim share the build stage cache)
build-amd64:
needs: [approve_manual_backfill]
if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
# WARNING: DO NOT REVERT THIS TO A BLACKSMITH RUNNER WITHOUT RE-VALIDATING TAG BACKFILLS.
runs-on: ubuntu-24.04
runs-on: blacksmith-16vcpu-ubuntu-2404
permissions:
packages: write
contents: read
@ -80,9 +35,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
fetch-depth: 0
- name: Set up Docker Builder
uses: docker/setup-buildx-action@v4
@ -99,22 +51,21 @@ jobs:
shell: bash
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
run: |
set -euo pipefail
tags=()
slim_tags=()
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
tags+=("${IMAGE}:main-amd64")
slim_tags+=("${IMAGE}:main-slim-amd64")
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
version="${GITHUB_REF#refs/tags/v}"
tags+=("${IMAGE}:${version}-amd64")
slim_tags+=("${IMAGE}:${version}-slim-amd64")
fi
if [[ ${#tags[@]} -eq 0 ]]; then
echo "::error::No amd64 tags resolved for ref ${SOURCE_REF}"
echo "::error::No amd64 tags resolved for ref ${GITHUB_REF}"
exit 1
fi
{
@ -131,22 +82,19 @@ jobs:
- name: Resolve OCI labels (amd64)
id: labels
shell: bash
env:
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
run: |
set -euo pipefail
source_sha="$(git rev-parse HEAD)"
version="${source_sha}"
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
version="${GITHUB_SHA}"
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
version="main"
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
version="${GITHUB_REF#refs/tags/v}"
fi
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
{
echo "value<<EOF"
echo "org.opencontainers.image.revision=${source_sha}"
echo "org.opencontainers.image.revision=${GITHUB_SHA}"
echo "org.opencontainers.image.version=${version}"
echo "org.opencontainers.image.created=${created}"
echo "EOF"
@ -154,8 +102,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@v6
uses: useblacksmith/build-push-action@v2
with:
context: .
platforms: linux/amd64
@ -166,8 +113,7 @@ jobs:
- name: Build and push amd64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@v6
uses: useblacksmith/build-push-action@v2
with:
context: .
platforms: linux/amd64
@ -180,10 +126,7 @@ jobs:
# Build arm64 images (default + slim share the build stage cache)
build-arm64:
needs: [approve_manual_backfill]
if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
# WARNING: DO NOT REVERT THIS TO A BLACKSMITH RUNNER WITHOUT RE-VALIDATING TAG BACKFILLS.
runs-on: ubuntu-24.04-arm
runs-on: blacksmith-16vcpu-ubuntu-2404-arm
permissions:
packages: write
contents: read
@ -193,9 +136,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
fetch-depth: 0
- name: Set up Docker Builder
uses: docker/setup-buildx-action@v4
@ -212,22 +152,21 @@ jobs:
shell: bash
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
run: |
set -euo pipefail
tags=()
slim_tags=()
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
tags+=("${IMAGE}:main-arm64")
slim_tags+=("${IMAGE}:main-slim-arm64")
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
version="${GITHUB_REF#refs/tags/v}"
tags+=("${IMAGE}:${version}-arm64")
slim_tags+=("${IMAGE}:${version}-slim-arm64")
fi
if [[ ${#tags[@]} -eq 0 ]]; then
echo "::error::No arm64 tags resolved for ref ${SOURCE_REF}"
echo "::error::No arm64 tags resolved for ref ${GITHUB_REF}"
exit 1
fi
{
@ -244,22 +183,19 @@ jobs:
- name: Resolve OCI labels (arm64)
id: labels
shell: bash
env:
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
run: |
set -euo pipefail
source_sha="$(git rev-parse HEAD)"
version="${source_sha}"
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
version="${GITHUB_SHA}"
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
version="main"
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
version="${GITHUB_REF#refs/tags/v}"
fi
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
{
echo "value<<EOF"
echo "org.opencontainers.image.revision=${source_sha}"
echo "org.opencontainers.image.revision=${GITHUB_SHA}"
echo "org.opencontainers.image.version=${version}"
echo "org.opencontainers.image.created=${created}"
echo "EOF"
@ -267,8 +203,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@v6
uses: useblacksmith/build-push-action@v2
with:
context: .
platforms: linux/arm64
@ -279,8 +214,7 @@ jobs:
- name: Build and push arm64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@v6
uses: useblacksmith/build-push-action@v2
with:
context: .
platforms: linux/arm64
@ -293,19 +227,14 @@ jobs:
# Create multi-platform manifests
create-manifest:
needs: [approve_manual_backfill, build-amd64, build-arm64]
if: ${{ always() && needs.build-amd64.result == 'success' && needs.build-arm64.result == 'success' && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
# WARNING: DO NOT REVERT THIS TO A BLACKSMITH RUNNER WITHOUT RE-VALIDATING TAG BACKFILLS.
runs-on: ubuntu-24.04
runs-on: blacksmith-16vcpu-ubuntu-2404
permissions:
packages: write
contents: read
needs: [build-amd64, build-arm64]
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
fetch-depth: 0
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
@ -319,28 +248,25 @@ jobs:
shell: bash
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
IS_MANUAL_BACKFILL: ${{ github.event_name == 'workflow_dispatch' && '1' || '0' }}
run: |
set -euo pipefail
tags=()
slim_tags=()
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
tags+=("${IMAGE}:main")
slim_tags+=("${IMAGE}:main-slim")
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
version="${GITHUB_REF#refs/tags/v}"
tags+=("${IMAGE}:${version}")
slim_tags+=("${IMAGE}:${version}-slim")
# Manual backfills should only republish the requested version tags.
if [[ "${IS_MANUAL_BACKFILL}" != "1" && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
tags+=("${IMAGE}:latest")
slim_tags+=("${IMAGE}:slim")
fi
fi
if [[ ${#tags[@]} -eq 0 ]]; then
echo "::error::No manifest tags resolved for ref ${SOURCE_REF}"
echo "::error::No manifest tags resolved for ref ${GITHUB_REF}"
exit 1
fi
{

View File

@ -4,15 +4,9 @@ on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: Release tag to publish (for example v2026.3.14, v2026.3.14-beta.1, or fallback v2026.3.14-1)
required: true
type: string
concurrency:
group: openclaw-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
group: openclaw-npm-release-${{ github.ref }}
cancel-in-progress: false
env:
@ -21,132 +15,16 @@ env:
PNPM_VERSION: "10.23.0"
jobs:
preview_openclaw_npm:
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
use-sticky-disk: "false"
- name: Print release plan
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
RELEASE_SHA=$(git rev-parse HEAD)
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if [[ "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*-[1-9][0-9]*$ ]]; then
TAG_KIND="fallback correction"
else
TAG_KIND="standard"
fi
echo "Release plan for ${RELEASE_TAG}:"
echo "Resolved release SHA: ${RELEASE_SHA}"
echo "Resolved package version: ${PACKAGE_VERSION}"
echo "Resolved tag kind: ${TAG_KIND}"
if [[ "${TAG_KIND}" == "fallback correction" ]]; then
echo "Correction tag note: npm version remains ${PACKAGE_VERSION}"
fi
echo "Would run: git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main"
echo "Would run with env: RELEASE_SHA=${RELEASE_SHA} RELEASE_TAG=${RELEASE_TAG} RELEASE_MAIN_REF=origin/main pnpm release:openclaw:npm:check"
echo "Would run: npm view openclaw@${PACKAGE_VERSION} version"
echo "Would run: pnpm check"
echo "Would run: pnpm build"
echo "Would run: pnpm release:check"
- name: Validate release tag and package metadata
env:
RELEASE_TAG: ${{ github.ref_name }}
RELEASE_MAIN_REF: origin/main
run: |
set -euxo pipefail
RELEASE_SHA=$(git rev-parse HEAD)
export RELEASE_SHA RELEASE_TAG RELEASE_MAIN_REF
# Fetch the full main ref so merge-base ancestry checks keep working
# for older tagged commits that are still contained in main.
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
pnpm release:openclaw:npm:check
- name: Ensure version is not already published
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euxo pipefail
PACKAGE_VERSION=$(node -p "require('./package.json').version")
IS_CORRECTION_TAG=0
if [[ "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*-[1-9][0-9]*$ ]]; then
IS_CORRECTION_TAG=1
fi
if npm view "openclaw@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
if [[ "${IS_CORRECTION_TAG}" == "1" ]]; then
echo "openclaw@${PACKAGE_VERSION} is already published on npm."
echo "Correction tag ${RELEASE_TAG} is allowed as a fallback release tag, so preview will continue without treating this as an error."
exit 0
fi
echo "openclaw@${PACKAGE_VERSION} is already published on npm."
exit 1
fi
if [[ "${IS_CORRECTION_TAG}" == "1" ]]; then
echo "Previewing fallback correction tag ${RELEASE_TAG} for npm version openclaw@${PACKAGE_VERSION}"
else
echo "Previewing openclaw@${PACKAGE_VERSION}"
fi
- name: Check
run: |
set -euxo pipefail
pnpm check
- name: Build
run: |
set -euxo pipefail
pnpm build
- name: Verify release contents
run: |
set -euxo pipefail
pnpm release:check
- name: Preview publish command
run: bash scripts/openclaw-npm-publish.sh --dry-run
publish_openclaw_npm:
if: github.event_name == 'workflow_dispatch'
# npm trusted publishing + provenance requires a GitHub-hosted runner.
runs-on: ubuntu-latest
environment: npm-release
permissions:
contents: read
id-token: write
steps:
- name: Validate tag input format
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then
echo "Invalid release tag format: ${RELEASE_TAG}"
exit 1
fi
- name: Checkout
uses: actions/checkout@v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
- name: Setup Node environment
@ -159,12 +37,11 @@ jobs:
- name: Validate release tag and package metadata
env:
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_SHA: ${{ github.sha }}
RELEASE_TAG: ${{ github.ref_name }}
RELEASE_MAIN_REF: origin/main
run: |
set -euo pipefail
RELEASE_SHA=$(git rev-parse HEAD)
export RELEASE_SHA RELEASE_TAG RELEASE_MAIN_REF
# Fetch the full main ref so merge-base ancestry checks keep working
# for older tagged commits that are still contained in main.
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
@ -192,4 +69,12 @@ jobs:
run: pnpm release:check
- name: Publish
run: bash scripts/openclaw-npm-publish.sh --publish
run: |
set -euo pipefail
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if [[ "$PACKAGE_VERSION" == *-beta.* ]]; then
npm publish --access public --tag beta --provenance
else
npm publish --access public --provenance
fi

View File

@ -4,7 +4,6 @@ on:
pull_request:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: workflow-sanity-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@ -15,7 +14,6 @@ env:
jobs:
no-tabs:
if: github.event_name != 'workflow_dispatch'
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout
@ -47,7 +45,6 @@ jobs:
PY
actionlint:
if: github.event_name != 'workflow_dispatch'
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout
@ -71,19 +68,3 @@ jobs:
- name: Disallow direct inputs interpolation in composite run blocks
run: python3 scripts/check-composite-action-input-interpolation.py
config-docs-drift:
if: github.event_name == 'workflow_dispatch'
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "false"
- name: Check config docs drift statefile
run: pnpm config:docs:check

View File

@ -12314,14 +12314,14 @@
"filename": "src/config/schema.help.ts",
"hashed_secret": "9f4cda226d3868676ac7f86f59e4190eb94bd208",
"is_verified": false,
"line_number": 657
"line_number": 653
},
{
"type": "Secret Keyword",
"filename": "src/config/schema.help.ts",
"hashed_secret": "01822c8bbf6a8b136944b14182cb885100ec2eae",
"is_verified": false,
"line_number": 690
"line_number": 686
}
],
"src/config/schema.irc.ts": [
@ -12360,14 +12360,14 @@
"filename": "src/config/schema.labels.ts",
"hashed_secret": "e73c9fcad85cd4eecc74181ec4bdb31064d68439",
"is_verified": false,
"line_number": 219
"line_number": 217
},
{
"type": "Secret Keyword",
"filename": "src/config/schema.labels.ts",
"hashed_secret": "2eda7cd978f39eebec3bf03e4410a40e14167fff",
"is_verified": false,
"line_number": 328
"line_number": 326
}
],
"src/config/slack-http-config.test.ts": [

View File

@ -9,7 +9,6 @@
- PR review conversations: if a bot leaves review conversations on your PR, address them and resolve those conversations yourself once fixed. Leave a conversation unresolved only when reviewer or maintainer judgment is still needed; do not leave bot-conversation cleanup to maintainers.
- GitHub searching footgun: don't limit yourself to the first 500 issues or PRs when wanting to search all. Unless you're supposed to look at the most recent, keep going until you've reached the last page in the search
- Security advisory analysis: before triage/severity decisions, read `SECURITY.md` to align with OpenClaw's trust model and design boundaries.
- Do not edit files covered by security-focused `CODEOWNERS` rules unless a listed owner explicitly asked for the change or is already reviewing it with you. Treat those paths as restricted surfaces, not drive-by cleanup.
## Auto-close labels (issues and PRs)
@ -204,17 +203,12 @@
- Vocabulary: "makeup" = "mac app".
- Parallels macOS retests: use the snapshot most closely named like `macOS 26.3.1 fresh` when the user asks for a clean/fresh macOS rerun; avoid older Tahoe snapshots unless explicitly requested.
- Parallels beta smoke: use `--target-package-spec openclaw@<beta-version>` for the beta artifact, and pin the stable side with both `--install-version <stable-version>` and `--latest-version <stable-version>` for upgrade runs. npm dist-tags can move mid-run.
- Parallels beta smoke, Windows nuance: old stable `2026.3.12` still prints the Unicode Windows onboarding banner, so mojibake during the stable precheck log is expected there. Judge the beta package by the post-upgrade lane.
- Parallels macOS smoke playbook:
- `prlctl exec` is fine for deterministic repo commands, but it can misrepresent interactive shell behavior (`PATH`, `HOME`, `curl | bash`, shebang resolution). For installer parity or shell-sensitive repros, prefer the guest Terminal or `prlctl enter`.
- Fresh Tahoe snapshot current reality: `brew` exists, `node` may not be on `PATH` in noninteractive guest exec. Use absolute `/opt/homebrew/bin/node` for repo/CLI runs when needed.
- Preferred automation entrypoint: `pnpm test:parallels:macos`. It restores the snapshot most closely matching `macOS 26.3.1 fresh`, serves the current `main` tarball from the host, then runs fresh-install and latest-release-to-main smoke lanes.
- Gateway verification in smoke runs should use `openclaw gateway status --deep --require-rpc`, not plain `--deep`, so probe failures go non-zero.
- Latest-release pre-upgrade diagnostics still need compatibility fallback: stable `2026.3.12` does not know `--require-rpc`, so precheck status dumps should fall back to plain `gateway status --deep` until the guest is upgraded.
- Harness output: pass `--json` for machine-readable summary; per-phase logs land under `/tmp/openclaw-parallels-smoke.*`.
- All-OS parallel runs should share the host `dist` build via `/tmp/openclaw-parallels-build.lock` instead of rebuilding three times.
- Current expected outcome on latest stable pre-upgrade: `precheck=latest-ref-fail` is normal on `2026.3.12`; treat it as a baseline signal, not a regression, unless the post-upgrade `main` lane also fails.
- Fresh host-served tgz install: restore fresh snapshot, install tgz as guest root with `HOME=/var/root`, then run onboarding as the desktop user via `prlctl exec --current-user`.
- For `openclaw onboard --non-interactive --secret-input-mode ref --install-daemon`, expect env-backed auth-profile refs (for example `OPENAI_API_KEY`) to be copied into the service env at install time; this path was fixed and should stay green.
- Dont run local + gateway agent turns in parallel on the same fresh workspace/session; they can collide on the session lock. Run sequentially.
@ -222,13 +216,10 @@
- Parallels Windows smoke playbook:
- Preferred automation entrypoint: `pnpm test:parallels:windows`. It restores the snapshot most closely matching `pre-openclaw-native-e2e-2026-03-12`, serves the current `main` tarball from the host, then runs fresh-install and latest-release-to-main smoke lanes.
- Gateway verification in smoke runs should use `openclaw gateway status --deep --require-rpc`, not plain `--deep`, so probe failures go non-zero.
- Latest-release pre-upgrade diagnostics still need compatibility fallback: stable `2026.3.12` does not know `--require-rpc`, so precheck status dumps should fall back to plain `gateway status --deep` until the guest is upgraded.
- Always use `prlctl exec --current-user` for Windows guest runs; plain `prlctl exec` lands in `NT AUTHORITY\SYSTEM` and does not match the real desktop-user install path.
- Prefer explicit `npm.cmd` / `openclaw.cmd`. Bare `npm` / `openclaw` in PowerShell can hit the `.ps1` shim and fail under restrictive execution policy.
- Use PowerShell only as the transport (`powershell.exe -NoProfile -ExecutionPolicy Bypass`) and call the `.cmd` shims explicitly from inside it.
- Harness output: pass `--json` for machine-readable summary; per-phase logs land under `/tmp/openclaw-parallels-windows.*`.
- Current expected outcome on latest stable pre-upgrade: `precheck=latest-ref-fail` is normal on `2026.3.12`; treat it as a baseline signal, not a regression, unless the post-upgrade `main` lane also fails.
- Keep Windows onboarding/status text ASCII-clean in logs. Fancy punctuation in banners shows up as mojibake through the current guest PowerShell capture path.
- Parallels Linux smoke playbook:
- Preferred automation entrypoint: `pnpm test:parallels:linux`. It restores the snapshot most closely matching `fresh` on `Ubuntu 24.04.3 ARM64`, serves the current `main` tarball from the host, then runs fresh-install and latest-release-to-main smoke lanes.
- Use plain `prlctl exec` on this snapshot. `--current-user` is not the right transport there.
@ -240,7 +231,6 @@
- When you do run Linux gateway checks manually from an interactive guest shell, use `openclaw gateway status --deep --require-rpc` so an RPC miss is a hard failure.
- Prefer direct argv guest commands for fetch/install steps (`curl`, `npm install -g`, `openclaw ...`) over nested `bash -lc` quoting; Linux guest quoting through Parallels was the flaky part.
- Harness output: pass `--json` for machine-readable summary; per-phase logs land under `/tmp/openclaw-parallels-linux.*`.
- Current expected outcome on Linux smoke: fresh + upgrade should pass installer and `agent --local`; gateway remains `skipped-no-detached-linux-gateway` on this snapshot and should not be treated as a regression by itself.
- Never edit `node_modules` (global/Homebrew/npm/git installs too). Updates overwrite. Skill notes go in `tools.md` or `AGENTS.md`.
- When adding a new `AGENTS.md` anywhere in the repo, also add a `CLAUDE.md` symlink pointing to it (example: `ln -s AGENTS.md CLAUDE.md`).
- Signal: "update fly" => `fly ssh console -a flawd-bot -C "bash -lc 'cd /data/clawd/openclaw && git pull --rebase origin main'"` then `fly machines restart e825232f34d058 -a flawd-bot`.

View File

@ -6,94 +6,30 @@ Docs: https://docs.openclaw.ai
### Changes
- Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl.
- Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman.
- Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327.
- Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl.
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior.
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob.
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
### Fixes
- Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) thanks @luzhidong.
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. Thanks @vincentkoc.
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. Thanks @Coobiw.
- Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) thanks @scoootscooob.
- Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46532) Thanks @vincentkoc.
- Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults.
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969)
- Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao.
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146)
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`.
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. Thanks @vincentkoc.
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT.
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
### Fixes
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. Thanks @vincentkoc.
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
- Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#45777) Thanks @odysseus0.
- Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark.
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274)
## 2026.3.13
### Changes
- Browser/existing-session: add an official Chrome DevTools MCP attach mode for signed-in live Chrome sessions, with docs for `chrome://inspect/#remote-debugging` enablement and direct backlinks to Chromes own setup guides.
- Browser/act automation: add batched actions, selector targeting, and delayed clicks for browser act requests with normalized batch dispatch. Thanks @vincentkoc.
- Android/chat settings: redesign the chat settings sheet with grouped device and media sections, refresh the Connect and Voice tabs, and tighten the chat composer/session header for a denser mobile layout. (#44894) Thanks @obviyus.
- iOS/onboarding: add a first-run welcome pager before gateway setup, stop auto-opening the QR scanner, and show `/pair qr` instructions on the connect step. (#45054) Thanks @ngutman.
- Browser/existing-session: add an official Chrome DevTools MCP attach mode for signed-in live Chrome sessions, with docs for `chrome://inspect/#remote-debugging` enablement and direct backlinks to Chromes own setup guides.
- Browser/agents: add built-in `profile="user"` for the logged-in host browser and `profile="chrome-relay"` for the extension relay, so agent browser calls can prefer the real signed-in browser without the extra `browserSession` selector.
- Browser/act automation: add batched actions, selector targeting, and delayed clicks for browser act requests with normalized batch dispatch. Thanks @vincentkoc.
- Docker/timezone override: add `OPENCLAW_TZ` so `docker-setup.sh` can pin gateway and CLI containers to a chosen IANA timezone instead of inheriting the daemon default. (#34119) Thanks @Lanfei.
- Dependencies/pi: bump `@mariozechner/pi-agent-core`, `@mariozechner/pi-ai`, `@mariozechner/pi-coding-agent`, and `@mariozechner/pi-tui` to `0.58.0`.
- Cron/sessions: add `sessionTarget: "current"` and `session:<id>` support so cron jobs can bind to the creating session or a persistent named session instead of only `main` or `isolated`. Thanks @kkhomej33-netizen and @ImLukeF.
- Telegram/message send: add `--force-document` so Telegram image and GIF sends can upload as documents without compression. (#45111) Thanks @thepagent.
### Breaking
- **BREAKING:** Agents now load at most one root memory bootstrap file. `MEMORY.md` wins; `memory.md` is only used when `MEMORY.md` is absent. If you intentionally kept both files and depended on both being injected, merge them before upgrade. This also fixes duplicate memory injection on case-insensitive Docker mounts. (#26054) Thanks @Lanfei.
### Fixes
- Dashboard/chat UI: stop reloading full chat history on every live tool result in dashboard v2 so tool-heavy runs no longer trigger UI freeze/re-render storms while the final event still refreshes persisted history. (#45541) Thanks @BunsDev.
- Gateway/client requests: reject unanswered gateway RPC calls after a bounded timeout and clear their pending state, so stalled connections no longer leak hanging `GatewayClient.request()` promises indefinitely.
- Build/plugin-sdk bundling: bundle plugin-sdk subpath entries in one shared build pass so published packages stop duplicating shared chunks and avoid the recent plugin-sdk memory blow-up. (#45426) Thanks @TarasShyn.
- Ollama/reasoning visibility: stop promoting native `thinking` and `reasoning` fields into final assistant text so local reasoning models no longer leak internal thoughts in normal replies. (#45330) Thanks @xi7ang.
- Android/onboarding QR scan: switch setup QR scanning to Google Code Scanner so onboarding uses a more reliable scanner instead of the legacy embedded ZXing flow. (#45021) Thanks @obviyus.
- Browser/existing-session: harden driver validation and session lifecycle so transport errors trigger reconnects while tool-level errors preserve the session, and extract shared ARIA role sets to deduplicate Playwright and Chrome MCP snapshot paths. (#45682) Thanks @odysseus0.
- Browser/existing-session: accept text-only `list_pages` and `new_page` responses from Chrome DevTools MCP so live-session tab discovery and new-tab open flows keep working when the server omits structured page metadata.
- Control UI/insecure auth: preserve explicit shared token and password auth on plain-HTTP Control UI connects so LAN and reverse-proxy sessions no longer drop shared auth before the first WebSocket handshake. (#45088) Thanks @velvet-shark.
- Gateway/session reset: preserve `lastAccountId` and `lastThreadId` across gateway session resets so replies keep routing back to the same account and thread after `/reset`. (#44773) Thanks @Lanfei.
- macOS/onboarding: avoid self-restarting freshly bootstrapped launchd gateways and give new daemon installs longer to become healthy, so `openclaw onboard --install-daemon` no longer false-fails on slower Macs and fresh VM snapshots.
- Gateway/status: add `openclaw gateway status --require-rpc` and clearer Linux non-interactive daemon-install failure reporting so automation can fail hard on probe misses instead of treating a printed RPC error as green.
- macOS/exec approvals: respect per-agent exec approval settings in the gateway prompter, including allowlist fallback when the native prompt cannot be shown, so gateway-triggered `system.run` requests follow configured policy instead of always prompting or denying unexpectedly. (#13707) Thanks @sliekens.
- Telegram/media downloads: thread the same direct or proxy transport policy into SSRF-guarded file fetches so inbound attachments keep working when Telegram falls back between env-proxy and direct networking. (#44639) Thanks @obviyus.
- Telegram/inbound media IPv4 fallback: retry SSRF-guarded Telegram file downloads once with the same IPv4 fallback policy as Bot API calls so fresh installs on IPv6-broken hosts no longer fail to download inbound images.
- Control UI/insecure auth: preserve explicit shared token and password auth on plain-HTTP Control UI connects so LAN and reverse-proxy sessions no longer drop shared auth before the first WebSocket handshake. (#45088) Thanks @velvet-shark.
- Windows/gateway install: bound `schtasks` calls and fall back to the Startup-folder login item when task creation hangs, so native `openclaw gateway install` fails fast instead of wedging forever on broken Scheduled Task setups.
- Windows/gateway stop: resolve Startup-folder fallback listeners from the installed `gateway.cmd` port, so `openclaw gateway stop` now actually kills fallback-launched gateway processes before restart.
- Windows/gateway status: reuse the installed service command environment when reading runtime status, so startup-fallback gateways keep reporting the configured port and running state in `gateway status --json` instead of falling back to `gateway port unknown`.
- Windows/gateway auth: stop attaching device identity on local loopback shared-token and password gateway calls, so native Windows agent replies no longer log stale `device signature expired` fallback noise before succeeding.
- Discord/gateway startup: treat plain-text and transient `/gateway/bot` metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman.
- Gateway/session reset: preserve `lastAccountId` and `lastThreadId` across gateway session resets so replies keep routing back to the same account and thread after `/reset`. (#44773) Thanks @Lanfei.
- macOS/onboarding: avoid self-restarting freshly bootstrapped launchd gateways and give new daemon installs longer to become healthy, so `openclaw onboard --install-daemon` no longer false-fails on slower Macs and fresh VM snapshots.
- Slack/probe: keep `auth.test()` bot and team metadata mapping stable while simplifying the probe result path. (#44775) Thanks @Cafexss.
- Dashboard/chat UI: render oversized plain-text replies as normal paragraphs instead of capped gray code blocks, so long desktop chat responses stay readable without tab-switching refreshes.
- Dashboard/chat UI: restore the `chat-new-messages` class on the New messages scroll pill so the button uses its existing compact styling instead of rendering as a full-screen SVG overlay. (#44856) Thanks @Astro-Han.
- Gateway/Control UI: restore the operator-only device-auth bypass and classify browser connect failures so origin and device-identity problems no longer show up as auth errors in the Control UI and web chat. (#45512) thanks @sallyom.
- macOS/voice wake: stop crashing wake-word command extraction when speech segment ranges come from a different transcript instance.
- Discord/allowlists: honor raw `guild_id` when hydrated guild objects are missing so allowlisted channels and threads like `#maintainers` no longer get false-dropped before channel allowlist checks.
- macOS/runtime locator: require Node >=22.16.0 during macOS runtime discovery so the app no longer accepts Node versions that the main runtime guard rejects later. Thanks @sumleo.
- Agents/custom providers: preserve blank API keys for loopback OpenAI-compatible custom providers by clearing the synthetic Authorization header at runtime, while keeping explicit apiKey and oauth/token config from silently downgrading into fake bearer auth. (#45631) Thanks @xinhuagu.
- Models/google-vertex Gemini flash-lite normalization: apply existing bare-ID preview normalization to `google-vertex` model refs and provider configs so `google-vertex/gemini-3.1-flash-lite` resolves as `gemini-3.1-flash-lite-preview`. (#42435) thanks @scoootscooob.
- iMessage/remote attachments: reject unsafe remote attachment paths before spawning SCP, so sender-controlled filenames can no longer inject shell metacharacters into remote media staging. Thanks @lintsinghua.
- Telegram/webhook auth: validate the Telegram webhook secret before reading or parsing request bodies, so unauthenticated requests are rejected immediately instead of consuming up to 1 MB first. Thanks @space08.
- Security/device pairing: make bootstrap setup codes single-use so pending device pairing requests cannot be silently replayed and widened to admin before approval. Thanks @tdjackey.
@ -104,10 +40,13 @@ Docs: https://docs.openclaw.ai
- Security/exec approvals: unwrap `env` dispatch wrappers inside shell-segment allowlist resolution on macOS so `env FOO=bar /path/to/bin` resolves against the effective executable instead of the wrapper token.
- Security/exec approvals: treat backslash-newline as shell line continuation during macOS shell-chain parsing so line-continued `$(` substitutions fail closed instead of slipping past command-substitution checks.
- Security/exec approvals: bind macOS skill auto-allow trust to both executable name and resolved path so same-basename binaries no longer inherit trust from unrelated skill bins.
- Gateway/status: add `openclaw gateway status --require-rpc` and clearer Linux non-interactive daemon-install failure reporting so automation can fail hard on probe misses instead of treating a printed RPC error as green.
- Dashboard/chat UI: restore the `chat-new-messages` class on the New messages scroll pill so the button uses its existing compact styling instead of rendering as a full-screen SVG overlay. (#44856) Thanks @Astro-Han.
- Build/plugin-sdk bundling: bundle plugin-sdk subpath entries in one shared build pass so published packages stop duplicating shared chunks and avoid the recent plugin-sdk memory blow-up. (#45426) Thanks @TarasShyn.
- Cron/isolated sessions: route nested cron-triggered embedded runner work onto the nested lane so isolated cron jobs no longer deadlock when compaction or other queued inner work runs. Thanks @vincentkoc.
- Agents/OpenAI-compatible compat overrides: respect explicit user `models[].compat` opt-ins for non-native `openai-completions` endpoints so usage-in-streaming capability overrides no longer get forced off when the endpoint actually supports them. (#44432) Thanks @cheapestinference.
- Agents/Azure OpenAI startup prompts: rephrase the built-in `/new`, `/reset`, and post-compaction startup instruction so Azure OpenAI deployments no longer hit HTTP 400 false positives from the content filter. (#43403) Thanks @xingsy97.
- Agents/memory bootstrap: load only one root memory file, preferring `MEMORY.md` and using `memory.md` as a fallback, so case-insensitive Docker mounts no longer inject duplicate memory context. (#26054) Thanks @Lanfei.
- Agents/compaction: compare post-compaction token sanity checks against full-session pre-compaction totals and skip the check when token estimation fails, so sessions with large bootstrap context keep real token counts instead of falling back to unknown. (#28347) thanks @efe-arv.
- Agents/compaction: preserve safeguard compaction summary language continuity via default and configurable custom instructions so persona drift is reduced after auto-compaction. (#10456) Thanks @keepitmello.
- Agents/tool warnings: distinguish gated core tools like `apply_patch` from plugin-only unknown entries in `tools.profile` warnings, so unavailable core tools now report current runtime/provider/model/config gating instead of suggesting a missing plugin.
@ -116,12 +55,10 @@ Docs: https://docs.openclaw.ai
- Signal/config validation: add `channels.signal.groups` schema support so per-group `requireMention`, `tools`, and `toolsBySender` overrides no longer get rejected during config validation. (#27199) Thanks @unisone.
- Config/discovery: accept `discovery.wideArea.domain` in strict config validation so unicast DNS-SD gateway configs no longer fail with an unrecognized-key error. (#35615) Thanks @ingyukoh.
- Telegram/media errors: redact Telegram file URLs before building media fetch errors so failed inbound downloads do not leak bot tokens into logs. Thanks @space08.
- Agents/failover: normalize abort-wrapped `429 RESOURCE_EXHAUSTED` provider failures before abort short-circuiting so wrapped Google/Vertex rate limits continue across configured fallback models, including the embedded runner prompt-error path. (#39820) Thanks @lupuletic.
- Mattermost/thread routing: non-inbound reply paths (TUI/WebUI turns, tool-call callbacks, subagent responses) now correctly route to the originating Mattermost thread when `replyToMode: "all"` is active; also prevents stale `origin.threadId` metadata from resurrecting cleared thread routes. (#44283) thanks @teconomix
- Gateway/websocket pairing bypass for disabled auth: skip device-pairing enforcement when `gateway.auth.mode=none` so Control UI connections behind reverse proxies no longer get stuck on `pairing required` (code 1008) despite auth being explicitly disabled. (#42931)
- Auth/login lockout recovery: clear stale `auth_permanent` and `billing` disabled state for all profiles matching the target provider when `openclaw models auth login` is invoked, so users locked out by expired or revoked OAuth tokens can recover by re-authenticating instead of waiting for the cooldown timer to expire. (#43057)
- Auto-reply/context-engine compaction: persist the exact embedded-run metadata compaction count for main and followup runner session accounting, so metadata-only auto-compactions no longer undercount multi-compaction runs. (#42629) thanks @uf-hy.
- Auth/Codex CLI reuse: sync reused Codex CLI credentials into the supported `openai-codex:default` OAuth profile instead of reviving the deprecated `openai-codex:codex-cli` slot, so doctor cleanup no longer loops. (#45353) thanks @Gugu-sugar.
- Dashboard/chat UI: render oversized plain-text replies as normal paragraphs instead of capped gray code blocks, so long desktop chat responses stay readable without tab-switching refreshes.
- Gateway/Control UI: restore the operator-only device-auth bypass and classify browser connect failures so origin and device-identity problems no longer show up as auth errors in the Control UI and web chat. (#45512) thanks @sallyom.
- macOS/voice wake: stop crashing wake-word command extraction when speech segment ranges come from a different transcript instance.
- Discord/allowlists: honor raw `guild_id` when hydrated guild objects are missing so allowlisted channels and threads like `#maintainers` no longer get false-dropped before channel allowlist checks.
## 2026.3.12
@ -198,9 +135,7 @@ Docs: https://docs.openclaw.ai
- Mattermost/reply media delivery: pass agent-scoped `mediaLocalRoots` through shared reply delivery so allowed local files upload correctly from button, slash-command, and model-picker replies. (#44021) Thanks @LyleLiu666.
- Plugins/env-scoped roots: fix plugin discovery/load caches and provenance tracking so same-process `HOME`/`OPENCLAW_HOME` changes no longer reuse stale plugin state or misreport `~/...` plugins as untracked. (#44046) thanks @gumadeiras.
- Gateway/session discovery: discover disk-only and retired ACP session stores under custom templated `session.store` roots so ACP reconciliation, session-id/session-label targeting, and run-id fallback keep working after restart. (#44176) thanks @gumadeiras.
- Browser/existing-session: stop reporting fake CDP ports/URLs for live attached Chrome sessions, render `transport: chrome-mcp` in CLI/status output instead of `port: 0`, and keep timeout diagnostics transport-aware when no direct CDP URL exists.
- Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and `models list --plain`, and migrate legacy duplicated `openrouter/openrouter/...` config entries forward on write.
- Feishu/event dedupe: keep early duplicate suppression aligned with the shared Feishu message-id contract and release the pre-queue dedupe marker after failed dispatch so retried events can recover instead of being dropped until the short TTL expires. (#43762) Thanks @yunweibang.
- Gateway/hooks: bucket hook auth failures by forwarded client IP behind trusted proxies and warn when `hooks.allowedAgentIds` leaves hook routing unrestricted.
- Agents/compaction: skip the post-compaction `cache-ttl` marker write when a compaction completed in the same attempt, preventing the next turn from immediately triggering a second tiny compaction. (#28548) thanks @MoerAI.
- Native chat/macOS: add `/new`, `/reset`, and `/clear` reset triggers, keep shared main-session aliases aligned, and ignore stale model-selection completions so native chat state stays in sync across reset and fast model changes. (#10898) Thanks @Nachx639.
@ -354,7 +289,6 @@ Docs: https://docs.openclaw.ai
- Agents/failover: classify HTTP 422 malformed-request responses as `format` and recognize OpenRouter "requires more credits" billing errors so provider fallback triggers instead of surfacing raw errors. (#43823) thanks @jnMetaCode.
- Memory/QMD Windows: fail closed when `qmd.cmd` or `mcporter.cmd` wrappers cannot be resolved to a direct entrypoint, so memory search no longer falls back to shell execution on Windows.
- macOS/remote gateway: stop PortGuardian from killing Docker Desktop and other external listeners on the gateway port in remote mode, so containerized and tunneled gateway setups no longer lose their port-forward owner on app startup. (#6755) Thanks @teslamint.
- Feishu/streaming recovery: clear stale `streamingStartPromise` when card creation fails (HTTP 400) so subsequent messages can retry streaming instead of silently dropping all future replies. Fixes #43322.
## 2026.3.8

View File

@ -61,7 +61,7 @@ Welcome to the lobster tank! 🦞
- **Josh Lehman** - Compaction, Tlon/Urbit subsystem
- GitHub [@jalehman](https://github.com/jalehman) · X: [@jlehman\_](https://x.com/jlehman_)
- **Radek Sienkiewicz** - Docs, Control UI
- **Radek Sienkiewicz** - Control UI + WebChat correctness
- GitHub [@velvet-shark](https://github.com/velvet-shark) · X: [@velvet_shark](https://twitter.com/velvet_shark)
- **Muhammed Mukhthar** - Mattermost, CLI
@ -76,9 +76,6 @@ Welcome to the lobster tank! 🦞
- **Tengji (George) Zhang** - Chinese model APIs, cloud, pi
- GitHub: [@odysseus0](https://github.com/odysseus0) · X: [@odysseus0z](https://x.com/odysseus0z)
- **Andrew (Bubbles) Demczuk** - Agents/Gateway/TTS/VTT
- GitHub: [@ademczuk](https://github.com/ademczuk) · X: [@ademczuk](https://x.com/ademczuk)
## How to Contribute
1. **Bugs & small fixes** → Open a PR!
@ -96,7 +93,6 @@ Welcome to the lobster tank! 🦞
- Reply to or resolve bot review conversations you addressed before asking for review again
- **Include screenshots** — one showing the problem/before, one showing the fix/after (for UI or visual changes)
- Use American English spelling and grammar in code, comments, docs, and UI strings
- Do not edit files covered by `CODEOWNERS` security ownership unless a listed owner explicitly asked for the change or is already reviewing it with you. Treat those paths as restricted review surfaces, not opportunistic cleanup targets.
## Review Conversations Are Author-Owned

View File

@ -134,7 +134,7 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
procps hostname curl git lsof openssl
procps hostname curl git openssl
RUN chown node:node /app

View File

@ -2,8 +2,8 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text-dark.svg">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.svg" alt="OpenClaw" width="500">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text-dark.png">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.png" alt="OpenClaw" width="500">
</picture>
</p>

View File

@ -2,82 +2,6 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.3.13</title>
<pubDate>Sat, 14 Mar 2026 05:19:48 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026031390</sparkle:version>
<sparkle:shortVersionString>2026.3.13</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.3.13</h2>
<h3>Changes</h3>
<ul>
<li>Android/chat settings: redesign the chat settings sheet with grouped device and media sections, refresh the Connect and Voice tabs, and tighten the chat composer/session header for a denser mobile layout. (#44894) Thanks @obviyus.</li>
<li>iOS/onboarding: add a first-run welcome pager before gateway setup, stop auto-opening the QR scanner, and show <code>/pair qr</code> instructions on the connect step. (#45054) Thanks @ngutman.</li>
<li>Browser/existing-session: add an official Chrome DevTools MCP attach mode for signed-in live Chrome sessions, with docs for <code>chrome://inspect/#remote-debugging</code> enablement and direct backlinks to Chromes own setup guides.</li>
<li>Browser/agents: add built-in <code>profile="user"</code> for the logged-in host browser and <code>profile="chrome-relay"</code> for the extension relay, so agent browser calls can prefer the real signed-in browser without the extra <code>browserSession</code> selector.</li>
<li>Browser/act automation: add batched actions, selector targeting, and delayed clicks for browser act requests with normalized batch dispatch. Thanks @vincentkoc.</li>
<li>Docker/timezone override: add <code>OPENCLAW_TZ</code> so <code>docker-setup.sh</code> can pin gateway and CLI containers to a chosen IANA timezone instead of inheriting the daemon default. (#34119) Thanks @Lanfei.</li>
<li>Dependencies/pi: bump <code>@mariozechner/pi-agent-core</code>, <code>@mariozechner/pi-ai</code>, <code>@mariozechner/pi-coding-agent</code>, and <code>@mariozechner/pi-tui</code> to <code>0.58.0</code>.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Dashboard/chat UI: stop reloading full chat history on every live tool result in dashboard v2 so tool-heavy runs no longer trigger UI freeze/re-render storms while the final event still refreshes persisted history. (#45541) Thanks @BunsDev.</li>
<li>Gateway/client requests: reject unanswered gateway RPC calls after a bounded timeout and clear their pending state, so stalled connections no longer leak hanging <code>GatewayClient.request()</code> promises indefinitely.</li>
<li>Build/plugin-sdk bundling: bundle plugin-sdk subpath entries in one shared build pass so published packages stop duplicating shared chunks and avoid the recent plugin-sdk memory blow-up. (#45426) Thanks @TarasShyn.</li>
<li>Ollama/reasoning visibility: stop promoting native <code>thinking</code> and <code>reasoning</code> fields into final assistant text so local reasoning models no longer leak internal thoughts in normal replies. (#45330) Thanks @xi7ang.</li>
<li>Android/onboarding QR scan: switch setup QR scanning to Google Code Scanner so onboarding uses a more reliable scanner instead of the legacy embedded ZXing flow. (#45021) Thanks @obviyus.</li>
<li>Browser/existing-session: harden driver validation and session lifecycle so transport errors trigger reconnects while tool-level errors preserve the session, and extract shared ARIA role sets to deduplicate Playwright and Chrome MCP snapshot paths. (#45682) Thanks @odysseus0.</li>
<li>Browser/existing-session: accept text-only <code>list_pages</code> and <code>new_page</code> responses from Chrome DevTools MCP so live-session tab discovery and new-tab open flows keep working when the server omits structured page metadata.</li>
<li>Control UI/insecure auth: preserve explicit shared token and password auth on plain-HTTP Control UI connects so LAN and reverse-proxy sessions no longer drop shared auth before the first WebSocket handshake. (#45088) Thanks @velvet-shark.</li>
<li>Gateway/session reset: preserve <code>lastAccountId</code> and <code>lastThreadId</code> across gateway session resets so replies keep routing back to the same account and thread after <code>/reset</code>. (#44773) Thanks @Lanfei.</li>
<li>macOS/onboarding: avoid self-restarting freshly bootstrapped launchd gateways and give new daemon installs longer to become healthy, so <code>openclaw onboard --install-daemon</code> no longer false-fails on slower Macs and fresh VM snapshots.</li>
<li>Gateway/status: add <code>openclaw gateway status --require-rpc</code> and clearer Linux non-interactive daemon-install failure reporting so automation can fail hard on probe misses instead of treating a printed RPC error as green.</li>
<li>macOS/exec approvals: respect per-agent exec approval settings in the gateway prompter, including allowlist fallback when the native prompt cannot be shown, so gateway-triggered <code>system.run</code> requests follow configured policy instead of always prompting or denying unexpectedly. (#13707) Thanks @sliekens.</li>
<li>Telegram/media downloads: thread the same direct or proxy transport policy into SSRF-guarded file fetches so inbound attachments keep working when Telegram falls back between env-proxy and direct networking. (#44639) Thanks @obviyus.</li>
<li>Telegram/inbound media IPv4 fallback: retry SSRF-guarded Telegram file downloads once with the same IPv4 fallback policy as Bot API calls so fresh installs on IPv6-broken hosts no longer fail to download inbound images.</li>
<li>Windows/gateway install: bound <code>schtasks</code> calls and fall back to the Startup-folder login item when task creation hangs, so native <code>openclaw gateway install</code> fails fast instead of wedging forever on broken Scheduled Task setups.</li>
<li>Windows/gateway stop: resolve Startup-folder fallback listeners from the installed <code>gateway.cmd</code> port, so <code>openclaw gateway stop</code> now actually kills fallback-launched gateway processes before restart.</li>
<li>Windows/gateway status: reuse the installed service command environment when reading runtime status, so startup-fallback gateways keep reporting the configured port and running state in <code>gateway status --json</code> instead of falling back to <code>gateway port unknown</code>.</li>
<li>Windows/gateway auth: stop attaching device identity on local loopback shared-token and password gateway calls, so native Windows agent replies no longer log stale <code>device signature expired</code> fallback noise before succeeding.</li>
<li>Discord/gateway startup: treat plain-text and transient <code>/gateway/bot</code> metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman.</li>
<li>Slack/probe: keep <code>auth.test()</code> bot and team metadata mapping stable while simplifying the probe result path. (#44775) Thanks @Cafexss.</li>
<li>Dashboard/chat UI: render oversized plain-text replies as normal paragraphs instead of capped gray code blocks, so long desktop chat responses stay readable without tab-switching refreshes.</li>
<li>Dashboard/chat UI: restore the <code>chat-new-messages</code> class on the New messages scroll pill so the button uses its existing compact styling instead of rendering as a full-screen SVG overlay. (#44856) Thanks @Astro-Han.</li>
<li>Gateway/Control UI: restore the operator-only device-auth bypass and classify browser connect failures so origin and device-identity problems no longer show up as auth errors in the Control UI and web chat. (#45512) thanks @sallyom.</li>
<li>macOS/voice wake: stop crashing wake-word command extraction when speech segment ranges come from a different transcript instance.</li>
<li>Discord/allowlists: honor raw <code>guild_id</code> when hydrated guild objects are missing so allowlisted channels and threads like <code>#maintainers</code> no longer get false-dropped before channel allowlist checks.</li>
<li>macOS/runtime locator: require Node >=22.16.0 during macOS runtime discovery so the app no longer accepts Node versions that the main runtime guard rejects later. Thanks @sumleo.</li>
<li>Agents/custom providers: preserve blank API keys for loopback OpenAI-compatible custom providers by clearing the synthetic Authorization header at runtime, while keeping explicit apiKey and oauth/token config from silently downgrading into fake bearer auth. (#45631) Thanks @xinhuagu.</li>
<li>Models/google-vertex Gemini flash-lite normalization: apply existing bare-ID preview normalization to <code>google-vertex</code> model refs and provider configs so <code>google-vertex/gemini-3.1-flash-lite</code> resolves as <code>gemini-3.1-flash-lite-preview</code>. (#42435) thanks @scoootscooob.</li>
<li>iMessage/remote attachments: reject unsafe remote attachment paths before spawning SCP, so sender-controlled filenames can no longer inject shell metacharacters into remote media staging. Thanks @lintsinghua.</li>
<li>Telegram/webhook auth: validate the Telegram webhook secret before reading or parsing request bodies, so unauthenticated requests are rejected immediately instead of consuming up to 1 MB first. Thanks @space08.</li>
<li>Security/device pairing: make bootstrap setup codes single-use so pending device pairing requests cannot be silently replayed and widened to admin before approval. Thanks @tdjackey.</li>
<li>Security/external content: strip zero-width and soft-hyphen marker-splitting characters during boundary sanitization so spoofed <code>EXTERNAL_UNTRUSTED_CONTENT</code> markers fall back to the existing hardening path instead of bypassing marker normalization.</li>
<li>Security/exec approvals: unwrap more <code>pnpm</code> runtime forms during approval binding, including <code>pnpm --reporter ... exec</code> and direct <code>pnpm node</code> file runs, with matching regression coverage and docs updates.</li>
<li>Security/exec approvals: fail closed for Perl <code>-M</code> and <code>-I</code> approval flows so preload and load-path module resolution stays outside approval-backed runtime execution unless the operator uses a broader explicit trust path.</li>
<li>Security/exec approvals: recognize PowerShell <code>-File</code> and <code>-f</code> wrapper forms during inline-command extraction so approval and command-analysis paths treat file-based PowerShell launches like the existing <code>-Command</code> variants.</li>
<li>Security/exec approvals: unwrap <code>env</code> dispatch wrappers inside shell-segment allowlist resolution on macOS so <code>env FOO=bar /path/to/bin</code> resolves against the effective executable instead of the wrapper token.</li>
<li>Security/exec approvals: treat backslash-newline as shell line continuation during macOS shell-chain parsing so line-continued <code>$(</code> substitutions fail closed instead of slipping past command-substitution checks.</li>
<li>Security/exec approvals: bind macOS skill auto-allow trust to both executable name and resolved path so same-basename binaries no longer inherit trust from unrelated skill bins.</li>
<li>Build/plugin-sdk bundling: bundle plugin-sdk subpath entries in one shared build pass so published packages stop duplicating shared chunks and avoid the recent plugin-sdk memory blow-up. (#45426) Thanks @TarasShyn.</li>
<li>Cron/isolated sessions: route nested cron-triggered embedded runner work onto the nested lane so isolated cron jobs no longer deadlock when compaction or other queued inner work runs. Thanks @vincentkoc.</li>
<li>Agents/OpenAI-compatible compat overrides: respect explicit user <code>models[].compat</code> opt-ins for non-native <code>openai-completions</code> endpoints so usage-in-streaming capability overrides no longer get forced off when the endpoint actually supports them. (#44432) Thanks @cheapestinference.</li>
<li>Agents/Azure OpenAI startup prompts: rephrase the built-in <code>/new</code>, <code>/reset</code>, and post-compaction startup instruction so Azure OpenAI deployments no longer hit HTTP 400 false positives from the content filter. (#43403) Thanks @xingsy97.</li>
<li>Agents/memory bootstrap: load only one root memory file, preferring <code>MEMORY.md</code> and using <code>memory.md</code> as a fallback, so case-insensitive Docker mounts no longer inject duplicate memory context. (#26054) Thanks @Lanfei.</li>
<li>Agents/compaction: compare post-compaction token sanity checks against full-session pre-compaction totals and skip the check when token estimation fails, so sessions with large bootstrap context keep real token counts instead of falling back to unknown. (#28347) thanks @efe-arv.</li>
<li>Agents/compaction: preserve safeguard compaction summary language continuity via default and configurable custom instructions so persona drift is reduced after auto-compaction. (#10456) Thanks @keepitmello.</li>
<li>Agents/tool warnings: distinguish gated core tools like <code>apply_patch</code> from plugin-only unknown entries in <code>tools.profile</code> warnings, so unavailable core tools now report current runtime/provider/model/config gating instead of suggesting a missing plugin.</li>
<li>Config/validation: accept documented <code>agents.list[].params</code> per-agent overrides in strict config validation so <code>openclaw config validate</code> no longer rejects runtime-supported <code>cacheRetention</code>, <code>temperature</code>, and <code>maxTokens</code> settings. (#41171) Thanks @atian8179.</li>
<li>Config/web fetch: restore runtime validation for documented <code>tools.web.fetch.readability</code> and <code>tools.web.fetch.firecrawl</code> settings so valid web fetch configs no longer fail with unrecognized-key errors. (#42583) Thanks @stim64045-spec.</li>
<li>Signal/config validation: add <code>channels.signal.groups</code> schema support so per-group <code>requireMention</code>, <code>tools</code>, and <code>toolsBySender</code> overrides no longer get rejected during config validation. (#27199) Thanks @unisone.</li>
<li>Config/discovery: accept <code>discovery.wideArea.domain</code> in strict config validation so unicast DNS-SD gateway configs no longer fail with an unrecognized-key error. (#35615) Thanks @ingyukoh.</li>
<li>Telegram/media errors: redact Telegram file URLs before building media fetch errors so failed inbound downloads do not leak bot tokens into logs. Thanks @space08.</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.3.13/OpenClaw-2026.3.13.zip" length="23640917" type="application/octet-stream" sparkle:edSignature="Me63UHSpFLocTo5Lt7Iqsl0Hq61y3jTcZ9DUkiFl9xQvTE0+ORuqRMFWqPgYwfaKMgcgQmUbrV/uFzEoTIRHBA=="/>
</item>
<item>
<title>2026.3.12</title>
<pubDate>Fri, 13 Mar 2026 04:25:50 +0000</pubDate>
@ -244,5 +168,367 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.8-beta.1/OpenClaw-2026.3.8-beta.1.zip" length="23407015" type="application/octet-stream" sparkle:edSignature="KCqhSmu4b0tHf55RqcQOHorsc55CgBI5BUmK/NTizxNq04INn/7QvsamHYQou9DbB2IW6B2nawBC4nn4au5yDA=="/>
</item>
<item>
<title>2026.3.7</title>
<pubDate>Sun, 08 Mar 2026 04:42:35 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026030790</sparkle:version>
<sparkle:shortVersionString>2026.3.7</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.3.7</h2>
<h3>Changes</h3>
<ul>
<li>Agents/context engine plugin interface: add <code>ContextEngine</code> plugin slot with full lifecycle hooks (<code>bootstrap</code>, <code>ingest</code>, <code>assemble</code>, <code>compact</code>, <code>afterTurn</code>, <code>prepareSubagentSpawn</code>, <code>onSubagentEnded</code>), slot-based registry with config-driven resolution, <code>LegacyContextEngine</code> wrapper preserving existing compaction behavior, scoped subagent runtime for plugin runtimes via <code>AsyncLocalStorage</code>, and <code>sessions.get</code> gateway method. Enables plugins like <code>lossless-claw</code> to provide alternative context management strategies without modifying core compaction logic. Zero behavior change when no context engine plugin is configured. (#22201) thanks @jalehman.</li>
<li>ACP/persistent channel bindings: add durable Discord channel and Telegram topic binding storage, routing resolution, and CLI/docs support so ACP thread targets survive restarts and can be managed consistently. (#34873) Thanks @dutifulbob.</li>
<li>Telegram/ACP topic bindings: accept Telegram Mac Unicode dash option prefixes in <code>/acp spawn</code>, support Telegram topic thread binding (<code>--thread here|auto</code>), route bound-topic follow-ups to ACP sessions, add actionable Telegram approval buttons with prefixed approval-id resolution, and pin successful bind confirmations in-topic. (#36683) Thanks @huntharo.</li>
<li>Telegram/topic agent routing: support per-topic <code>agentId</code> overrides in forum groups and DM topics so topics can route to dedicated agents with isolated sessions. (#33647; based on #31513) Thanks @kesor and @Sid-Qin.</li>
<li>Web UI/i18n: add Spanish (<code>es</code>) locale support in the Control UI, including locale detection, lazy loading, and language picker labels across supported locales. (#35038) Thanks @DaoPromociones.</li>
<li>Onboarding/web search: add provider selection step and full provider list in configure wizard, with SecretRef ref-mode support during onboarding. (#34009) Thanks @kesku and @thewilloftheshadow.</li>
<li>Tools/Web search: switch Perplexity provider to Search API with structured results plus new language/region/time filters. (#33822) Thanks @kesku.</li>
<li>Gateway: add SecretRef support for gateway.auth.token with auth-mode guardrails. (#35094) Thanks @joshavant.</li>
<li>Docker/Podman extension dependency baking: add <code>OPENCLAW_EXTENSIONS</code> so container builds can preinstall selected bundled extension npm dependencies into the image for faster and more reproducible startup in container deployments. (#32223) Thanks @sallyom.</li>
<li>Plugins/before_prompt_build system-context fields: add <code>prependSystemContext</code> and <code>appendSystemContext</code> so static plugin guidance can be placed in system prompt space for provider caching and lower repeated prompt token cost. (#35177) thanks @maweibin.</li>
<li>Plugins/hook policy: add <code>plugins.entries.<id>.hooks.allowPromptInjection</code>, validate unknown typed hook names at runtime, and preserve legacy <code>before_agent_start</code> model/provider overrides while stripping prompt-mutating fields when prompt injection is disabled. (#36567) thanks @gumadeiras.</li>
<li>Hooks/Compaction lifecycle: emit <code>session:compact:before</code> and <code>session:compact:after</code> internal events plus plugin compaction callbacks with session/count metadata, so automations can react to compaction runs consistently. (#16788) thanks @vincentkoc.</li>
<li>Agents/compaction post-context configurability: add <code>agents.defaults.compaction.postCompactionSections</code> so deployments can choose which <code>AGENTS.md</code> sections are re-injected after compaction, while preserving legacy fallback behavior when the documented default pair is configured in any order. (#34556) thanks @efe-arv.</li>
<li>TTS/OpenAI-compatible endpoints: add <code>messages.tts.openai.baseUrl</code> config support with config-over-env precedence, endpoint-aware directive validation, and OpenAI TTS request routing to the resolved base URL. (#34321) thanks @RealKai42.</li>
<li>Slack/DM typing feedback: add <code>channels.slack.typingReaction</code> so Socket Mode DMs can show reaction-based processing status even when Slack native assistant typing is unavailable. (#19816) Thanks @dalefrieswthat.</li>
<li>Discord/allowBots mention gating: add <code>allowBots: "mentions"</code> to only accept bot-authored messages that mention the bot. Thanks @thewilloftheshadow.</li>
<li>Agents/tool-result truncation: preserve important tail diagnostics by using head+tail truncation for oversized tool results while keeping configurable truncation options. (#20076) thanks @jlwestsr.</li>
<li>Cron/job snapshot persistence: skip backup during normalization persistence in <code>ensureLoaded</code> so <code>jobs.json.bak</code> keeps the pre-edit snapshot for recovery, while preserving backup creation on explicit user-driven writes. (#35234) Thanks @0xsline.</li>
<li>CLI: make read-only SecretRef status flows degrade safely (#37023) thanks @joshavant.</li>
<li>Tools/Diffs guidance: restore a short system-prompt hint for enabled diffs while keeping the detailed instructions in the companion skill, so diffs usage guidance stays out of user-prompt space. (#36904) thanks @gumadeiras.</li>
<li>Tools/Diffs guidance loading: move diffs usage guidance from unconditional prompt-hook injection to the plugin companion skill path, reducing unrelated-turn prompt noise while keeping diffs tool behavior unchanged. (#32630) thanks @sircrumpet.</li>
<li>Docs/Web search: remove outdated Brave free-tier wording and replace prescriptive AI ToS guidance with neutral compliance language in Brave setup docs. (#26860) Thanks @HenryLoenwind.</li>
<li>Config/Compaction safeguard tuning: expose <code>agents.defaults.compaction.recentTurnsPreserve</code> and quality-guard retry knobs through the validated config surface and embedded-runner wiring, with regression coverage for real config loading and schema metadata. (#25557) thanks @rodrigouroz.</li>
<li>iOS/App Store Connect release prep: align iOS bundle identifiers under <code>ai.openclaw.client</code>, refresh Watch app icons, add Fastlane metadata/screenshot automation, and support Keychain-backed ASC auth for uploads. (#38936) Thanks @ngutman.</li>
<li>Mattermost/model picker: add Telegram-style interactive provider/model browsing for <code>/oc_model</code> and <code>/oc_models</code>, fix picker callback updates, and emit a normal confirmation reply when a model is selected. (#38767) thanks @mukhtharcm.</li>
<li>Docker/multi-stage build: restructure Dockerfile as a multi-stage build to produce a minimal runtime image without build tools, source code, or Bun; add <code>OPENCLAW_VARIANT=slim</code> build arg for a bookworm-slim variant. (#38479) Thanks @sallyom.</li>
<li>Google/Gemini 3.1 Flash-Lite: add first-class <code>google/gemini-3.1-flash-lite-preview</code> support across model-id normalization, default aliases, media-understanding image lookups, Google Gemini CLI forward-compat fallback, and docs.</li>
</ul>
<h3>Breaking</h3>
<ul>
<li><strong>BREAKING:</strong> Gateway auth now requires explicit <code>gateway.auth.mode</code> when both <code>gateway.auth.token</code> and <code>gateway.auth.password</code> are configured (including SecretRefs). Set <code>gateway.auth.mode</code> to <code>token</code> or <code>password</code> before upgrade to avoid startup/pairing/TUI failures. (#35094) Thanks @joshavant.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Models/MiniMax: stop advertising removed <code>MiniMax-M2.5-Lightning</code> in built-in provider catalogs, onboarding metadata, and docs; keep the supported fast-tier model as <code>MiniMax-M2.5-highspeed</code>.</li>
<li>Security/Config: fail closed when <code>loadConfig()</code> hits validation or read errors so invalid configs cannot silently fall back to permissive runtime defaults. (#9040) Thanks @joetomasone.</li>
<li>Memory/Hybrid search: preserve negative FTS5 BM25 relevance ordering in <code>bm25RankToScore()</code> so stronger keyword matches rank above weaker ones instead of collapsing or reversing scores. (#33757) Thanks @lsdcc01.</li>
<li>LINE/<code>requireMention</code> group gating: align inbound and reply-stage LINE group policy resolution across raw, <code>group:</code>, and <code>room:</code> keys (including account-scoped group config), preserve plugin-backed reply-stage fallback behavior, and add regression coverage for prefixed-only group/room config plus reply-stage policy resolution. (#35847) Thanks @kirisame-wang.</li>
<li>Onboarding/local setup: default unset local <code>tools.profile</code> to <code>coding</code> instead of <code>messaging</code>, restoring file/runtime tools for fresh local installs while preserving explicit user-set profiles. (from #38241, overlap with #34958) Thanks @cgdusek.</li>
<li>Gateway/Telegram stale-socket restart guard: only apply stale-socket restarts to channels that publish event-liveness timestamps, preventing Telegram providers from being misclassified as stale solely due to long uptime and avoiding restart/pairing storms after upgrade. (openclaw#38464)</li>
<li>Onboarding/headless Linux daemon probe hardening: treat <code>systemctl --user is-enabled</code> probe failures as non-fatal during daemon install flow so onboarding no longer crashes on SSH/headless VPS environments before showing install guidance. (#37297) Thanks @acarbajal-web.</li>
<li>Memory/QMD mcporter Windows spawn hardening: when <code>mcporter.cmd</code> launch fails with <code>spawn EINVAL</code>, retry via bare <code>mcporter</code> shell resolution so QMD recall can continue instead of falling back to builtin memory search. (#27402) Thanks @i0ivi0i.</li>
<li>Tools/web_search Brave language-code validation: align <code>search_lang</code> handling with Brave-supported codes (including <code>zh-hans</code>, <code>zh-hant</code>, <code>en-gb</code>, and <code>pt-br</code>), map common alias inputs (<code>zh</code>, <code>ja</code>) to valid Brave values, and reject unsupported codes before upstream requests to prevent 422 failures. (#37260) Thanks @heyanming.</li>
<li>Models/openai-completions streaming compatibility: force <code>compat.supportsUsageInStreaming=false</code> for non-native OpenAI-compatible endpoints during model normalization, preventing usage-only stream chunks from triggering <code>choices[0]</code> parser crashes in provider streams. (#8714) Thanks @nonanon1.</li>
<li>Tools/xAI native web-search collision guard: drop OpenClaw <code>web_search</code> from tool registration when routing to xAI/Grok model providers (including OpenRouter <code>x-ai/*</code>) to avoid duplicate tool-name request failures against provider-native <code>web_search</code>. (#14749) Thanks @realsamrat.</li>
<li>TUI/token copy-safety rendering: treat long credential-like mixed alphanumeric tokens (including quoted forms) as copy-sensitive in render sanitization so formatter hard-wrap guards no longer inject visible spaces into auth-style values before display. (#26710) Thanks @jasonthane.</li>
<li>WhatsApp/self-chat response prefix fallback: stop forcing <code>"[openclaw]"</code> as the implicit outbound response prefix when no identity name or response prefix is configured, so blank/default prefix settings no longer inject branding text unexpectedly in self-chat flows. (#27962) Thanks @ecanmor.</li>
<li>Memory/QMD search result decoding: accept <code>qmd search</code> hits that only include <code>file</code> URIs (for example <code>qmd://collection/path.md</code>) without <code>docid</code>, resolve them through managed collection roots, and keep multi-collection results keyed by file fallback so valid QMD hits no longer collapse to empty <code>memory_search</code> output. (#28181) Thanks @0x76696265.</li>
<li>Memory/QMD collection-name conflict recovery: when <code>qmd collection add</code> fails because another collection already occupies the same <code>path + pattern</code>, detect the conflicting collection from <code>collection list</code>, remove it, and retry add so agent-scoped managed collections are created deterministically instead of being silently skipped; also add warning-only fallback when qmd metadata is unavailable to avoid destructive guesses. (#25496) Thanks @Ramsbaby.</li>
<li>Slack/app_mention race dedupe: when <code>app_mention</code> dispatch wins while same-<code>ts</code> <code>message</code> prepare is still in-flight, suppress the later message dispatch so near-simultaneous Slack deliveries do not produce duplicate replies; keep single-retry behavior and add regression coverage for both dropped and successful message-prepare outcomes. (#37033) Thanks @Takhoffman.</li>
<li>Gateway/chat streaming tool-boundary text retention: merge assistant delta segments into per-run chat buffers so pre-tool text is preserved in live chat deltas/finals when providers emit post-tool assistant segments as non-prefix snapshots. (#36957) Thanks @Datyedyeguy.</li>
<li>TUI/model indicator freshness: prevent stale session snapshots from overwriting freshly patched model selection (and reset per-session freshness when switching session keys) so <code>/model</code> updates reflect immediately instead of lagging by one or more commands. (#21255) Thanks @kowza.</li>
<li>TUI/final-error rendering fallback: when a chat <code>final</code> event has no renderable assistant content but includes envelope <code>errorMessage</code>, render the formatted error text instead of collapsing to <code>"(no output)"</code>, preserving actionable failure context in-session. (#14687) Thanks @Mquarmoc.</li>
<li>TUI/session-key alias event matching: treat chat events whose session keys are canonical aliases (for example <code>agent:<id>:main</code> vs <code>main</code>) as the same session while preserving cross-agent isolation, so assistant replies no longer disappear or surface in another terminal window due to strict key-form mismatch. (#33937) Thanks @yjh1412.</li>
<li>OpenAI Codex OAuth/login parity: keep <code>openclaw models auth login --provider openai-codex</code> on the built-in path even without provider plugins, preserve Pi-generated authorize URLs without local scope rewriting, and stop validating successful Codex sign-ins against the public OpenAI Responses API after callback. (#37558; follow-up to #36660 and #24720) Thanks @driesvints, @Skippy-Gunboat, and @obviyus.</li>
<li>Agents/config schema lookup: add <code>gateway</code> tool action <code>config.schema.lookup</code> so agents can inspect one config path at a time before edits without loading the full schema into prompt context. (#37266) Thanks @gumadeiras.</li>
<li>Onboarding/API key input hardening: strip non-Latin1 Unicode artifacts from normalized secret input (while preserving Latin-1 content and internal spaces) so malformed copied API keys cannot trigger HTTP header <code>ByteString</code> construction crashes; adds regression coverage for shared normalization and MiniMax auth header usage. (#24496) Thanks @fa6maalassaf.</li>
<li>Kimi Coding/Anthropic tools compatibility: normalize <code>anthropic-messages</code> tool payloads to OpenAI-style <code>tools[].function</code> + compatible <code>tool_choice</code> when targeting Kimi Coding endpoints, restoring tool-call workflows that regressed after v2026.3.2. (#37038) Thanks @mochimochimochi-hub.</li>
<li>Heartbeat/workspace-path guardrails: append explicit workspace <code>HEARTBEAT.md</code> path guidance (and <code>docs/heartbeat.md</code> avoidance) to heartbeat prompts so heartbeat runs target workspace checklists reliably across packaged install layouts. (#37037) Thanks @stofancy.</li>
<li>Subagents/kill-complete announce race: when a late <code>subagent-complete</code> lifecycle event arrives after an earlier kill marker, clear stale kill suppression/cleanup flags and re-run announce cleanup so finished runs no longer get silently swallowed. (#37024) Thanks @cmfinlan.</li>
<li>Agents/tool-result cleanup timeout hardening: on embedded runner teardown idle timeouts, clear pending tool-call state without persisting synthetic <code>missing tool result</code> entries, preventing timeout cleanups from poisoning follow-up turns; adds regression coverage for timeout clear-vs-flush behavior. (#37081) Thanks @Coyote-Den.</li>
<li>Agents/openai-completions stream timeout hardening: ensure runtime undici global dispatchers use extended streaming body/header timeouts (including env-proxy dispatcher mode) before embedded runs, reducing forced mid-stream <code>terminated</code> failures on long generations; adds regression coverage for dispatcher selection and idempotent reconfiguration. (#9708) Thanks @scottchguard.</li>
<li>Agents/fallback cooldown probe execution: thread explicit rate-limit cooldown probe intent from model fallback into embedded runner auth-profile selection so same-provider fallback attempts can actually run when all profiles are cooldowned for <code>rate_limit</code> (instead of failing pre-run as <code>No available auth profile</code>), while preserving default cooldown skip behavior and adding regression tests at both fallback and runner layers. (#13623) Thanks @asfura.</li>
<li>Cron/OpenAI Codex OAuth refresh hardening: when <code>openai-codex</code> token refresh fails specifically on account-id extraction, reuse the cached access token instead of failing the run immediately, with regression coverage to keep non-Codex and unrelated refresh failures unchanged. (#36604) Thanks @laulopezreal.</li>
<li>TUI/session isolation for <code>/new</code>: make <code>/new</code> allocate a unique <code>tui-<uuid></code> session key instead of resetting the shared agent session, so multiple TUI clients on the same agent stop receiving each others replies; also sanitize <code>/new</code> and <code>/reset</code> failure text before rendering in-terminal. Landed from contributor PR #39238 by @widingmarcus-cyber. Thanks @widingmarcus-cyber.</li>
<li>Synology Chat/rate-limit env parsing: honor <code>SYNOLOGY_RATE_LIMIT=0</code> as an explicit value while still falling back to the default limit for malformed env values instead of partially parsing them. Landed from contributor PR #39197 by @scoootscooob. Thanks @scoootscooob.</li>
<li>Voice-call/OpenAI Realtime STT config defaults: honor explicit <code>vadThreshold: 0</code> and <code>silenceDurationMs: 0</code> instead of silently replacing them with defaults. Landed from contributor PR #39196 by @scoootscooob. Thanks @scoootscooob.</li>
<li>Voice-call/OpenAI TTS speed config: honor explicit <code>speed: 0</code> instead of silently replacing it with the default speed. Landed from contributor PR #39318 by @ql-wade. Thanks @ql-wade.</li>
<li>launchd/runtime PID parsing: reject <code>pid <= 0</code> from <code>launchctl print</code> so the daemon state parser no longer treats kernel/non-running sentinel values as real process IDs. Landed from contributor PR #39281 by @mvanhorn. Thanks @mvanhorn.</li>
<li>Cron/file permission hardening: enforce owner-only (<code>0600</code>) cron store/backup/run-log files and harden cron store + run-log directories to <code>0700</code>, including pre-existing directories from older installs. (#36078) Thanks @aerelune.</li>
<li>Gateway/remote WS break-glass hostname support: honor <code>OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1</code> for <code>ws://</code> hostname URLs (not only private IP literals) across onboarding validation and runtime gateway connection checks, while still rejecting public IP literals and non-unicast IPv6 endpoints. (#36930) Thanks @manju-rn.</li>
<li>Routing/binding lookup scalability: pre-index route bindings by channel/account and avoid full binding-list rescans on channel-account cache rollover, preventing multi-second <code>resolveAgentRoute</code> stalls in large binding configurations. (#36915) Thanks @songchenghao.</li>
<li>Browser/session cleanup: track browser tabs opened by session-scoped browser tool runs and close tracked tabs during <code>sessions.reset</code>/<code>sessions.delete</code> runtime cleanup, preventing orphaned tabs and unbounded browser memory growth after session teardown. (#36666) Thanks @Harnoor6693.</li>
<li>Plugin/hook install rollback hardening: stage installs under the canonical install base, validate and run dependency installs before publish, and restore updates by rename instead of deleting the target path, reducing partial-replace and symlink-rebind risk during install failures.</li>
<li>Slack/local file upload allowlist parity: propagate <code>mediaLocalRoots</code> through the Slack send action pipeline so workspace-rooted attachments pass <code>assertLocalMediaAllowed</code> checks while non-allowlisted paths remain blocked. (synthesis: #36656; overlap considered from #36516, #36496, #36493, #36484, #32648, #30888) Thanks @2233admin.</li>
<li>Agents/compaction safeguard pre-check: skip embedded compaction before entering the Pi SDK when a session has no real conversation messages, avoiding unnecessary LLM API calls on idle sessions. (#36451) thanks @Sid-Qin.</li>
<li>Config/schema cache key stability: build merged schema cache keys with incremental hashing to avoid large single-string serialization and prevent <code>RangeError: Invalid string length</code> on high-cardinality plugin/channel metadata. (#36603) Thanks @powermaster888.</li>
<li>iMessage/cron completion announces: strip leaked inline reply tags (for example <code>[[reply_to:6100]]</code>) from user-visible completion text so announcement deliveries do not expose threading metadata. (#24600) Thanks @vincentkoc.</li>
<li>Control UI/iMessage duplicate reply routing: keep internal webchat turns on dispatcher delivery (instead of origin-channel reroute) so Control UI chats do not duplicate replies into iMessage, while preserving webchat-provider relayed routing for external surfaces. Fixes #33483. Thanks @alicexmolt.</li>
<li>Sessions/daily reset transcript archival: archive prior transcript files during stale-session scheduled/daily resets by capturing the previous session entry before rollover, preventing orphaned transcript files on disk. (#35493) Thanks @byungsker.</li>
<li>Feishu/group slash command detection: normalize group mention wrappers before command-authorization probing so mention-prefixed commands (for example <code>@Bot/model</code> and <code>@Bot /reset</code>) are recognized as gateway commands instead of being forwarded to the agent. (#35994) Thanks @liuxiaopai-ai.</li>
<li>Control UI/auth token separation: keep the shared gateway token in browser auth validation while reserving cached device tokens for signed device payloads, preventing false <code>device token mismatch</code> disconnects after restart/rotation. Landed from contributor PR #37382 by @FradSer. Thanks @FradSer.</li>
<li>Gateway/browser auth reconnect hardening: stop counting missing token/password submissions as auth rate-limit failures, and stop auto-reconnecting Control UI clients on non-recoverable auth errors so misconfigured browser tabs no longer lock out healthy sessions. Landed from contributor PR #38725 by @ademczuk. Thanks @ademczuk.</li>
<li>Gateway/service token drift repair: stop persisting shared auth tokens into installed gateway service units, flag stale embedded service tokens for reinstall, and treat tokenless service env as canonical so token rotation/reboot flows stay aligned with config/env resolution. Landed from contributor PR #28428 by @l0cka. Thanks @l0cka.</li>
<li>Control UI/agents-page selection: keep the edited agent selected after saving agent config changes and reloading the agents list, so <code>/agents</code> no longer snaps back to the default agent. Landed from contributor PR #39301 by @MumuTW. Thanks @MumuTW.</li>
<li>Gateway/auth follow-up hardening: preserve systemd <code>EnvironmentFile=</code> precedence/source provenance in daemon audits and doctor repairs, block shared-password override flows from piggybacking cached device tokens, and fail closed when config-first gateway SecretRefs cannot resolve. Follow-up to #39241.</li>
<li>Agents/context pruning: guard assistant thinking/text char estimation against malformed blocks (missing <code>thinking</code>/<code>text</code> strings or null entries) so pruning no longer crashes with malformed provider content. (openclaw#35146) thanks @Sid-Qin.</li>
<li>Agents/transcript policy: set <code>preserveSignatures</code> to Anthropic-only handling in <code>resolveTranscriptPolicy</code> so Anthropic thinking signatures are preserved while non-Anthropic providers remain unchanged. (#32813) thanks @Sid-Qin.</li>
<li>Agents/schema cleaning: detect Venice + Grok model IDs as xAI-proxied targets so unsupported JSON Schema keywords are stripped before requests, preventing Venice/Grok <code>Invalid arguments</code> failures. (openclaw#35355) thanks @Sid-Qin.</li>
<li>Skills/native command deduplication: centralize skill command dedupe by canonical <code>skillName</code> in <code>listSkillCommandsForAgents</code> so duplicate suffixed variants (for example <code>_2</code>) are no longer surfaced across interfaces outside Discord. (#27521) thanks @shivama205.</li>
<li>Agents/xAI tool-call argument decoding: decode HTML-entity encoded xAI/Grok tool-call argument values (<code>&amp;</code>, <code>&quot;</code>, <code>&lt;</code>, <code>&gt;</code>, numeric entities) before tool execution so commands with shell operators and quotes no longer fail with parse errors. (#35276) Thanks @Sid-Qin.</li>
<li>Linux/WSL2 daemon install hardening: add regression coverage for WSL environment detection, WSL-specific systemd guidance, and <code>systemctl --user is-enabled</code> failure paths so WSL2/headless onboarding keeps treating bus-unavailable probes as non-fatal while preserving real permission errors. Related: #36495. Thanks @vincentkoc.</li>
<li>Linux/systemd status and degraded-session handling: treat degraded-but-reachable <code>systemctl --user status</code> results as available, preserve early errors for truly unavailable user-bus cases, and report externally managed running services as running instead of <code>not installed</code>. Thanks @vincentkoc.</li>
<li>Agents/thinking-tag promotion hardening: guard <code>promoteThinkingTagsToBlocks</code> against malformed assistant content entries (<code>null</code>/<code>undefined</code>) before <code>block.type</code> reads so malformed provider payloads no longer crash session processing while preserving pass-through behavior. (#35143) thanks @Sid-Qin.</li>
<li>Gateway/Control UI version reporting: align runtime and browser client version metadata to avoid <code>dev</code> placeholders, wait for bootstrap version before first UI websocket connect, and only forward bootstrap <code>serverVersion</code> to same-origin gateway targets to prevent cross-target version leakage. (from #35230, #30928, #33928) Thanks @Sid-Qin, @joelnishanth, and @MoerAI.</li>
<li>Control UI/markdown parser crash fallback: catch <code>marked.parse()</code> failures and fall back to escaped plain-text <code><pre></code> rendering so malformed recursive markdown no longer crashes Control UI session rendering on load. (#36445) Thanks @BinHPdev.</li>
<li>Control UI/markdown fallback regression coverage: add explicit regression assertions for parser-error fallback behavior so malformed markdown no longer risks reintroducing hard-crash rendering paths in future markdown/parser upgrades. (#36445) Thanks @BinHPdev.</li>
<li>Web UI/config form: treat <code>additionalProperties: true</code> object schemas as editable map entries instead of unsupported fields so Accounts-style maps stay editable in form mode. (#35380, supersedes #32072) Thanks @stakeswky and @liuxiaopai-ai.</li>
<li>Feishu/streaming card delivery synthesis: unify snapshot and delta streaming merge semantics, apply overlap-aware final merge, suppress duplicate final text delivery (including text+media final packets), prefer topic-thread <code>message.reply</code> routing when a reply target exists, and tune card print cadence to avoid duplicate incremental rendering. (from #33245, #32896, #33840) Thanks @rexl2018, @kcinzgg, and @aerelune.</li>
<li>Feishu/group mention detection: carry startup-probed bot display names through monitor dispatch so <code>requireMention</code> checks compare against current bot identity instead of stale config names, fixing missed <code>@bot</code> handling in groups while preserving multi-bot false-positive guards. (#36317, #34271) Thanks @liuxiaopai-ai.</li>
<li>Security/dependency audit: patch transitive Hono vulnerabilities by pinning <code>hono</code> to <code>4.12.5</code> and <code>@hono/node-server</code> to <code>1.19.10</code> in production resolution paths. Thanks @shakkernerd.</li>
<li>Security/dependency audit: bump <code>tar</code> to <code>7.5.10</code> (from <code>7.5.9</code>) to address the high-severity hardlink path traversal advisory (<code>GHSA-qffp-2rhf-9h96</code>). Thanks @shakkernerd.</li>
<li>Cron/announce delivery robustness: bypass pending-descendant announce guards for cron completion sends, ensure named-agent announce routes have outbound session entries, and fall back to direct delivery only when an announce send was actually attempted and failed. (from #35185, #32443, #34987) Thanks @Sid-Qin, @scoootscooob, and @bmendonca3.</li>
<li>Cron/announce best-effort fallback: run direct outbound fallback after attempted announce failures even when delivery is configured as best-effort, so Telegram cron sends are not left as attempted-but-undelivered after <code>cron announce delivery failed</code> warnings.</li>
<li>Auto-reply/system events: restore runtime system events to the message timeline (<code>System:</code> lines), preserve think-hint parsing with prepended events, and carry events into deferred followup/collect/steer-backlog prompts to keep cache behavior stable without dropping queued metadata. (#34794) Thanks @anisoptera.</li>
<li>Security/audit account handling: avoid prototype-chain account IDs in audit validation by using own-property checks for <code>accounts</code>. (#34982) Thanks @HOYALIM.</li>
<li>Cron/restart catch-up semantics: replay interrupted recurring jobs and missed immediate cron slots on startup without replaying interrupted one-shot jobs, with guarded missed-slot probing to avoid malformed-schedule startup aborts and duplicate-trigger drift after restart. (from #34466, #34896, #34625, #33206) Thanks @dunamismax, @dsantoreis, @Octane0411, and @Sid-Qin.</li>
<li>Venice/provider onboarding hardening: align per-model Venice completion-token limits with discovery metadata, clamp untrusted discovery values to safe bounds, sync the static Venice fallback catalog with current live model metadata, and disable tool wiring for Venice models that do not support function calling so default Venice setups no longer fail with <code>max_completion_tokens</code> or unsupported-tools 400s. Fixes #38168. Thanks @Sid-Qin, @powermaster888 and @vincentkoc.</li>
<li>Agents/session usage tracking: preserve accumulated usage metadata on embedded Pi runner error exits so failed turns still update session <code>totalTokens</code> from real usage instead of stale prior values. (#34275) thanks @RealKai42.</li>
<li>Slack/reaction thread context routing: carry Slack native DM channel IDs through inbound context and threading tool resolution so reaction targets resolve consistently for DM <code>To=user:*</code> sessions (including <code>toolContext.currentChannelId</code> fallback behavior). (from #34831; overlaps #34440, #34502, #34483, #32754) Thanks @dunamismax.</li>
<li>Subagents/announce completion scoping: scope nested direct-child completion aggregation to the current requester run window, harden frozen completion capture for deterministic descendant synthesis, and route completion announce delivery through parent-agent announce turns with provenance-aware internal events. (#35080) Thanks @tyler6204.</li>
<li>Nodes/system.run approval hardening: use explicit argv-mutation signaling when regenerating prepared <code>rawCommand</code>, and cover the <code>system.run.prepare -> system.run</code> handoff so direct PATH-based <code>nodes.run</code> commands no longer fail with <code>rawCommand does not match command</code>. (#33137) thanks @Sid-Qin.</li>
<li>Models/custom provider headers: propagate <code>models.providers.<name>.headers</code> across inline, fallback, and registry-found model resolution so header-authenticated proxies consistently receive configured request headers. (#27490) thanks @Sid-Qin.</li>
<li>Ollama/remote provider auth fallback: synthesize a local runtime auth key for explicitly configured <code>models.providers.ollama</code> entries that omit <code>apiKey</code>, so remote Ollama endpoints run without requiring manual dummy-key setup while preserving env/profile/config key precedence and missing-config failures. (#11283) Thanks @cpreecs.</li>
<li>Ollama/custom provider headers: forward resolved model headers into native Ollama stream requests so header-authenticated Ollama proxies receive configured request headers. (#24337) thanks @echoVic.</li>
<li>Ollama/compaction and summarization: register custom <code>api: "ollama"</code> handling for compaction, branch-style internal summarization, and TTS text summarization on current <code>main</code>, so native Ollama models no longer fail with <code>No API provider registered for api: ollama</code> outside the main run loop. Thanks @JaviLib.</li>
<li>Daemon/systemd install robustness: treat <code>systemctl --user is-enabled</code> exit-code-4 <code>not-found</code> responses as not-enabled by combining stderr/stdout detail parsing, so Ubuntu fresh installs no longer fail with <code>systemctl is-enabled unavailable</code>. (#33634) Thanks @Yuandiaodiaodiao.</li>
<li>Slack/system-event session routing: resolve reaction/member/pin/interaction system-event session keys through channel/account bindings (with sender-aware DM routing) so inbound Slack events target the correct agent session in multi-account setups instead of defaulting to <code>agent:main</code>. (#34045) Thanks @paulomcg, @daht-mad and @vincentkoc.</li>
<li>Slack/native streaming markdown conversion: stop pre-normalizing text passed to Slack native <code>markdown_text</code> in streaming start/append/stop paths to prevent Markdown style corruption from double conversion. (#34931)</li>
<li>Gateway/HTTP tools invoke media compatibility: preserve raw media payload access for direct <code>/tools/invoke</code> clients by allowing media <code>nodes</code> invoke commands only in HTTP tool context, while keeping agent-context media invoke blocking to prevent base64 prompt bloat. (#34365) Thanks @obviyus.</li>
<li>Security/archive ZIP hardening: extract ZIP entries via same-directory temp files plus atomic rename, then re-open and reject post-rename hardlink alias races outside the destination root.</li>
<li>Agents/Nodes media outputs: add dedicated <code>photos_latest</code> action handling, block media-returning <code>nodes invoke</code> commands, keep metadata-only <code>camera.list</code> invoke allowed, and normalize empty <code>photos_latest</code> results to a consistent response shape to prevent base64 context bloat. (#34332) Thanks @obviyus.</li>
<li>TUI/session-key canonicalization: normalize <code>openclaw tui --session</code> values to lowercase so uppercase session names no longer drop real-time streaming updates due to gateway/TUI key mismatches. (#33866, #34013) thanks @lynnzc.</li>
<li>iMessage/echo loop hardening: strip leaked assistant-internal scaffolding from outbound iMessage replies, drop reflected assistant-content messages before they re-enter inbound processing, extend echo-cache text retention for delayed reflections, and suppress repeated loop traffic before it amplifies into queue overflow. (#33295) Thanks @joelnishanth.</li>
<li>Skills/workspace boundary hardening: reject workspace and extra-dir skill roots or <code>SKILL.md</code> files whose realpath escapes the configured source root, and skip syncing those escaped skills into sandbox workspaces.</li>
<li>Outbound/send config threading: pass resolved SecretRef config through outbound adapters and helper send paths so send flows do not reload unresolved runtime config. (#33987) Thanks @joshavant.</li>
<li>gateway: harden shared auth resolution across systemd, discord, and node host (#39241) Thanks @joshavant.</li>
<li>Secrets/models.json persistence hardening: keep SecretRef-managed api keys + headers from persisting in generated models.json, expand audit/apply coverage, and harden marker handling/serialization. (#38955) Thanks @joshavant.</li>
<li>Sessions/subagent attachments: remove <code>attachments[].content.maxLength</code> from <code>sessions_spawn</code> schema to avoid llama.cpp GBNF repetition overflow, and preflight UTF-8 byte size before buffer allocation while keeping runtime file-size enforcement unchanged. (#33648) Thanks @anisoptera.</li>
<li>Runtime/tool-state stability: recover from dangling Anthropic <code>tool_use</code> after compaction, serialize long-running Discord handler runs without blocking new inbound events, and prevent stale busy snapshots from suppressing stuck-channel recovery. (from #33630, #33583) Thanks @kevinWangSheng and @theotarr.</li>
<li>ACP/Discord startup hardening: clean up stuck ACP worker children on gateway restart, unbind stale ACP thread bindings during Discord startup reconciliation, and add per-thread listener watchdog timeouts so wedged turns cannot block later messages. (#33699) Thanks @dutifulbob.</li>
<li>Extensions/media local-root propagation: consistently forward <code>mediaLocalRoots</code> through extension <code>sendMedia</code> adapters (Google Chat, Slack, iMessage, Signal, WhatsApp), preserving non-local media behavior while restoring local attachment resolution from configured roots. Synthesis of #33581, #33545, #33540, #33536, #33528. Thanks @bmendonca3.</li>
<li>Gateway/plugin HTTP auth hardening: require gateway auth when any overlapping matched route needs it, block mixed-auth fallthrough at dispatch, and reject mixed-auth exact/prefix route overlaps during plugin registration.</li>
<li>Feishu/video media send contract: keep mp4-like outbound payloads on <code>msg_type: "media"</code> (including reply and reply-in-thread paths) so videos render as media instead of degrading to file-link behavior, while preserving existing non-video file subtype handling. (from #33720, #33808, #33678) Thanks @polooooo, @dingjianrui, and @kevinWangSheng.</li>
<li>Gateway/security default response headers: add <code>Permissions-Policy: camera=(), microphone=(), geolocation=()</code> to baseline gateway HTTP security headers for all responses. (#30186) thanks @habakan.</li>
<li>Plugins/startup loading: lazily initialize plugin runtime, split startup-critical plugin SDK imports into <code>openclaw/plugin-sdk/core</code> and <code>openclaw/plugin-sdk/telegram</code>, and preserve <code>api.runtime</code> reflection semantics for plugin compatibility. (#28620) thanks @hmemcpy.</li>
<li>Plugins/startup performance: reduce bursty plugin discovery/manifest overhead with short in-process caches, skip importing bundled memory plugins that are disabled by slot selection, and speed legacy root <code>openclaw/plugin-sdk</code> compatibility via runtime root-alias routing while preserving backward compatibility. Thanks @gumadeiras.</li>
<li>Build/lazy runtime boundaries: replace ineffective dynamic import sites with dedicated lazy runtime boundaries across Slack slash handling, Telegram audit, CLI send deps, memory fallback, and outbound delivery paths while preserving behavior. (#33690) thanks @gumadeiras.</li>
<li>Gateway/password CLI hardening: add <code>openclaw gateway run --password-file</code>, warn when inline <code>--password</code> is used because it can leak via process listings, and document env/file-backed password input as the preferred startup path. Fixes #27948. Thanks @vibewrk and @vincentkoc.</li>
<li>Config/heartbeat legacy-path handling: auto-migrate top-level <code>heartbeat</code> into <code>agents.defaults.heartbeat</code> (with merge semantics that preserve explicit defaults), and keep startup failures on non-migratable legacy entries in the detailed invalid-config path instead of generic migration-failed errors. (#32706) thanks @xiwan.</li>
<li>Plugins/SDK subpath parity: expand plugin SDK subpaths across bundled channels/extensions (Discord, Slack, Signal, iMessage, WhatsApp, LINE, and bundled companion plugins), with build/export/type/runtime wiring so scoped imports resolve consistently in source and dist while preserving compatibility. (#33737) thanks @gumadeiras.</li>
<li>Google/Gemini Flash model selection: switch built-in <code>gemini-flash</code> defaults and docs/examples from the nonexistent <code>google/gemini-3.1-flash-preview</code> ID to the working <code>google/gemini-3-flash-preview</code>, while normalizing legacy OpenClaw config that still uses the old Flash 3.1 alias.</li>
<li>Plugins/bundled scoped-import migration: migrate bundled plugins from monolithic <code>openclaw/plugin-sdk</code> imports to scoped subpaths (or <code>openclaw/plugin-sdk/core</code>) across registration and startup-sensitive runtime files, add CI/release guardrails to prevent regressions, and keep root <code>openclaw/plugin-sdk</code> support for external/community plugins. Thanks @gumadeiras.</li>
<li>Routing/session duplicate suppression synthesis: align shared session delivery-context inheritance, channel-paired route-field merges, and reply-surface target matching so dmScope=main turns avoid cross-surface duplicate replies while thread-aware forwarding keeps intended routing semantics. (from #33629, #26889, #17337, #33250) Thanks @Yuandiaodiaodiao, @kevinwildenradt, @Glucksberg, and @bmendonca3.</li>
<li>Routing/legacy session route inheritance: preserve external route metadata inheritance for legacy channel session keys (<code>agent:<agent>:<channel>:<peer></code> and <code>...:thread:<id></code>) so <code>chat.send</code> does not incorrectly fall back to webchat when valid delivery context exists. Follow-up to #33786.</li>
<li>Routing/legacy route guard tightening: require legacy session-key channel hints to match the saved delivery channel before inheriting external routing metadata, preventing custom namespaced keys like <code>agent:<agent>:work:<ticket></code> from inheriting stale non-webchat routes.</li>
<li>Gateway/internal client routing continuity: prevent webchat/TUI/UI turns from inheriting stale external reply routes by requiring explicit <code>deliver: true</code> for external delivery, keeping main-session external inheritance scoped to non-Webchat/UI clients, and honoring configured <code>session.mainKey</code> when identifying main-session continuity. (from #35321, #34635, #35356) Thanks @alexyyyander and @Octane0411.</li>
<li>Security/auth labels: remove token and API-key snippets from user-facing auth status labels so <code>/status</code> and <code>/models</code> do not expose credential fragments. (#33262) thanks @cu1ch3n.</li>
<li>Models/MiniMax portal vision routing: add <code>MiniMax-VL-01</code> to the <code>minimax-portal</code> provider, route portal image understanding through the MiniMax VLM endpoint, and align media auto-selection plus Telegram sticker description with the shared portal image provider path. (#33953) Thanks @tars90percent.</li>
<li>Auth/credential semantics: align profile eligibility + probe diagnostics with SecretRef/expiry rules and harden browser download atomic writes. (#33733) thanks @joshavant.</li>
<li>Security/audit denyCommands guidance: suggest likely exact node command IDs for unknown <code>gateway.nodes.denyCommands</code> entries so ineffective denylist entries are easier to correct. (#29713) thanks @liquidhorizon88-bot.</li>
<li>Agents/overload failover handling: classify overloaded provider failures separately from rate limits/status timeouts, add short overload backoff before retry/failover, record overloaded prompt/assistant failures as transient auth-profile cooldowns (with probeable same-provider fallback) instead of treating them like persistent auth/billing failures, and keep one-shot cron retry classification aligned so overloaded fallback summaries still count as transient retries.</li>
<li>Docs/security hardening guidance: document Docker <code>DOCKER-USER</code> + UFW policy and add cross-linking from Docker install docs for VPS/public-host setups. (#27613) thanks @dorukardahan.</li>
<li>Docs/security threat-model links: replace relative <code>.md</code> links with Mintlify-compatible root-relative routes in security docs to prevent broken internal navigation. (#27698) thanks @clawdoo.</li>
<li>Plugins/Update integrity drift: avoid false integrity drift prompts when updating npm-installed plugins from unpinned specs, while keeping drift checks for exact pinned versions. (#37179) Thanks @vincentkoc.</li>
<li>iOS/Voice timing safety: guard system speech start/finish callbacks to the active utterance to avoid misattributed start events during rapid stop/restart cycles. (#33304) thanks @mbelinky; original implementation direction by @ngutman.</li>
<li>Gateway/chat.send command scopes: require <code>operator.admin</code> for persistent <code>/config set|unset</code> writes routed through gateway chat clients while keeping <code>/config show</code> available to normal write-scoped operator clients, preserving messaging-channel config command behavior without widening RPC write scope into admin config mutation. Thanks @tdjackey for reporting.</li>
<li>iOS/Talk incremental speech pacing: allow long punctuation-free assistant chunks to start speaking at safe whitespace boundaries so voice responses begin sooner instead of waiting for terminal punctuation. (#33305) thanks @mbelinky; original implementation by @ngutman.</li>
<li>iOS/Watch reply reliability: make watch session activation waiters robust under concurrent requests so status/send calls no longer hang intermittently, and align delegate callbacks with Swift 6 actor safety. (#33306) thanks @mbelinky; original implementation by @Rocuts.</li>
<li>Docs/tool-loop detection config keys: align <code>docs/tools/loop-detection.md</code> examples and field names with the current <code>tools.loopDetection</code> schema to prevent copy-paste validation failures from outdated keys. (#33182) Thanks @Mylszd.</li>
<li>Gateway/session agent discovery: include disk-scanned agent IDs in <code>listConfiguredAgentIds</code> even when <code>agents.list</code> is configured, so disk-only/ACP agent sessions remain visible in gateway session aggregation and listings. (#32831) thanks @Sid-Qin.</li>
<li>Discord/inbound debouncer: skip bot-own MESSAGE_CREATE events before they reach the debounce queue to avoid self-triggered slowdowns in busy servers. Thanks @thewilloftheshadow.</li>
<li>Discord/Agent-scoped media roots: pass <code>mediaLocalRoots</code> through Discord monitor reply delivery (message + component interaction paths) so local media attachments honor per-agent workspace roots instead of falling back to default global roots. Thanks @thewilloftheshadow.</li>
<li>Discord/slash command handling: intercept text-based slash commands in channels, register plugin commands as native, and send fallback acknowledgments for empty slash runs so interactions do not hang. Thanks @thewilloftheshadow.</li>
<li>Discord/thread session lifecycle: reset thread-scoped sessions when a thread is archived so reopening a thread starts fresh without deleting transcript history. Thanks @thewilloftheshadow.</li>
<li>Discord/presence defaults: send an online presence update on ready when no custom presence is configured so bots no longer appear offline by default. Thanks @thewilloftheshadow.</li>
<li>Discord/typing cleanup: stop typing indicators after silent/NO_REPLY runs by marking the run complete before dispatch idle cleanup. Thanks @thewilloftheshadow.</li>
<li>ACP/sandbox spawn parity: block <code>/acp spawn</code> from sandboxed requester sessions with the same host-runtime guard already enforced for <code>sessions_spawn({ runtime: "acp" })</code>, preserving non-sandbox ACP flows while closing the command-path policy gap. Thanks @patte.</li>
<li>Discord/config SecretRef typing: align Discord account token config typing with SecretInput so SecretRef tokens typecheck. (#32490) Thanks @scoootscooob.</li>
<li>Discord/voice messages: request upload slots with JSON fetch calls so voice message uploads no longer fail with content-type errors. Thanks @thewilloftheshadow.</li>
<li>Discord/voice decoder fallback: drop the native Opus dependency and use opusscript for voice decoding to avoid native-opus installs. Thanks @thewilloftheshadow.</li>
<li>Discord/auto presence health signal: add runtime availability-driven presence updates plus connected-state reporting to improve health monitoring and operator visibility. (#33277) Thanks @thewilloftheshadow.</li>
<li>HEIC image inputs: accept HEIC/HEIF <code>input_image</code> sources in Gateway HTTP APIs, normalize them to JPEG before provider delivery, and document the expanded default MIME allowlist. Thanks @vincentkoc.</li>
<li>Gateway/HEIC input follow-up: keep non-HEIC <code>input_image</code> MIME handling unchanged, make HEIC tests hermetic, and enforce chat-completions <code>maxTotalImageBytes</code> against post-normalization image payload size. Thanks @vincentkoc.</li>
<li>Telegram/draft-stream boundary stability: materialize DM draft previews at assistant-message/tool boundaries, serialize lane-boundary callbacks before final delivery, and scope preview cleanup to the active preview so multi-step Telegram streams no longer lose, overwrite, or leave stale preview bubbles. (#33842) Thanks @ngutman.</li>
<li>Telegram/DM draft finalization reliability: require verified final-text draft emission before treating preview finalization as delivered, and fall back to normal payload send when final draft delivery is not confirmed (preventing missing final responses and preserving media/button delivery). (#32118) Thanks @OpenCils.</li>
<li>Telegram/DM draft final delivery: materialize text-only <code>sendMessageDraft</code> previews into one permanent final message and skip duplicate final payload sends, while preserving fallback behavior when materialization fails. (#34318) Thanks @Brotherinlaw-13.</li>
<li>Telegram/DM draft duplicate display: clear stale DM draft previews after materializing the real final message, including threadless fallback when DM topic lookup fails, so partial streaming no longer briefly shows duplicate replies. (#36746) Thanks @joelnishanth.</li>
<li>Telegram/draft preview boundary + silent-token reliability: stabilize answer-lane message boundaries across late-partial/message-start races, preserve/reset finalized preview state at the correct boundaries, and suppress <code>NO_REPLY</code> lead-fragment leaks without broad heartbeat-prefix false positives. (#33169) Thanks @obviyus.</li>
<li>Telegram/native commands <code>commands.allowFrom</code> precedence: make native Telegram commands honor <code>commands.allowFrom</code> as the command-specific authorization source, including group chats, instead of falling back to channel sender allowlists. (#28216) Thanks @toolsbybuddy and @vincentkoc.</li>
<li>Telegram/<code>groupAllowFrom</code> sender-ID validation: restore sender-only runtime validation so negative chat/group IDs remain invalid entries instead of appearing accepted while still being unable to authorize group access. (#37134) Thanks @qiuyuemartin-max and @vincentkoc.</li>
<li>Telegram/native group command auth: authorize native commands in groups and forum topics against <code>groupAllowFrom</code> and per-group/topic sender overrides, while keeping auth rejection replies in the originating topic thread. (#39267) Thanks @edwluo.</li>
<li>Telegram/named-account DMs: restore non-default-account DM routing when a named Telegram account falls back to the default agent by keeping groups fail-closed but deriving a per-account session key for DMs, including identity-link canonicalization and regression coverage for account isolation. (from #32426; fixes #32351) Thanks @chengzhichao-xydt.</li>
<li>Discord/audit wildcard warnings: ignore "\*" wildcard keys when counting unresolved guild channels so doctor/status no longer warns on allow-all configs. (#33125) Thanks @thewilloftheshadow.</li>
<li>Discord/channel resolution: default bare numeric recipients to channels, harden allowlist numeric ID handling with safe fallbacks, and avoid inbound WS heartbeat stalls. (#33142) Thanks @thewilloftheshadow.</li>
<li>Discord/chunk delivery reliability: preserve chunk ordering when using a REST client and retry chunk sends on 429/5xx using account retry settings. (#33226) Thanks @thewilloftheshadow.</li>
<li>Discord/mention handling: add id-based mention formatting + cached rewrites, resolve inbound mentions to display names, and add optional ignoreOtherMentions gating (excluding @everyone/@here). (#33224) Thanks @thewilloftheshadow.</li>
<li>Discord/media SSRF allowlist: allow Discord CDN hostnames (including wildcard domains) in inbound media SSRF policy to prevent proxy/VPN fake-ip blocks. (#33275) Thanks @thewilloftheshadow.</li>
<li>Telegram/device pairing notifications: auto-arm one-shot notify on <code>/pair qr</code>, auto-ping on new pairing requests, and add manual fallback via <code>/pair approve latest</code> if the ping does not arrive. (#33299) thanks @mbelinky.</li>
<li>Exec heartbeat routing: scope exec-triggered heartbeat wakes to agent session keys so unrelated agents are no longer awakened by exec events, while preserving legacy unscoped behavior for non-canonical session keys. (#32724) thanks @altaywtf</li>
<li>macOS/Tailscale remote gateway discovery: add a Tailscale Serve fallback peer probe path (<code>wss://<peer>.ts.net</code>) when Bonjour and wide-area DNS-SD discovery return no gateways, and refresh both discovery paths from macOS onboarding. (#32860) Thanks @ngutman.</li>
<li>iOS/Gateway keychain hardening: move gateway metadata and TLS fingerprints to device keychain storage with safer migration behavior and rollback-safe writes to reduce credential loss risk during upgrades. (#33029) thanks @mbelinky.</li>
<li>iOS/Concurrency stability: replace risky shared-state access in camera and gateway connection paths with lock-protected access patterns to reduce crash risk under load. (#33241) thanks @mbelinky.</li>
<li>iOS/Security guardrails: limit production API-key sourcing to app config and make deep-link confirmation prompts safer by coalescing queued requests instead of silently dropping them. (#33031) thanks @mbelinky.</li>
<li>iOS/TTS playback fallback: keep voice playback resilient by switching from PCM to MP3 when provider format support is unavailable, while avoiding sticky fallback on generic local playback errors. (#33032) thanks @mbelinky.</li>
<li>Plugin outbound/text-only adapter compatibility: allow direct-delivery channel plugins that only implement <code>sendText</code> (without <code>sendMedia</code>) to remain outbound-capable, gracefully fall back to text delivery for media payloads when <code>sendMedia</code> is absent, and fail explicitly for media-only payloads with no text fallback. (#32788) thanks @liuxiaopai-ai.</li>
<li>Telegram/multi-account default routing clarity: warn only for ambiguous (2+) account setups without an explicit default, add <code>openclaw doctor</code> warnings for missing/invalid multi-account defaults across channels, and document explicit-default guidance for channel routing and Telegram config. (#32544) thanks @Sid-Qin.</li>
<li>Telegram/plugin outbound hook parity: run <code>message_sending</code> + <code>message_sent</code> in Telegram reply delivery, include reply-path hook metadata (<code>mediaUrls</code>, <code>threadId</code>), and report <code>message_sent.success=false</code> when hooks blank text and no outbound message is delivered. (#32649) Thanks @KimGLee.</li>
<li>CLI/Coding-agent reliability: switch default <code>claude-cli</code> non-interactive args to <code>--permission-mode bypassPermissions</code>, auto-normalize legacy <code>--dangerously-skip-permissions</code> backend overrides to the modern permission-mode form, align coding-agent + live-test docs with the non-PTY Claude path, and emit session system-event heartbeat notices when CLI watchdog no-output timeouts terminate runs. (#28610, #31149, #34055). Thanks @niceysam, @cryptomaltese and @vincentkoc.</li>
<li>Gateway/OpenAI chat completions: parse active-turn <code>image_url</code> content parts (including parameterized data URIs and guarded URL sources), forward them as multimodal <code>images</code>, accept image-only user turns, enforce per-request image-part/byte budgets, default URL-based image fetches to disabled unless explicitly enabled by config, and redact image base64 data in cache-trace/provider payload diagnostics. (#17685) Thanks @vincentkoc</li>
<li>ACP/ACPX session bootstrap: retry with <code>sessions new</code> when <code>sessions ensure</code> returns no session identifiers so ACP spawns avoid <code>NO_SESSION</code>/<code>ACP_TURN_FAILED</code> failures on affected agents. (#28786, #31338, #34055). Thanks @Sid-Qin and @vincentkoc.</li>
<li>ACP/sessions_spawn parent stream visibility: add <code>streamTo: "parent"</code> for <code>runtime: "acp"</code> to forward initial child-run progress/no-output/completion updates back into the requester session as system events (instead of direct child delivery), and emit a tail-able session-scoped relay log (<code><sessionId>.acp-stream.jsonl</code>, returned as <code>streamLogPath</code> when available), improving orchestrator visibility for blocked or long-running harness turns. (#34310, #29909; reopened from #34055). Thanks @vincentkoc.</li>
<li>Agents/bootstrap truncation warning handling: unify bootstrap budget/truncation analysis across embedded + CLI runtime, <code>/context</code>, and <code>openclaw doctor</code>; add <code>agents.defaults.bootstrapPromptTruncationWarning</code> (<code>off|once|always</code>, default <code>once</code>) and persist warning-signature metadata so truncation warnings are consistent and deduped across turns. (#32769) Thanks @gumadeiras.</li>
<li>Agents/Skills runtime loading: propagate run config into embedded attempt and compaction skill-entry loading so explicitly enabled bundled companion skills are discovered consistently when skill snapshots do not already provide resolved entries. Thanks @gumadeiras.</li>
<li>Agents/Session startup date grounding: substitute <code>YYYY-MM-DD</code> placeholders in startup/post-compaction AGENTS context and append runtime current-time lines for <code>/new</code> and <code>/reset</code> prompts so daily-memory references resolve correctly. (#32381) Thanks @chengzhichao-xydt.</li>
<li>Agents/Compaction template heading alignment: update AGENTS template section names to <code>Session Startup</code>/<code>Red Lines</code> and keep legacy <code>Every Session</code>/<code>Safety</code> fallback extraction so post-compaction context remains intact across template versions. (#25098) thanks @echoVic.</li>
<li>Agents/Compaction continuity: expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context. (#8903) thanks @joetomasone.</li>
<li>Agents/Compaction safeguard structure hardening: require exact fallback summary headings, sanitize untrusted compaction instruction text before prompt embedding, and keep structured sections when preserving all turns. (#25555) thanks @rodrigouroz.</li>
<li>Gateway/status self version reporting: make Gateway self version in <code>openclaw status</code> prefer runtime <code>VERSION</code> (while preserving explicit <code>OPENCLAW_VERSION</code> override), preventing stale post-upgrade app version output. (#32655) thanks @liuxiaopai-ai.</li>
<li>Memory/QMD index isolation: set <code>QMD_CONFIG_DIR</code> alongside <code>XDG_CONFIG_HOME</code> so QMD config state stays per-agent despite upstream XDG handling bugs, preventing cross-agent collection indexing and excess disk/CPU usage. (#27028) thanks @HenryLoenwind.</li>
<li>Memory/QMD collection safety: stop destructive collection rebinds when QMD <code>collection list</code> only reports names without path metadata, preventing <code>memory search</code> from dropping existing collections if re-add fails. (#36870) Thanks @Adnannnnnnna.</li>
<li>Memory/QMD duplicate-document recovery: detect <code>UNIQUE constraint failed: documents.collection, documents.path</code> update failures, rebuild managed collections once, and retry update so periodic QMD syncs recover instead of failing every run; includes regression coverage to avoid over-matching unrelated unique constraints. (#27649) Thanks @MiscMich.</li>
<li>Memory/local embedding initialization hardening: add regression coverage for transient initialization retry and mixed <code>embedQuery</code> + <code>embedBatch</code> concurrent startup to lock single-flight initialization behavior. (#15639) thanks @SubtleSpark.</li>
<li>CLI/Coding-agent reliability: switch default <code>claude-cli</code> non-interactive args to <code>--permission-mode bypassPermissions</code>, auto-normalize legacy <code>--dangerously-skip-permissions</code> backend overrides to the modern permission-mode form, align coding-agent + live-test docs with the non-PTY Claude path, and emit session system-event heartbeat notices when CLI watchdog no-output timeouts terminate runs. Related to #28261. Landed from contributor PRs #28610 and #31149. Thanks @niceysam, @cryptomaltese and @vincentkoc.</li>
<li>ACP/ACPX session bootstrap: retry with <code>sessions new</code> when <code>sessions ensure</code> returns no session identifiers so ACP spawns avoid <code>NO_SESSION</code>/<code>ACP_TURN_FAILED</code> failures on affected agents. Related to #28786. Landed from contributor PR #31338. Thanks @Sid-Qin and @vincentkoc.</li>
<li>LINE/auth boundary hardening synthesis: enforce strict LINE webhook authn/z boundary semantics across pairing-store account scoping, DM/group allowlist separation, fail-closed webhook auth/runtime behavior, and replay/duplication controls (including in-flight replay reservation and post-success dedupe marking). (from #26701, #26683, #25978, #17593, #16619, #31990, #26047, #30584, #18777) Thanks @bmendonca3, @davidahmann, @harshang03, @haosenwang1018, @liuxiaopai-ai, @coygeek, and @Takhoffman.</li>
<li>LINE/media download synthesis: fix file-media download handling and M4A audio classification across overlapping LINE regressions. (from #26386, #27761, #27787, #29509, #29755, #29776, #29785, #32240) Thanks @kevinWangSheng, @loiie45e, @carrotRakko, @Sid-Qin, @codeafridi, and @bmendonca3.</li>
<li>LINE/context and routing synthesis: fix group/room peer routing and command-authorization context propagation, and keep processing later events in mixed-success webhook batches. (from #21955, #24475, #27035, #28286) Thanks @lailoo, @mcaxtr, @jervyclaw, @Glucksberg, and @Takhoffman.</li>
<li>LINE/status/config/webhook synthesis: fix status false positives from snapshot/config state and accept LINE webhook HEAD probes for compatibility. (from #10487, #25726, #27537, #27908, #31387) Thanks @BlueBirdBack, @stakeswky, @loiie45e, @puritysb, and @mcaxtr.</li>
<li>LINE cleanup/test follow-ups: fold cleanup/test learnings into the synthesis review path while keeping runtime changes focused on regression fixes. (from #17630, #17289) Thanks @Clawborn and @davidahmann.</li>
<li>Mattermost/interactive buttons: add interactive button send/callback support with directory-based channel/user target resolution, and harden callbacks via account-scoped HMAC verification plus sender-scoped DM routing. (#19957) thanks @tonydehnke.</li>
<li>Feishu/groupPolicy legacy alias compatibility: treat legacy <code>groupPolicy: "allowall"</code> as <code>open</code> in both schema parsing and runtime policy checks so intended open-group configs no longer silently drop group messages when <code>groupAllowFrom</code> is empty. (from #36358) Thanks @Sid-Qin.</li>
<li>Mattermost/plugin SDK import policy: replace remaining monolithic <code>openclaw/plugin-sdk</code> imports in Mattermost mention-gating paths/tests with scoped subpaths (<code>openclaw/plugin-sdk/compat</code> and <code>openclaw/plugin-sdk/mattermost</code>) so <code>pnpm check</code> passes <code>lint:plugins:no-monolithic-plugin-sdk-entry-imports</code> on baseline. (#36480) Thanks @Takhoffman.</li>
<li>Telegram/polls: add Telegram poll action support to channel action discovery and tool/CLI poll flows, with multi-account discoverability gated to accounts that can actually execute polls (<code>sendMessage</code> + <code>poll</code>). (#36547) thanks @gumadeiras.</li>
<li>Agents/failover cooldown classification: stop treating generic <code>cooling down</code> text as provider <code>rate_limit</code> so healthy models no longer show false global cooldown/rate-limit warnings while explicit <code>model_cooldown</code> markers still trigger failover. (#32972) thanks @stakeswky.</li>
<li>Agents/failover service-unavailable handling: stop treating bare proxy/CDN <code>service unavailable</code> errors as provider overload while keeping them retryable via the timeout/failover path, so transient outages no longer show false rate-limit warnings or block fallback. (#36646) thanks @jnMetaCode.</li>
<li>Plugins/HTTP route migration diagnostics: rewrite legacy <code>api.registerHttpHandler(...)</code> loader failures into actionable migration guidance so doctor/plugin diagnostics point operators to <code>api.registerHttpRoute(...)</code> or <code>registerPluginHttpRoute(...)</code>. (#36794) Thanks @vincentkoc</li>
<li>Doctor/Heartbeat upgrade diagnostics: warn when heartbeat delivery is configured with an implicit <code>directPolicy</code> so upgrades pin direct/DM behavior explicitly instead of relying on the current default. (#36789) Thanks @vincentkoc.</li>
<li>Agents/current-time UTC anchor: append a machine-readable UTC suffix alongside local <code>Current time:</code> lines in shared cron-style prompt contexts so agents can compare UTC-stamped workspace timestamps without doing timezone math. (#32423) thanks @jriff.</li>
<li>Ollama/local model handling: preserve explicit lower <code>contextWindow</code> / <code>maxTokens</code> overrides during merge refresh, and keep native Ollama streamed replies from surfacing fallback <code>thinking</code> / <code>reasoning</code> text once real content starts streaming. (#39292) Thanks @vincentkoc.</li>
<li>TUI/webchat command-owner scope alignment: treat internal-channel gateway sessions with <code>operator.admin</code> as owner-authorized in command auth, restoring cron/gateway/connector tool access for affected TUI/webchat sessions while keeping external channels on identity-based owner checks. (from #35666, #35673, #35704) Thanks @Naylenv, @Octane0411, and @Sid-Qin.</li>
<li>Discord/inbound timeout isolation: separate inbound worker timeout tracking from listener timeout budgets so queued Discord replies are no longer dropped when listener watchdog windows expire mid-run. (#36602) Thanks @dutifulbob.</li>
<li>Memory/doctor SecretRef handling: treat SecretRef-backed memory-search API keys as configured, and fail embedding setup with explicit unresolved-secret errors instead of crashing. (#36835) Thanks @joshavant.</li>
<li>Memory/flush default prompt: ban timestamped variant filenames during default memory flush runs so durable notes stay in the canonical daily <code>memory/YYYY-MM-DD.md</code> file. (#34951) thanks @zerone0x.</li>
<li>Agents/reply delivery timing: flush embedded Pi block replies before waiting on compaction retries so already-generated assistant replies reach channels before compaction wait completes. (#35489) thanks @Sid-Qin.</li>
<li>Agents/gateway config guidance: stop exposing <code>config.schema</code> through the agent <code>gateway</code> tool, remove prompt/docs guidance that told agents to call it, and keep agents on <code>config.get</code> plus <code>config.patch</code>/<code>config.apply</code> for config changes. (#7382) thanks @kakuteki.</li>
<li>Provider/KiloCode: Keep duplicate models after malformed discovery rows, and strip legacy <code>reasoning_effort</code> when proxy reasoning injection is skipped. (#32352) Thanks @pandemicsyn and @vincentkoc.</li>
<li>Agents/failover: classify periodic provider limit exhaustion text (for example <code>Weekly/Monthly Limit Exhausted</code>) as <code>rate_limit</code> while keeping explicit <code>402 Payment Required</code> variants in billing, so failover continues without misclassifying billing-wrapped quota errors. (#33813) thanks @zhouhe-xydt.</li>
<li>Mattermost/interactive button callbacks: allow external callback base URLs and stop requiring loopback-origin requests so button clicks work when Mattermost reaches the gateway over Tailscale, LAN, or a reverse proxy. (#37543) thanks @mukhtharcm.</li>
<li>Gateway/chat.send route inheritance: keep explicit external delivery for channel-scoped sessions while preventing shared-main and other channel-agnostic webchat sessions from inheriting stale external routes, so Control UI replies stay on webchat without breaking selected channel-target sessions. (#34669) Thanks @vincentkoc.</li>
<li>Telegram/Discord media upload caps: make outbound uploads honor channel <code>mediaMaxMb</code> config, raise Telegram's default media cap to 100MB, and remove MIME fallback limits that kept some Telegram uploads at 16MB. Thanks @vincentkoc.</li>
<li>Skills/nano-banana-pro resolution override: respect explicit <code>--resolution</code> values during image editing and only auto-detect output size from input images when the flag is omitted. (#36880) Thanks @shuofengzhang and @vincentkoc.</li>
<li>Skills/openai-image-gen CLI validation: validate <code>--background</code> and <code>--style</code> inputs early, normalize supported values, and warn when those flags are ignored for incompatible models. (#36762) Thanks @shuofengzhang and @vincentkoc.</li>
<li>Skills/openai-image-gen output formats: validate <code>--output-format</code> values early, normalize aliases like <code>jpg -> jpeg</code>, and warn when the flag is ignored for incompatible models. (#36648) Thanks @shuofengzhang and @vincentkoc.</li>
<li>ACP/skill env isolation: strip skill-injected API keys from ACP harness child-process environments so tools like Codex CLI keep their own auth flow instead of inheriting billed provider keys from active skills. (#36316) Thanks @taw0002 and @vincentkoc.</li>
<li>WhatsApp media upload caps: make outbound media sends and auto-replies honor <code>channels.whatsapp.mediaMaxMb</code> with per-account overrides so inbound and outbound limits use the same channel config. Thanks @vincentkoc.</li>
<li>Windows/Plugin install: when OpenClaw runs on Windows via Bun and <code>npm-cli.js</code> is not colocated with the runtime binary, fall back to <code>npm.cmd</code>/<code>npx.cmd</code> through the existing <code>cmd.exe</code> wrapper so <code>openclaw plugins install</code> no longer fails with <code>spawn EINVAL</code>. (#38056) Thanks @0xlin2023.</li>
<li>Telegram/send retry classification: retry grammY <code>Network request ... failed after N attempts</code> envelopes in send flows without reclassifying plain <code>Network request ... failed!</code> wrappers as transient, restoring the intended retry path while keeping broad send-context message matching tight. (#38056) Thanks @0xlin2023.</li>
<li>Gateway/probes: keep <code>/health</code>, <code>/healthz</code>, <code>/ready</code>, and <code>/readyz</code> reachable when the Control UI is mounted at <code>/</code>, preserve plugin-owned route precedence on those paths, and make <code>/ready</code> and <code>/readyz</code> report channel-backed readiness with startup grace plus <code>503</code> on disconnected managed channels, while <code>/health</code> and <code>/healthz</code> stay shallow liveness probes. (#18446) Thanks @vibecodooor, @mahsumaktas, and @vincentkoc.</li>
<li>Feishu/media downloads: drop invalid timeout fields from SDK method calls now that client-level <code>httpTimeoutMs</code> applies to requests. (#38267) Thanks @ant1eicher and @thewilloftheshadow.</li>
<li>PI embedded runner/Feishu docs: propagate sender identity into embedded attempts so Feishu doc auto-grant restores requester access for embedded-runner executions. (#32915) thanks @cszhouwei.</li>
<li>Agents/usage normalization: normalize missing or partial assistant usage snapshots before compaction accounting so <code>openclaw agent --json</code> no longer crashes when provider payloads omit <code>totalTokens</code> or related usage fields. (#34977) thanks @sp-hk2ldn.</li>
<li>Venice/default model refresh: switch the built-in Venice default to <code>kimi-k2-5</code>, update onboarding aliasing, and refresh Venice provider docs/recommendations to match the current private and anonymized catalog. (from #12964) Fixes #20156. Thanks @sabrinaaquino and @vincentkoc.</li>
<li>Agents/skill API write pacing: add a global prompt guardrail that treats skill-driven external API writes as rate-limited by default, so runners prefer batched writes, avoid tight request loops, and respect <code>429</code>/<code>Retry-After</code>. Thanks @vincentkoc.</li>
<li>Google Chat/multi-account webhook auth fallback: when <code>channels.googlechat.accounts.default</code> carries shared webhook audience/path settings (for example after config normalization), inherit those defaults for named accounts while preserving top-level and per-account overrides, so inbound webhook verification no longer fails silently for named accounts missing duplicated audience fields. Fixes #38369.</li>
<li>Models/tool probing: raise the tool-capability probe budget from 32 to 256 tokens so reasoning models that spend tokens on thinking before returning a required tool call are less likely to be misclassified as not supporting tools. (#7521) Thanks @jakobdylanc.</li>
<li>Gateway/transient network classification: treat wrapped <code>...: fetch failed</code> transport messages as transient while avoiding broad matches like <code>Web fetch failed (404): ...</code>, preventing Discord reconnect wrappers from crashing the gateway without suppressing non-network tool failures. (#38530) Thanks @xinhuagu.</li>
<li>ACP/console silent reply suppression: filter ACP <code>NO_REPLY</code> lead fragments and silent-only finals before <code>openclaw agent</code> logging/delivery so console-backed ACP sessions no longer leak <code>NO</code>/<code>NO_REPLY</code> placeholders. (#38436) Thanks @ql-wade.</li>
<li>Feishu/reply delivery reliability: disable block streaming in Feishu reply options so plain-text auto-render replies are no longer silently dropped before final delivery. (#38258) Thanks @xinhuagu.</li>
<li>Agents/reply MEDIA delivery: normalize local assistant <code>MEDIA:</code> paths before block/final delivery, keep media dedupe aligned with message-tool sends, and contain malformed media normalization failures so generated files send reliably instead of falling back to empty responses. (#38572) Thanks @obviyus.</li>
<li>Sessions/bootstrap cache rollover invalidation: clear cached workspace bootstrap snapshots whenever an existing <code>sessionKey</code> rolls to a new <code>sessionId</code> across auto-reply, command, and isolated cron session resolvers, so <code>AGENTS.md</code>/<code>MEMORY.md</code>/<code>USER.md</code> updates are reloaded after daily, idle, or forced session resets instead of staying stale until gateway restart. (#38494) Thanks @LivingInDrm.</li>
<li>Gateway/Telegram polling health monitor: skip stale-socket restarts for Telegram long-polling channels and thread channel identity through shared health evaluation so polling connections are not restarted on the WebSocket stale-socket heuristic. (#38395) Thanks @ql-wade and @Takhoffman.</li>
<li>Daemon/systemd fresh-install probe: check for OpenClaw's managed user unit before running <code>systemctl --user is-enabled</code>, so first-time Linux installs no longer fail on generic missing-unit probe errors. (#38819) Thanks @adaHubble.</li>
<li>Gateway/container lifecycle: allow <code>openclaw gateway stop</code> to SIGTERM unmanaged gateway listeners and <code>openclaw gateway restart</code> to SIGUSR1 a single unmanaged listener when no service manager is installed, so container and supervisor-based deployments are no longer blocked by <code>service disabled</code> no-op responses. Fixes #36137. Thanks @vincentkoc.</li>
<li>Gateway/Windows restart supervision: relaunch task-managed gateways through Scheduled Task with quoted helper-script command paths, distinguish restart-capable supervisors per platform, and stop orphaned Windows gateway children during self-restart. (#38825) Thanks @obviyus.</li>
<li>Telegram/native topic command routing: resolve forum-topic native commands through the same conversation route as inbound messages so topic <code>agentId</code> overrides and bound topic sessions target the active session instead of the default topic-parent session. (#38871) Thanks @obviyus.</li>
<li>Markdown/assistant image hardening: flatten remote markdown images to plain text across the Control UI, exported HTML, and shared Swift chat while keeping inline <code>data:image/...</code> markdown renderable, so model output no longer triggers automatic remote image fetches. (#38895) Thanks @obviyus.</li>
<li>Config/compaction safeguard settings: regression-test <code>agents.defaults.compaction.recentTurnsPreserve</code> through <code>loadConfig()</code> and cover the new help metadata entry so the exposed preserve knob stays wired through schema validation and config UX. (#25557) thanks @rodrigouroz.</li>
<li>iOS/Quick Setup presentation: skip automatic Quick Setup when a gateway is already configured (active connect config, last-known connection, preferred gateway, or manual host), so reconnecting installs no longer get prompted to connect again. (#38964) Thanks @ngutman.</li>
<li>CLI/Docs memory help accuracy: clarify <code>openclaw memory status --deep</code> behavior and align memory command examples/docs with the current search options. (#31803) Thanks @JasonOA888 and @Avi974.</li>
<li>Auto-reply/allowlist store account scoping: keep <code>/allowlist ... --store</code> writes scoped to the selected account and clear legacy unscoped entries when removing default-account store access, preventing cross-account default allowlist bleed-through from legacy pairing-store reads. Thanks @tdjackey for reporting and @vincentkoc for the fix.</li>
<li>Security/Nostr: harden profile mutation/import loopback guards by failing closed on non-loopback forwarded client headers (<code>x-forwarded-for</code> / <code>x-real-ip</code>) and rejecting <code>sec-fetch-site: cross-site</code>; adds regression coverage for proxy-forwarded and browser cross-site mutation attempts.</li>
<li>CLI/bootstrap Node version hint maintenance: replace hardcoded nvm <code>22</code> instructions in <code>openclaw.mjs</code> with <code>MIN_NODE_MAJOR</code> interpolation so future minimum-Node bumps keep startup guidance in sync automatically. (#39056) Thanks @onstash.</li>
<li>Discord/native slash command auth: honor <code>commands.allowFrom.discord</code> (and <code>commands.allowFrom["*"]</code>) in guild slash-command pre-dispatch authorization so allowlisted senders are no longer incorrectly rejected as unauthorized. (#38794) Thanks @jskoiz and @thewilloftheshadow.</li>
<li>Outbound/message target normalization: ignore empty legacy <code>to</code>/<code>channelId</code> fields when explicit <code>target</code> is provided so valid target-based sends no longer fail legacy-param validation; includes regression coverage. (#38944) Thanks @Narcooo.</li>
<li>Models/auth token prompts: guard cancelled manual token prompts so <code>Symbol(clack:cancel)</code> values cannot be persisted into auth profiles; adds regression coverage for cancelled <code>models auth paste-token</code>. (#38951) Thanks @MumuTW.</li>
<li>Gateway/loopback announce URLs: treat <code>http://</code> and <code>https://</code> aliases with the same loopback/private-network policy as websocket URLs so loopback cron announce delivery no longer fails secure URL validation. (#39064) Thanks @Narcooo.</li>
<li>Models/default provider fallback: when the hardcoded default provider is removed from <code>models.providers</code>, resolve defaults from configured providers instead of reporting stale removed-provider defaults in status output. (#38947) Thanks @davidemanuelDEV.</li>
<li>Agents/cache-trace stability: guard stable stringify against circular references in trace payloads so near-limit payloads no longer crash with <code>Maximum call stack size exceeded</code>; adds regression coverage. (#38935) Thanks @MumuTW.</li>
<li>Extensions/diffs CI stability: add <code>headers</code> to the <code>localReq</code> test helper in <code>extensions/diffs/index.test.ts</code> so forwarding-hint checks no longer crash with <code>req.headers</code> undefined. (supersedes #39063) Thanks @Shennng.</li>
<li>Agents/compaction thresholding: apply <code>agents.defaults.contextTokens</code> cap to the model passed into embedded run and <code>/compact</code> session creation so auto-compaction thresholds use the effective context window, not native model max context. (#39099) Thanks @MumuTW.</li>
<li>Models/merge mode provider precedence: when <code>models.mode: "merge"</code> is active and config explicitly sets a provider <code>baseUrl</code>, keep config as source of truth instead of preserving stale runtime <code>models.json</code> <code>baseUrl</code> values; includes normalized provider-key coverage. (#39103) Thanks @BigUncle.</li>
<li>UI/Control chat tool streaming: render tool events live in webchat without requiring refresh by enabling <code>tool-events</code> capability, fixing stream/event correlation, and resetting/reloading stream state around tool results and terminal events. (#39104) Thanks @jakepresent.</li>
<li>Models/provider apiKey persistence hardening: when a provider <code>apiKey</code> value equals a known provider env var value, persist the canonical env var name into <code>models.json</code> instead of resolved plaintext secrets. (#38889) Thanks @gambletan.</li>
<li>Discord/model picker persistence check: add a short post-dispatch settle delay before reading back session model state so picker confirmations stop reporting false mismatch warnings after successful model switches. (#39105) Thanks @akropp.</li>
<li>Agents/OpenAI WS compat store flag: omit <code>store</code> from <code>response.create</code> payloads when model compat sets <code>supportsStore: false</code>, preventing strict OpenAI-compatible providers from rejecting websocket requests with unknown-field errors. (#39113) Thanks @scoootscooob.</li>
<li>Config/validation log sanitization: sanitize config-validation issue paths/messages before logging so control characters and ANSI escape sequences cannot inject misleading terminal output from crafted config content. (#39116) Thanks @powermaster888.</li>
<li>Agents/compaction counter accuracy: count successful overflow-triggered auto-compactions (<code>willRetry=true</code>) in the compaction counter while still excluding aborted/no-result events, so <code>/status</code> reflects actual safeguard compaction activity. (#39123) Thanks @MumuTW.</li>
<li>Gateway/chat delta ordering: flush buffered assistant deltas before emitting tool <code>start</code> events so pre-tool text is delivered to Control UI before tool cards, avoiding transient text/tool ordering artifacts in streaming. (#39128) Thanks @0xtangping.</li>
<li>Voice-call plugin schema parity: add missing manifest <code>configSchema</code> fields (<code>webhookSecurity</code>, <code>streaming.preStartTimeoutMs|maxPendingConnections|maxPendingConnectionsPerIp|maxConnections</code>, <code>staleCallReaperSeconds</code>) so gateway AJV validation accepts already-supported runtime config instead of failing with <code>additionalProperties</code> errors. (#38892) Thanks @giumex.</li>
<li>Agents/OpenAI WS reconnect retry accounting: avoid double retry scheduling when reconnect failures emit both <code>error</code> and <code>close</code>, so retry budgets track actual reconnect attempts instead of exhausting early. (#39133) Thanks @scoootscooob.</li>
<li>Daemon/Windows schtasks runtime detection: use locale-invariant <code>Last Run Result</code> running codes (<code>0x41301</code>/<code>267009</code>) as the primary running signal so <code>openclaw node status</code> no longer misreports active tasks as stopped on non-English Windows locales. (#39076) Thanks @ademczuk.</li>
<li>Usage/token count formatting: round near-million token counts to millions (<code>1.0m</code>) instead of <code>1000k</code>, with explicit boundary coverage for <code>999_499</code> and <code>999_500</code>. (#39129) Thanks @CurryMessi.</li>
<li>Gateway/session bootstrap cache invalidation ordering: clear bootstrap snapshots only after active embedded-run shutdown wait completes, preventing dying runs from repopulating stale cache between <code>/new</code>/<code>sessions.reset</code> turns. (#38873) Thanks @MumuTW.</li>
<li>Browser/dispatcher error clarity: preserve dispatcher-side failure context in browser fetch errors while still appending operator guidance and explicit no-retry model hints, preventing misleading <code>"Can't reach service"</code> wrapping and avoiding LLM retry loops. (#39090) Thanks @NewdlDewdl.</li>
<li>Telegram/polling offset safety: confirm persisted offsets before polling startup while validating stored <code>lastUpdateId</code> values as non-negative safe integers (with overflow guards) so malformed offset state cannot cause update skipping/dropping. (#39111) Thanks @MumuTW.</li>
<li>Telegram/status SecretRef read-only resolution: resolve env-backed bot-token SecretRefs in config-only/status inspection while respecting provider source/defaults and env allowlists, so status no longer crashes or reports false-ready tokens for disallowed providers. (#39130) Thanks @neocody.</li>
<li>Agents/OpenAI WS max-token zero forwarding: treat <code>maxTokens: 0</code> as an explicit value in websocket <code>response.create</code> payloads (instead of dropping it as falsy), with regression coverage for zero-token forwarding. (#39148) Thanks @scoootscooob.</li>
<li>Podman/.env gateway bind precedence: evaluate <code>OPENCLAW_GATEWAY_BIND</code> after sourcing <code>.env</code> in <code>run-openclaw-podman.sh</code> so env-file overrides are honored. (#38785) Thanks @majinyu666.</li>
<li>Models/default alias refresh: bump <code>gpt</code> to <code>openai/gpt-5.4</code> and Gemini defaults to <code>gemini-3.1</code> preview aliases (including normalization/default wiring) to track current model IDs. (#38638) Thanks @ademczuk.</li>
<li>Config/env substitution degraded mode: convert missing <code>${VAR}</code> resolution in config reads from hard-fail to warning-backed degraded behavior, while preventing unresolved placeholders from being accepted as gateway credentials. (#39050) Thanks @akz142857.</li>
<li>Discord inbound listener non-blocking dispatch: make <code>MESSAGE_CREATE</code> listener handoff asynchronous (no per-listener queue blocking), so long runs no longer stall unrelated incoming events. (#39154) Thanks @yaseenkadlemakki.</li>
<li>Daemon/Windows PATH freeze fix: stop persisting install-time <code>PATH</code> snapshots into Scheduled Task scripts so runtime tool lookup follows current host PATH updates; also refresh local TUI history on silent local finals. (#39139) Thanks @Narcooo.</li>
<li>Gateway/systemd service restart hardening: clear stale gateway listeners by explicit run-port before service bind, add restart stale-pid port-override support, tune systemd start/stop/exit handling, and disable detached child mode only in service-managed runtime so cgroup stop semantics clean up descendants reliably. (#38463) Thanks @spirittechie.</li>
<li>Discord/plugin native command aliases: let plugins declare provider-specific slash names so native Discord registration can avoid built-in command collisions; the bundled Talk voice plugin now uses <code>/talkvoice</code> natively on Discord while keeping text <code>/voice</code>.</li>
<li>Daemon/Windows schtasks status normalization: derive runtime state from locale-neutral numeric <code>Last Run Result</code> codes only (without language string matching) and surface unknown when numeric result data is unavailable, preventing locale-specific misclassification drift. (#39153) Thanks @scoootscooob.</li>
<li>Telegram/polling conflict recovery: reset the polling <code>webhookCleared</code> latch on <code>getUpdates</code> 409 conflicts so webhook cleanup re-runs on restart cycles and polling avoids infinite conflict loops. (#39205) Thanks @amittell.</li>
<li>Heartbeat/requests-in-flight scheduling: stop advancing <code>nextDueMs</code> and avoid immediate <code>scheduleNext()</code> timer overrides on requests-in-flight skips, so wake-layer retry cooldowns are honored and heartbeat cadence no longer drifts under sustained contention. (#39182) Thanks @MumuTW.</li>
<li>Memory/SQLite contention resilience: re-apply <code>PRAGMA busy_timeout</code> on every sync-store and QMD connection open so process restarts/reopens no longer revert to immediate <code>SQLITE_BUSY</code> failures under lock contention. (#39183) Thanks @MumuTW.</li>
<li>Gateway/webchat route safety: block webchat/control-ui clients from inheriting stored external delivery routes on channel-scoped sessions (while preserving route inheritance for UI/TUI clients), preventing cross-channel leakage from scoped chats. (#39175) Thanks @widingmarcus-cyber.</li>
<li>Telegram error-surface resilience: return a user-visible fallback reply when dispatch/debounce processing fails instead of going silent, while preserving draft-stream cleanup and best-effort thread-scoped fallback delivery. (#39209) Thanks @riftzen-bit.</li>
<li>Gateway/password auth startup diagnostics: detect unresolved provider-reference objects in <code>gateway.auth.password</code> and fail with a specific bootstrap-secrets error message instead of generic misconfiguration output. (#39230) Thanks @ademczuk.</li>
<li>Agents/OpenAI-responses compatibility: strip unsupported <code>store</code> payload fields when <code>supportsStore=false</code> (including OpenAI-compatible non-OpenAI providers) while preserving server-compaction payload behavior. (#39219) Thanks @ademczuk.</li>
<li>Agents/model fallback visibility: warn when configured model IDs cannot be resolved and fallback is applied, with log-safe sanitization of model text to prevent control-sequence injection in warning output. (#39215) Thanks @ademczuk.</li>
<li>Outbound delivery replay safety: use two-phase delivery ACK markers (<code>.json</code> -> <code>.delivered</code> -> unlink) and startup marker cleanup so crash windows between send and cleanup do not replay already-delivered messages. (#38668) Thanks @Gundam98.</li>
<li>Nodes/system.run approval binding: carry prepared approval plans through gateway forwarding and bind interpreter-style script operands across approval to execution, so post-approval script rewrites are denied while unchanged approved script runs keep working. Thanks @tdjackey for reporting.</li>
<li>Nodes/system.run PowerShell wrapper parsing: treat <code>pwsh</code>/<code>powershell</code> <code>-EncodedCommand</code> forms as shell-wrapper payloads so allowlist mode still requires approval instead of falling back to plain argv analysis. Thanks @tdjackey for reporting.</li>
<li>Control UI/auth error reporting: map generic browser <code>Fetch failed</code> websocket close errors back to actionable gateway auth messages (<code>gateway token mismatch</code>, <code>authentication failed</code>, <code>retry later</code>) so dashboard disconnects stop hiding credential problems. Landed from contributor PR #28608 by @KimGLee. Thanks @KimGLee.</li>
<li>Media/mime unknown-kind handling: return <code>undefined</code> (not <code>"unknown"</code>) for missing/unrecognized MIME kinds and use document-size fallback caps for unknown remote media, preventing phantom <code><media:unknown></code> Signal events from being treated as real messages. (#39199) Thanks @nicolasgrasset.</li>
<li>Nodes/system.run allow-always persistence: honor shell comment semantics during allowlist analysis so <code>#</code>-tailed payloads that never execute are not persisted as trusted follow-up commands. Thanks @tdjackey for reporting.</li>
<li>Signal/inbound attachment fan-in: forward all successfully fetched inbound attachments through <code>MediaPaths</code>/<code>MediaUrls</code>/<code>MediaTypes</code> (instead of only the first), and improve multi-attachment placeholder summaries in mention-gated pending history. (#39212) Thanks @joeykrug.</li>
<li>Nodes/system.run dispatch-wrapper boundary: keep shell-wrapper approval classification active at the depth boundary so <code>env</code> wrapper stacks cannot reach <code>/bin/sh -c</code> execution without the expected approval gate. Thanks @tdjackey for reporting.</li>
<li>Docker/token persistence on reconfigure: reuse the existing <code>.env</code> gateway token during <code>docker-setup.sh</code> reruns and align compose token env defaults, so Docker installs stop silently rotating tokens and breaking existing dashboard sessions. Landed from contributor PR #33097 by @chengzhichao-xydt. Thanks @chengzhichao-xydt.</li>
<li>Agents/strict OpenAI turn ordering: apply assistant-first transcript bootstrap sanitization to strict OpenAI-compatible providers (for example vLLM/Gemma via <code>openai-completions</code>) without adding Google-specific session markers, preventing assistant-first history rejections. (#39252) Thanks @scoootscooob.</li>
<li>Discord/exec approvals gateway auth: pass resolved shared gateway credentials into the Discord exec-approvals gateway client so token-auth installs stop failing approvals with <code>gateway token mismatch</code>. Related to #38179. Thanks @0riginal-claw for the adjacent PR #35147 investigation.</li>
<li>Subagents/workspace inheritance: propagate parent workspace directory to spawned subagent runs so child sessions reliably inherit workspace-scoped instructions (<code>AGENTS.md</code>, <code>SOUL.md</code>, etc.) without exposing workspace override through tool-call arguments. (#39247) Thanks @jasonQin6.</li>
<li>Exec approvals/gateway-node policy: honor explicit <code>ask=off</code> from <code>exec-approvals.json</code> even when runtime defaults are stricter, so trusted full/off setups stop re-prompting on gateway and node exec paths. Landed from contributor PR #26789 by @pandego. Thanks @pandego.</li>
<li>Exec approvals/config fallback: inherit <code>ask</code> from <code>exec-approvals.json</code> when <code>tools.exec.ask</code> is unset, so local full/off defaults no longer fall back to <code>on-miss</code> for exec tool and <code>nodes run</code>. Landed from contributor PR #29187 by @Bartok9. Thanks @Bartok9.</li>
<li>Exec approvals/allow-always shell scripts: persist and match script paths for wrapper invocations like <code>bash scripts/foo.sh</code> while still blocking <code>-c</code>/<code>-s</code> wrapper bypasses. Landed from contributor PR #35137 by @yuweuii. Thanks @yuweuii.</li>
<li>Queue/followup dedupe across drain restarts: dedupe queued redelivery <code>message_id</code> values after queue recreation so busy-session followups no longer duplicate on replayed inbound events. Landed from contributor PR #33168 by @rylena. Thanks @rylena.</li>
<li>Telegram/preview-final edit idempotence: treat <code>message is not modified</code> errors during preview finalization as delivered so partial-stream final replies do not fall back to duplicate sends. Landed from contributor PR #34983 by @HOYALIM. Thanks @HOYALIM.</li>
<li>Telegram/DM streaming transport parity: use message preview transport for all DM streaming lanes so final delivery can edit the active preview instead of sending duplicate finals. Landed from contributor PR #38906 by @gambletan. Thanks @gambletan.</li>
<li>Telegram/DM draft streaming restoration: restore native <code>sendMessageDraft</code> preview transport for DM answer streaming while keeping reasoning on message transport, with regression coverage to keep draft finalization from sending duplicate finals. (#39398) Thanks @obviyus.</li>
<li>Telegram/send retry safety: retry non-idempotent send paths only for pre-connect failures and make custom retry predicates strict, preventing ambiguous reconnect retries from sending duplicate messages. Landed from contributor PR #34238 by @hal-crackbot. Thanks @hal-crackbot.</li>
<li>ACP/run spawn delivery bootstrap: stop reusing requester inline delivery targets for one-shot <code>mode: "run"</code> ACP spawns, so fresh run-mode workers bootstrap in isolation instead of inheriting thread-bound session delivery behavior. (#39014) Thanks @lidamao633.</li>
<li>Discord/DM session-key normalization: rewrite legacy <code>discord:dm:*</code> and phantom direct-message <code>discord:channel:<user></code> session keys to <code>discord:direct:*</code> when the sender matches, so multi-agent Discord DMs stop falling into empty channel-shaped sessions and resume replying correctly.</li>
<li>Discord/native slash session fallback: treat empty configured bound-session keys as missing so <code>/status</code> and other native commands fall back to the routed slash session and routed channel session instead of blanking Discord session keys in normal channel bindings.</li>
<li>Agents/tool-call dispatch normalization: normalize provider-prefixed tool names before dispatch across <code>toolCall</code>, <code>toolUse</code>, and <code>functionCall</code> blocks, while preserving multi-segment tool suffixes when stripping provider wrappers so malformed-but-recoverable tool names no longer fail with <code>Tool not found</code>. (#39328) Thanks @vincentkoc.</li>
<li>Agents/parallel tool-call compatibility: honor <code>parallel_tool_calls</code> / <code>parallelToolCalls</code> extra params only for <code>openai-completions</code> and <code>openai-responses</code> payloads, preserve higher-precedence alias overrides across config and runtime layers, and ignore invalid non-boolean values so single-tool-call providers like NVIDIA-hosted Kimi stop failing on forced parallel tool-call payloads. (#37048) Thanks @vincentkoc.</li>
<li>Config/invalid-load fail-closed: stop converting <code>INVALID_CONFIG</code> into an empty runtime config, keep valid settings available only through explicit best-effort diagnostic reads, and route read-only CLI diagnostics through that path so unknown keys no longer silently drop security-sensitive config. (#28140) Thanks @bobsahur-robot and @vincentkoc.</li>
<li>Agents/codex-cli sandbox defaults: switch the built-in Codex backend from <code>read-only</code> to <code>workspace-write</code> so spawned coding runs can edit files out of the box. Landed from contributor PR #39336 by @0xtangping. Thanks @0xtangping.</li>
<li>Gateway/health-monitor restart reason labeling: report <code>disconnected</code> instead of <code>stuck</code> for clean channel disconnect restarts, so operator logs distinguish socket drops from genuinely stuck channels. (#36436) Thanks @Sid-Qin.</li>
<li>Control UI/agents-page overrides: auto-create minimal per-agent config entries when editing inherited agents, so model/tool/skill changes enable Save and inherited model fallbacks can be cleared by writing a primary-only override. Landed from contributor PR #39326 by @dunamismax. Thanks @dunamismax.</li>
<li>Gateway/Telegram webhook-mode recovery: add <code>webhookCertPath</code> to re-upload self-signed certificates during webhook registration and skip stale-socket detection for webhook-mode channels, so Telegram webhook setups survive health-monitor restarts. Landed from contributor PR #39313 by @fellanH. Thanks @fellanH.</li>
<li>Discord/config schema parity: add <code>channels.discord.agentComponents</code> to the strict Zod config schema so valid <code>agentComponents.enabled</code> settings (root and account-scoped) no longer fail with unrecognized-key validation errors. Landed from contributor PR #39378 by @gambletan. Thanks @gambletan and @thewilloftheshadow.</li>
<li>ACPX/MCP session bootstrap: inject configured MCP servers into ACP <code>session/new</code> and <code>session/load</code> for acpx-backed sessions, restoring Canva and other external MCP tools. Landed from contributor PR #39337. Thanks @goodspeed-apps.</li>
<li>Control UI/Telegram sender labels: preserve inbound sender labels in sanitized chat history so dashboard user-message groups split correctly and show real group-member names instead of <code>You</code>. (#39414) Thanks @obviyus.</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.3.7/OpenClaw-2026.3.7.zip" length="23263833" type="application/octet-stream" sparkle:edSignature="SO0zedZMzrvSDltLkuaSVQTWFPPPe1iu/enS4TGGb5EGckhqRCmNJWMKNID5lKwFC8vefTbfG9JTlSrZedP4Bg=="/>
</item>
</channel>
</rss>

View File

@ -30,12 +30,8 @@ cd apps/android
./gradlew :app:assembleDebug
./gradlew :app:installDebug
./gradlew :app:testDebugUnitTest
cd ../..
bun run android:bundle:release
```
`bun run android:bundle:release` auto-bumps Android `versionName`/`versionCode` in `apps/android/app/build.gradle.kts`, then builds a signed release `.aab`.
## Kotlin Lint + Format
```bash

View File

@ -1,7 +1,5 @@
import com.android.build.api.variant.impl.VariantOutputImpl
val dnsjavaInetAddressResolverService = "META-INF/services/java.net.spi.InetAddressResolverProvider"
val androidStoreFile = providers.gradleProperty("OPENCLAW_ANDROID_STORE_FILE").orNull?.takeIf { it.isNotBlank() }
val androidStorePassword = providers.gradleProperty("OPENCLAW_ANDROID_STORE_PASSWORD").orNull?.takeIf { it.isNotBlank() }
val androidKeyAlias = providers.gradleProperty("OPENCLAW_ANDROID_KEY_ALIAS").orNull?.takeIf { it.isNotBlank() }
@ -65,8 +63,8 @@ android {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
versionCode = 2026031400
versionName = "2026.3.14"
versionCode = 202603130
versionName = "2026.3.13"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
@ -80,9 +78,6 @@ android {
}
isMinifyEnabled = true
isShrinkResources = true
ndk {
debugSymbolLevel = "SYMBOL_TABLE"
}
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
debug {
@ -109,10 +104,6 @@ android {
"/META-INF/LICENSE*.txt",
"DebugProbesKt.bin",
"kotlin-tooling-metadata.json",
"org/bouncycastle/pqc/crypto/picnic/lowmcL1.bin.properties",
"org/bouncycastle/pqc/crypto/picnic/lowmcL3.bin.properties",
"org/bouncycastle/pqc/crypto/picnic/lowmcL5.bin.properties",
"org/bouncycastle/x509/CertPathReviewerMessages*.properties",
)
}
}
@ -177,6 +168,7 @@ dependencies {
// material-icons-extended pulled in full icon set (~20 MB DEX). Only ~18 icons used.
// R8 will tree-shake unused icons when minify is enabled on release builds.
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.navigation:navigation-compose:2.9.7")
debugImplementation("androidx.compose.ui:ui-tooling")
@ -201,6 +193,7 @@ dependencies {
implementation("androidx.camera:camera-camera2:1.5.2")
implementation("androidx.camera:camera-lifecycle:1.5.2")
implementation("androidx.camera:camera-video:1.5.2")
implementation("androidx.camera:camera-view:1.5.2")
implementation("com.google.android.gms:play-services-code-scanner:16.1.0")
// Unicast DNS-SD (Wide-Area Bonjour) for tailnet discovery domains.
@ -218,45 +211,3 @@ dependencies {
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
val stripReleaseDnsjavaServiceDescriptor =
tasks.register("stripReleaseDnsjavaServiceDescriptor") {
val mergedJar =
layout.buildDirectory.file(
"intermediates/merged_java_res/release/mergeReleaseJavaResource/base.jar",
)
inputs.file(mergedJar)
outputs.file(mergedJar)
doLast {
val jarFile = mergedJar.get().asFile
if (!jarFile.exists()) {
return@doLast
}
val unpackDir = temporaryDir.resolve("merged-java-res")
delete(unpackDir)
copy {
from(zipTree(jarFile))
into(unpackDir)
exclude(dnsjavaInetAddressResolverService)
}
delete(jarFile)
ant.invokeMethod(
"zip",
mapOf(
"destfile" to jarFile.absolutePath,
"basedir" to unpackDir.absolutePath,
),
)
}
}
tasks.matching { it.name == "stripReleaseDnsjavaServiceDescriptor" }.configureEach {
dependsOn("mergeReleaseJavaResource")
}
tasks.matching { it.name == "minifyReleaseWithR8" }.configureEach {
dependsOn(stripReleaseDnsjavaServiceDescriptor)
}

View File

@ -1,6 +1,26 @@
# ── App classes ───────────────────────────────────────────────────
-keep class ai.openclaw.app.** { *; }
# ── Bouncy Castle ─────────────────────────────────────────────────
-keep class org.bouncycastle.** { *; }
-dontwarn org.bouncycastle.**
# ── CameraX ───────────────────────────────────────────────────────
-keep class androidx.camera.** { *; }
# ── kotlinx.serialization ────────────────────────────────────────
-keep class kotlinx.serialization.** { *; }
-keepclassmembers class * {
@kotlinx.serialization.Serializable *;
}
-keepattributes *Annotation*, InnerClasses
# ── OkHttp ────────────────────────────────────────────────────────
-dontwarn okhttp3.**
-dontwarn okio.**
-keep class okhttp3.internal.platform.** { *; }
# ── Misc suppressions ────────────────────────────────────────────
-dontwarn com.sun.jna.**
-dontwarn javax.naming.**
-dontwarn lombok.Generated

View File

@ -19,7 +19,6 @@
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />

View File

@ -176,10 +176,6 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
runtime.requestCanvasRehydrate(source = source, force = true)
}
fun refreshHomeCanvasOverviewIfConnected() {
runtime.refreshHomeCanvasOverviewIfConnected()
}
fun loadChat(sessionKey: String) {
runtime.loadChat(sessionKey)
}

View File

@ -33,8 +33,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
@ -110,10 +108,6 @@ class NodeRuntime(context: Context) {
appContext = appContext,
)
private val callLogHandler: CallLogHandler = CallLogHandler(
appContext = appContext,
)
private val motionHandler: MotionHandler = MotionHandler(
appContext = appContext,
)
@ -155,7 +149,6 @@ class NodeRuntime(context: Context) {
smsHandler = smsHandlerImpl,
a2uiHandler = a2uiHandler,
debugHandler = debugHandler,
callLogHandler = callLogHandler,
isForeground = { _isForeground.value },
cameraEnabled = { cameraEnabled.value },
locationEnabled = { locationMode.value != LocationMode.Off },
@ -217,8 +210,7 @@ class NodeRuntime(context: Context) {
private val _isForeground = MutableStateFlow(true)
val isForeground: StateFlow<Boolean> = _isForeground.asStateFlow()
private var gatewayDefaultAgentId: String? = null
private var gatewayAgents: List<GatewayAgentSummary> = emptyList()
private var lastAutoA2uiUrl: String? = null
private var didAutoRequestCanvasRehydrate = false
private val canvasRehydrateSeq = AtomicLong(0)
private var operatorConnected = false
@ -240,7 +232,7 @@ class NodeRuntime(context: Context) {
updateStatus()
micCapture.onGatewayConnectionChanged(true)
scope.launch {
refreshHomeCanvasOverviewIfConnected()
refreshBrandingFromGateway()
if (voiceReplySpeakerLazy.isInitialized()) {
voiceReplySpeaker.refreshConfig()
}
@ -278,7 +270,7 @@ class NodeRuntime(context: Context) {
_canvasRehydratePending.value = false
_canvasRehydrateErrorText.value = null
updateStatus()
showLocalCanvasOnConnect()
maybeNavigateToA2uiOnConnect()
},
onDisconnected = { message ->
_nodeConnected.value = false
@ -404,7 +396,6 @@ class NodeRuntime(context: Context) {
_mainSessionKey.value = trimmed
talkMode.setMainSessionKey(trimmed)
chat.applyMainSessionKey(trimmed)
updateHomeCanvasState()
}
private fun updateStatus() {
@ -424,7 +415,6 @@ class NodeRuntime(context: Context) {
operator.isNotBlank() && operator != "Offline" -> operator
else -> node
}
updateHomeCanvasState()
}
private fun resolveMainSessionKey(): String {
@ -432,31 +422,23 @@ class NodeRuntime(context: Context) {
return if (trimmed.isEmpty()) "main" else trimmed
}
private fun showLocalCanvasOnConnect() {
_canvasA2uiHydrated.value = false
_canvasRehydratePending.value = false
_canvasRehydrateErrorText.value = null
canvas.navigate("")
private fun maybeNavigateToA2uiOnConnect() {
val a2uiUrl = a2uiHandler.resolveA2uiHostUrl() ?: return
val current = canvas.currentUrl()?.trim().orEmpty()
if (current.isEmpty() || current == lastAutoA2uiUrl) {
lastAutoA2uiUrl = a2uiUrl
canvas.navigate(a2uiUrl)
}
}
private fun showLocalCanvasOnDisconnect() {
lastAutoA2uiUrl = null
_canvasA2uiHydrated.value = false
_canvasRehydratePending.value = false
_canvasRehydrateErrorText.value = null
canvas.navigate("")
}
fun refreshHomeCanvasOverviewIfConnected() {
if (!operatorConnected) {
updateHomeCanvasState()
return
}
scope.launch {
refreshBrandingFromGateway()
refreshAgentsFromGateway()
}
}
fun requestCanvasRehydrate(source: String = "manual", force: Boolean = true) {
scope.launch {
if (!_nodeConnected.value) {
@ -620,8 +602,6 @@ class NodeRuntime(context: Context) {
canvas.setDebugStatus(status, server ?: remote)
}
}
updateHomeCanvasState()
}
fun setForeground(value: Boolean) {
@ -948,177 +928,11 @@ class NodeRuntime(context: Context) {
val parsed = parseHexColorArgb(raw)
_seamColorArgb.value = parsed ?: DEFAULT_SEAM_COLOR_ARGB
updateHomeCanvasState()
} catch (_: Throwable) {
// ignore
}
}
private suspend fun refreshAgentsFromGateway() {
if (!operatorConnected) return
try {
val res = operatorSession.request("agents.list", "{}")
val root = json.parseToJsonElement(res).asObjectOrNull() ?: return
val defaultAgentId = root["defaultId"].asStringOrNull()?.trim().orEmpty()
val mainKey = normalizeMainKey(root["mainKey"].asStringOrNull())
val agents =
(root["agents"] as? JsonArray)?.mapNotNull { item ->
val obj = item.asObjectOrNull() ?: return@mapNotNull null
val id = obj["id"].asStringOrNull()?.trim().orEmpty()
if (id.isEmpty()) return@mapNotNull null
val name = obj["name"].asStringOrNull()?.trim()
val emoji = obj["identity"].asObjectOrNull()?.get("emoji").asStringOrNull()?.trim()
GatewayAgentSummary(
id = id,
name = name?.takeIf { it.isNotEmpty() },
emoji = emoji?.takeIf { it.isNotEmpty() },
)
} ?: emptyList()
gatewayDefaultAgentId = defaultAgentId.ifEmpty { null }
gatewayAgents = agents
applyMainSessionKey(mainKey)
updateHomeCanvasState()
} catch (_: Throwable) {
// ignore
}
}
private fun updateHomeCanvasState() {
val payload =
try {
json.encodeToString(makeHomeCanvasPayload())
} catch (_: Throwable) {
null
}
canvas.updateHomeCanvasState(payload)
}
private fun makeHomeCanvasPayload(): HomeCanvasPayload {
val state = resolveHomeCanvasGatewayState()
val gatewayName = normalized(_serverName.value)
val gatewayAddress = normalized(_remoteAddress.value)
val gatewayLabel = gatewayName ?: gatewayAddress ?: "Gateway"
val activeAgentId = resolveActiveAgentId()
val agents = homeCanvasAgents(activeAgentId)
return when (state) {
HomeCanvasGatewayState.Connected ->
HomeCanvasPayload(
gatewayState = "connected",
eyebrow = "Connected to $gatewayLabel",
title = "Your agents are ready",
subtitle =
"This phone stays dormant until the gateway needs it, then wakes, syncs, and goes back to sleep.",
gatewayLabel = gatewayLabel,
activeAgentName = resolveActiveAgentName(activeAgentId),
activeAgentBadge = agents.firstOrNull { it.isActive }?.badge ?: "OC",
activeAgentCaption = "Selected on this phone",
agentCount = agents.size,
agents = agents.take(6),
footer = "The overview refreshes on reconnect and when this screen opens.",
)
HomeCanvasGatewayState.Connecting ->
HomeCanvasPayload(
gatewayState = "connecting",
eyebrow = "Reconnecting",
title = "OpenClaw is syncing back up",
subtitle =
"The gateway session is coming back online. Agent shortcuts should settle automatically in a moment.",
gatewayLabel = gatewayLabel,
activeAgentName = resolveActiveAgentName(activeAgentId),
activeAgentBadge = "OC",
activeAgentCaption = "Gateway session in progress",
agentCount = agents.size,
agents = agents.take(4),
footer = "If the gateway is reachable, reconnect should complete without intervention.",
)
HomeCanvasGatewayState.Error, HomeCanvasGatewayState.Offline ->
HomeCanvasPayload(
gatewayState = if (state == HomeCanvasGatewayState.Error) "error" else "offline",
eyebrow = "Welcome to OpenClaw",
title = "Your phone stays quiet until it is needed",
subtitle =
"Pair this device to your gateway to wake it only for real work, keep a live agent overview handy, and avoid battery-draining background loops.",
gatewayLabel = gatewayLabel,
activeAgentName = "Main",
activeAgentBadge = "OC",
activeAgentCaption = "Connect to load your agents",
agentCount = agents.size,
agents = agents.take(4),
footer = "When connected, the gateway can wake the phone with a silent push instead of holding an always-on session.",
)
}
}
private fun resolveHomeCanvasGatewayState(): HomeCanvasGatewayState {
val lower = _statusText.value.trim().lowercase()
return when {
_isConnected.value -> HomeCanvasGatewayState.Connected
lower.contains("connecting") || lower.contains("reconnecting") -> HomeCanvasGatewayState.Connecting
lower.contains("error") || lower.contains("failed") -> HomeCanvasGatewayState.Error
else -> HomeCanvasGatewayState.Offline
}
}
private fun resolveActiveAgentId(): String {
val mainKey = _mainSessionKey.value.trim()
if (mainKey.startsWith("agent:")) {
val agentId = mainKey.removePrefix("agent:").substringBefore(':').trim()
if (agentId.isNotEmpty()) return agentId
}
return gatewayDefaultAgentId?.trim().orEmpty()
}
private fun resolveActiveAgentName(activeAgentId: String): String {
if (activeAgentId.isNotEmpty()) {
gatewayAgents.firstOrNull { it.id == activeAgentId }?.let { agent ->
return normalized(agent.name) ?: agent.id
}
return activeAgentId
}
return gatewayAgents.firstOrNull()?.let { normalized(it.name) ?: it.id } ?: "Main"
}
private fun homeCanvasAgents(activeAgentId: String): List<HomeCanvasAgentCard> {
val defaultAgentId = gatewayDefaultAgentId?.trim().orEmpty()
return gatewayAgents
.map { agent ->
val isActive = activeAgentId.isNotEmpty() && agent.id == activeAgentId
val isDefault = defaultAgentId.isNotEmpty() && agent.id == defaultAgentId
HomeCanvasAgentCard(
id = agent.id,
name = normalized(agent.name) ?: agent.id,
badge = homeCanvasBadge(agent),
caption =
when {
isActive -> "Active on this phone"
isDefault -> "Default agent"
else -> "Ready"
},
isActive = isActive,
)
}.sortedWith(compareByDescending<HomeCanvasAgentCard> { it.isActive }.thenBy { it.name.lowercase() })
}
private fun homeCanvasBadge(agent: GatewayAgentSummary): String {
val emoji = normalized(agent.emoji)
if (emoji != null) return emoji
val initials =
(normalized(agent.name) ?: agent.id)
.split(' ', '-', '_')
.filter { it.isNotBlank() }
.take(2)
.mapNotNull { token -> token.firstOrNull()?.uppercaseChar()?.toString() }
.joinToString("")
return if (initials.isNotEmpty()) initials else "OC"
}
private fun normalized(value: String?): String? {
val trimmed = value?.trim().orEmpty()
return trimmed.ifEmpty { null }
}
private fun triggerCameraFlash() {
// Token is used as a pulse trigger; value doesn't matter as long as it changes.
_cameraFlashToken.value = SystemClock.elapsedRealtimeNanos()
@ -1137,40 +951,3 @@ class NodeRuntime(context: Context) {
}
}
private enum class HomeCanvasGatewayState {
Connected,
Connecting,
Error,
Offline,
}
private data class GatewayAgentSummary(
val id: String,
val name: String?,
val emoji: String?,
)
@Serializable
private data class HomeCanvasPayload(
val gatewayState: String,
val eyebrow: String,
val title: String,
val subtitle: String,
val gatewayLabel: String,
val activeAgentName: String,
val activeAgentBadge: String,
val activeAgentCaption: String,
val agentCount: Int,
val agents: List<HomeCanvasAgentCard>,
val footer: String,
)
@Serializable
private data class HomeCanvasAgentCard(
val id: String,
val name: String,
val badge: String,
val caption: String,
val isActive: Boolean,
)

View File

@ -1,247 +0,0 @@
package ai.openclaw.app.node
import android.Manifest
import android.content.Context
import android.provider.CallLog
import androidx.core.content.ContextCompat
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.put
private const val DEFAULT_CALL_LOG_LIMIT = 25
internal data class CallLogRecord(
val number: String?,
val cachedName: String?,
val date: Long,
val duration: Long,
val type: Int,
)
internal data class CallLogSearchRequest(
val limit: Int, // Number of records to return
val offset: Int, // Offset value
val cachedName: String?, // Search by contact name
val number: String?, // Search by phone number
val date: Long?, // Search by time (timestamp, deprecated, use dateStart/dateEnd)
val dateStart: Long?, // Query start time (timestamp)
val dateEnd: Long?, // Query end time (timestamp)
val duration: Long?, // Search by duration (seconds)
val type: Int?, // Search by call log type
)
internal interface CallLogDataSource {
fun hasReadPermission(context: Context): Boolean
fun search(context: Context, request: CallLogSearchRequest): List<CallLogRecord>
}
private object SystemCallLogDataSource : CallLogDataSource {
override fun hasReadPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_CALL_LOG
) == android.content.pm.PackageManager.PERMISSION_GRANTED
}
override fun search(context: Context, request: CallLogSearchRequest): List<CallLogRecord> {
val resolver = context.contentResolver
val projection = arrayOf(
CallLog.Calls.NUMBER,
CallLog.Calls.CACHED_NAME,
CallLog.Calls.DATE,
CallLog.Calls.DURATION,
CallLog.Calls.TYPE,
)
// Build selection and selectionArgs for filtering
val selections = mutableListOf<String>()
val selectionArgs = mutableListOf<String>()
request.cachedName?.let {
selections.add("${CallLog.Calls.CACHED_NAME} LIKE ?")
selectionArgs.add("%$it%")
}
request.number?.let {
selections.add("${CallLog.Calls.NUMBER} LIKE ?")
selectionArgs.add("%$it%")
}
// Support time range query
if (request.dateStart != null && request.dateEnd != null) {
selections.add("${CallLog.Calls.DATE} >= ? AND ${CallLog.Calls.DATE} <= ?")
selectionArgs.add(request.dateStart.toString())
selectionArgs.add(request.dateEnd.toString())
} else if (request.dateStart != null) {
selections.add("${CallLog.Calls.DATE} >= ?")
selectionArgs.add(request.dateStart.toString())
} else if (request.dateEnd != null) {
selections.add("${CallLog.Calls.DATE} <= ?")
selectionArgs.add(request.dateEnd.toString())
} else if (request.date != null) {
// Compatible with the old date parameter (exact match)
selections.add("${CallLog.Calls.DATE} = ?")
selectionArgs.add(request.date.toString())
}
request.duration?.let {
selections.add("${CallLog.Calls.DURATION} = ?")
selectionArgs.add(it.toString())
}
request.type?.let {
selections.add("${CallLog.Calls.TYPE} = ?")
selectionArgs.add(it.toString())
}
val selection = if (selections.isNotEmpty()) selections.joinToString(" AND ") else null
val selectionArgsArray = if (selectionArgs.isNotEmpty()) selectionArgs.toTypedArray() else null
val sortOrder = "${CallLog.Calls.DATE} DESC"
resolver.query(
CallLog.Calls.CONTENT_URI,
projection,
selection,
selectionArgsArray,
sortOrder,
).use { cursor ->
if (cursor == null) return emptyList()
val numberIndex = cursor.getColumnIndex(CallLog.Calls.NUMBER)
val cachedNameIndex = cursor.getColumnIndex(CallLog.Calls.CACHED_NAME)
val dateIndex = cursor.getColumnIndex(CallLog.Calls.DATE)
val durationIndex = cursor.getColumnIndex(CallLog.Calls.DURATION)
val typeIndex = cursor.getColumnIndex(CallLog.Calls.TYPE)
// Skip offset rows
if (request.offset > 0 && cursor.moveToPosition(request.offset - 1)) {
// Successfully moved to offset position
}
val out = mutableListOf<CallLogRecord>()
var count = 0
while (cursor.moveToNext() && count < request.limit) {
out += CallLogRecord(
number = cursor.getString(numberIndex),
cachedName = cursor.getString(cachedNameIndex),
date = cursor.getLong(dateIndex),
duration = cursor.getLong(durationIndex),
type = cursor.getInt(typeIndex),
)
count++
}
return out
}
}
}
class CallLogHandler private constructor(
private val appContext: Context,
private val dataSource: CallLogDataSource,
) {
constructor(appContext: Context) : this(appContext = appContext, dataSource = SystemCallLogDataSource)
fun handleCallLogSearch(paramsJson: String?): GatewaySession.InvokeResult {
if (!dataSource.hasReadPermission(appContext)) {
return GatewaySession.InvokeResult.error(
code = "CALL_LOG_PERMISSION_REQUIRED",
message = "CALL_LOG_PERMISSION_REQUIRED: grant Call Log permission",
)
}
val request = parseSearchRequest(paramsJson)
?: return GatewaySession.InvokeResult.error(
code = "INVALID_REQUEST",
message = "INVALID_REQUEST: expected JSON object",
)
return try {
val callLogs = dataSource.search(appContext, request)
GatewaySession.InvokeResult.ok(
buildJsonObject {
put(
"callLogs",
buildJsonArray {
callLogs.forEach { add(callLogJson(it)) }
},
)
}.toString(),
)
} catch (err: Throwable) {
GatewaySession.InvokeResult.error(
code = "CALL_LOG_UNAVAILABLE",
message = "CALL_LOG_UNAVAILABLE: ${err.message ?: "call log query failed"}",
)
}
}
private fun parseSearchRequest(paramsJson: String?): CallLogSearchRequest? {
if (paramsJson.isNullOrBlank()) {
return CallLogSearchRequest(
limit = DEFAULT_CALL_LOG_LIMIT,
offset = 0,
cachedName = null,
number = null,
date = null,
dateStart = null,
dateEnd = null,
duration = null,
type = null,
)
}
val params = try {
Json.parseToJsonElement(paramsJson).asObjectOrNull()
} catch (_: Throwable) {
null
} ?: return null
val limit = ((params["limit"] as? JsonPrimitive)?.content?.toIntOrNull() ?: DEFAULT_CALL_LOG_LIMIT)
.coerceIn(1, 200)
val offset = ((params["offset"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 0)
.coerceAtLeast(0)
val cachedName = (params["cachedName"] as? JsonPrimitive)?.content?.takeIf { it.isNotBlank() }
val number = (params["number"] as? JsonPrimitive)?.content?.takeIf { it.isNotBlank() }
val date = (params["date"] as? JsonPrimitive)?.content?.toLongOrNull()
val dateStart = (params["dateStart"] as? JsonPrimitive)?.content?.toLongOrNull()
val dateEnd = (params["dateEnd"] as? JsonPrimitive)?.content?.toLongOrNull()
val duration = (params["duration"] as? JsonPrimitive)?.content?.toLongOrNull()
val type = (params["type"] as? JsonPrimitive)?.content?.toIntOrNull()
return CallLogSearchRequest(
limit = limit,
offset = offset,
cachedName = cachedName,
number = number,
date = date,
dateStart = dateStart,
dateEnd = dateEnd,
duration = duration,
type = type,
)
}
private fun callLogJson(callLog: CallLogRecord): JsonObject {
return buildJsonObject {
put("number", JsonPrimitive(callLog.number))
put("cachedName", JsonPrimitive(callLog.cachedName))
put("date", JsonPrimitive(callLog.date))
put("duration", JsonPrimitive(callLog.duration))
put("type", JsonPrimitive(callLog.type))
}
}
companion object {
internal fun forTesting(
appContext: Context,
dataSource: CallLogDataSource,
): CallLogHandler = CallLogHandler(appContext = appContext, dataSource = dataSource)
}
}

View File

@ -34,7 +34,6 @@ class CanvasController {
@Volatile private var debugStatusEnabled: Boolean = false
@Volatile private var debugStatusTitle: String? = null
@Volatile private var debugStatusSubtitle: String? = null
@Volatile private var homeCanvasStateJson: String? = null
private val _currentUrl = MutableStateFlow<String?>(null)
val currentUrl: StateFlow<String?> = _currentUrl.asStateFlow()
@ -57,7 +56,6 @@ class CanvasController {
this.webView = webView
reload()
applyDebugStatus()
applyHomeCanvasState()
}
fun detach(webView: WebView) {
@ -90,12 +88,6 @@ class CanvasController {
fun onPageFinished() {
applyDebugStatus()
applyHomeCanvasState()
}
fun updateHomeCanvasState(json: String?) {
homeCanvasStateJson = json
applyHomeCanvasState()
}
private inline fun withWebViewOnMain(crossinline block: (WebView) -> Unit) {
@ -150,22 +142,6 @@ class CanvasController {
}
}
private fun applyHomeCanvasState() {
val payload = homeCanvasStateJson ?: "null"
withWebViewOnMain { wv ->
val js = """
(() => {
try {
const api = globalThis.__openclaw;
if (!api || typeof api.renderHome !== 'function') return;
api.renderHome($payload);
} catch (_) {}
})();
""".trimIndent()
wv.evaluateJavascript(js, null)
}
}
suspend fun eval(javaScript: String): String =
withContext(Dispatchers.Main) {
val wv = webView ?: throw IllegalStateException("no webview")

View File

@ -212,13 +212,6 @@ class DeviceHandler(
promptableWhenDenied = true,
),
)
put(
"callLog",
permissionStateJson(
granted = hasPermission(Manifest.permission.READ_CALL_LOG),
promptableWhenDenied = true,
),
)
put(
"motion",
permissionStateJson(

View File

@ -5,7 +5,6 @@ import ai.openclaw.app.protocol.OpenClawCanvasA2UICommand
import ai.openclaw.app.protocol.OpenClawCanvasCommand
import ai.openclaw.app.protocol.OpenClawCameraCommand
import ai.openclaw.app.protocol.OpenClawCapability
import ai.openclaw.app.protocol.OpenClawCallLogCommand
import ai.openclaw.app.protocol.OpenClawContactsCommand
import ai.openclaw.app.protocol.OpenClawDeviceCommand
import ai.openclaw.app.protocol.OpenClawLocationCommand
@ -85,7 +84,6 @@ object InvokeCommandRegistry {
name = OpenClawCapability.Motion.rawValue,
availability = NodeCapabilityAvailability.MotionAvailable,
),
NodeCapabilitySpec(name = OpenClawCapability.CallLog.rawValue),
)
val all: List<InvokeCommandSpec> =
@ -189,9 +187,6 @@ object InvokeCommandRegistry {
name = OpenClawSmsCommand.Send.rawValue,
availability = InvokeCommandAvailability.SmsAvailable,
),
InvokeCommandSpec(
name = OpenClawCallLogCommand.Search.rawValue,
),
InvokeCommandSpec(
name = "debug.logs",
availability = InvokeCommandAvailability.DebugBuild,

View File

@ -5,7 +5,6 @@ import ai.openclaw.app.protocol.OpenClawCalendarCommand
import ai.openclaw.app.protocol.OpenClawCanvasA2UICommand
import ai.openclaw.app.protocol.OpenClawCanvasCommand
import ai.openclaw.app.protocol.OpenClawCameraCommand
import ai.openclaw.app.protocol.OpenClawCallLogCommand
import ai.openclaw.app.protocol.OpenClawContactsCommand
import ai.openclaw.app.protocol.OpenClawDeviceCommand
import ai.openclaw.app.protocol.OpenClawLocationCommand
@ -28,7 +27,6 @@ class InvokeDispatcher(
private val smsHandler: SmsHandler,
private val a2uiHandler: A2UIHandler,
private val debugHandler: DebugHandler,
private val callLogHandler: CallLogHandler,
private val isForeground: () -> Boolean,
private val cameraEnabled: () -> Boolean,
private val locationEnabled: () -> Boolean,
@ -163,9 +161,6 @@ class InvokeDispatcher(
// SMS command
OpenClawSmsCommand.Send.rawValue -> smsHandler.handleSmsSend(paramsJson)
// CallLog command
OpenClawCallLogCommand.Search.rawValue -> callLogHandler.handleCallLogSearch(paramsJson)
// Debug commands
"debug.ed25519" -> debugHandler.handleEd25519()
"debug.logs" -> debugHandler.handleLogs()

View File

@ -13,7 +13,6 @@ enum class OpenClawCapability(val rawValue: String) {
Contacts("contacts"),
Calendar("calendar"),
Motion("motion"),
CallLog("callLog"),
}
enum class OpenClawCanvasCommand(val rawValue: String) {
@ -138,12 +137,3 @@ enum class OpenClawMotionCommand(val rawValue: String) {
const val NamespacePrefix: String = "motion."
}
}
enum class OpenClawCallLogCommand(val rawValue: String) {
Search("callLog.search"),
;
companion object {
const val NamespacePrefix: String = "callLog."
}
}

View File

@ -51,7 +51,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.ui.mobileCardSurface
private enum class ConnectInputMode {
SetupCode,
@ -92,28 +91,20 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
val prompt = pendingTrust!!
AlertDialog(
onDismissRequest = { viewModel.declineGatewayTrustPrompt() },
containerColor = mobileCardSurface,
title = { Text("Trust this gateway?", style = mobileHeadline, color = mobileText) },
title = { Text("Trust this gateway?") },
text = {
Text(
"First-time TLS connection.\n\nVerify this SHA-256 fingerprint before trusting:\n${prompt.fingerprintSha256}",
style = mobileCallout,
color = mobileText,
)
},
confirmButton = {
TextButton(
onClick = { viewModel.acceptGatewayTrustPrompt() },
colors = ButtonDefaults.textButtonColors(contentColor = mobileAccent),
) {
TextButton(onClick = { viewModel.acceptGatewayTrustPrompt() }) {
Text("Trust and continue")
}
},
dismissButton = {
TextButton(
onClick = { viewModel.declineGatewayTrustPrompt() },
colors = ButtonDefaults.textButtonColors(contentColor = mobileTextSecondary),
) {
TextButton(onClick = { viewModel.declineGatewayTrustPrompt() }) {
Text("Cancel")
}
},
@ -153,7 +144,7 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
color = mobileCardSurface,
color = Color.White,
border = BorderStroke(1.dp, mobileBorder),
) {
Column {
@ -214,7 +205,7 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
shape = RoundedCornerShape(14.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = mobileCardSurface,
containerColor = Color.White,
contentColor = mobileDanger,
),
border = BorderStroke(1.dp, mobileDanger.copy(alpha = 0.4f)),
@ -307,7 +298,7 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
color = mobileCardSurface,
color = Color.White,
border = BorderStroke(1.dp, mobileBorder),
) {
Column(
@ -489,7 +480,7 @@ private fun MethodChip(label: String, active: Boolean, onClick: () -> Unit) {
containerColor = if (active) mobileAccent else mobileSurface,
contentColor = if (active) Color.White else mobileText,
),
border = BorderStroke(1.dp, if (active) mobileAccentBorderStrong else mobileBorderStrong),
border = BorderStroke(1.dp, if (active) Color(0xFF184DAF) else mobileBorderStrong),
) {
Text(label, style = mobileCaption1.copy(fontWeight = FontWeight.Bold))
}
@ -518,10 +509,10 @@ private fun CommandBlock(command: String) {
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
color = mobileCodeBg,
border = BorderStroke(1.dp, mobileCodeBorder),
border = BorderStroke(1.dp, Color(0xFF2B2E35)),
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.width(3.dp).height(42.dp).background(mobileCodeAccent))
Box(modifier = Modifier.width(3.dp).height(42.dp).background(Color(0xFF3FC97A)))
Text(
text = command,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),

View File

@ -97,7 +97,7 @@ internal fun parseGatewayEndpoint(rawInput: String): GatewayEndpointConfig? {
"wss", "https" -> true
else -> true
}
val port = uri.port.takeIf { it in 1..65535 } ?: if (tls) 443 else 18789
val port = uri.port.takeIf { it in 1..65535 } ?: 18789
val displayUrl = "${if (tls) "https" else "http"}://$host:$port"
return GatewayEndpointConfig(host = host, port = port, tls = tls, displayUrl = displayUrl)

View File

@ -1,7 +1,5 @@
package ai.openclaw.app.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
@ -11,147 +9,32 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import ai.openclaw.app.R
// ---------------------------------------------------------------------------
// MobileColors semantic color tokens with light + dark variants
// ---------------------------------------------------------------------------
internal data class MobileColors(
val surface: Color,
val surfaceStrong: Color,
val cardSurface: Color,
val border: Color,
val borderStrong: Color,
val text: Color,
val textSecondary: Color,
val textTertiary: Color,
val accent: Color,
val accentSoft: Color,
val accentBorderStrong: Color,
val success: Color,
val successSoft: Color,
val warning: Color,
val warningSoft: Color,
val danger: Color,
val dangerSoft: Color,
val codeBg: Color,
val codeText: Color,
val codeBorder: Color,
val codeAccent: Color,
val chipBorderConnected: Color,
val chipBorderConnecting: Color,
val chipBorderWarning: Color,
val chipBorderError: Color,
)
internal fun lightMobileColors() =
MobileColors(
surface = Color(0xFFF6F7FA),
surfaceStrong = Color(0xFFECEEF3),
cardSurface = Color(0xFFFFFFFF),
border = Color(0xFFE5E7EC),
borderStrong = Color(0xFFD6DAE2),
text = Color(0xFF17181C),
textSecondary = Color(0xFF5D6472),
textTertiary = Color(0xFF99A0AE),
accent = Color(0xFF1D5DD8),
accentSoft = Color(0xFFECF3FF),
accentBorderStrong = Color(0xFF184DAF),
success = Color(0xFF2F8C5A),
successSoft = Color(0xFFEEF9F3),
warning = Color(0xFFC8841A),
warningSoft = Color(0xFFFFF8EC),
danger = Color(0xFFD04B4B),
dangerSoft = Color(0xFFFFF2F2),
codeBg = Color(0xFF15171B),
codeText = Color(0xFFE8EAEE),
codeBorder = Color(0xFF2B2E35),
codeAccent = Color(0xFF3FC97A),
chipBorderConnected = Color(0xFFCFEBD8),
chipBorderConnecting = Color(0xFFD5E2FA),
chipBorderWarning = Color(0xFFEED8B8),
chipBorderError = Color(0xFFF3C8C8),
)
internal fun darkMobileColors() =
MobileColors(
surface = Color(0xFF1A1C20),
surfaceStrong = Color(0xFF24262B),
cardSurface = Color(0xFF1E2024),
border = Color(0xFF2E3038),
borderStrong = Color(0xFF3A3D46),
text = Color(0xFFE4E5EA),
textSecondary = Color(0xFFA0A6B4),
textTertiary = Color(0xFF6B7280),
accent = Color(0xFF6EA8FF),
accentSoft = Color(0xFF1A2A44),
accentBorderStrong = Color(0xFF5B93E8),
success = Color(0xFF5FBB85),
successSoft = Color(0xFF152E22),
warning = Color(0xFFE8A844),
warningSoft = Color(0xFF2E2212),
danger = Color(0xFFE87070),
dangerSoft = Color(0xFF2E1616),
codeBg = Color(0xFF111317),
codeText = Color(0xFFE8EAEE),
codeBorder = Color(0xFF2B2E35),
codeAccent = Color(0xFF3FC97A),
chipBorderConnected = Color(0xFF1E4A30),
chipBorderConnecting = Color(0xFF1E3358),
chipBorderWarning = Color(0xFF3E3018),
chipBorderError = Color(0xFF3E1E1E),
)
internal val LocalMobileColors = staticCompositionLocalOf { lightMobileColors() }
internal object MobileColorsAccessor {
val current: MobileColors
@Composable get() = LocalMobileColors.current
}
// ---------------------------------------------------------------------------
// Backward-compatible top-level accessors (composable getters)
// ---------------------------------------------------------------------------
// These allow existing call sites to keep using `mobileSurface`, `mobileText`, etc.
// without converting every file at once. Each resolves to the themed value.
internal val mobileSurface: Color @Composable get() = LocalMobileColors.current.surface
internal val mobileSurfaceStrong: Color @Composable get() = LocalMobileColors.current.surfaceStrong
internal val mobileCardSurface: Color @Composable get() = LocalMobileColors.current.cardSurface
internal val mobileBorder: Color @Composable get() = LocalMobileColors.current.border
internal val mobileBorderStrong: Color @Composable get() = LocalMobileColors.current.borderStrong
internal val mobileText: Color @Composable get() = LocalMobileColors.current.text
internal val mobileTextSecondary: Color @Composable get() = LocalMobileColors.current.textSecondary
internal val mobileTextTertiary: Color @Composable get() = LocalMobileColors.current.textTertiary
internal val mobileAccent: Color @Composable get() = LocalMobileColors.current.accent
internal val mobileAccentSoft: Color @Composable get() = LocalMobileColors.current.accentSoft
internal val mobileAccentBorderStrong: Color @Composable get() = LocalMobileColors.current.accentBorderStrong
internal val mobileSuccess: Color @Composable get() = LocalMobileColors.current.success
internal val mobileSuccessSoft: Color @Composable get() = LocalMobileColors.current.successSoft
internal val mobileWarning: Color @Composable get() = LocalMobileColors.current.warning
internal val mobileWarningSoft: Color @Composable get() = LocalMobileColors.current.warningSoft
internal val mobileDanger: Color @Composable get() = LocalMobileColors.current.danger
internal val mobileDangerSoft: Color @Composable get() = LocalMobileColors.current.dangerSoft
internal val mobileCodeBg: Color @Composable get() = LocalMobileColors.current.codeBg
internal val mobileCodeText: Color @Composable get() = LocalMobileColors.current.codeText
internal val mobileCodeBorder: Color @Composable get() = LocalMobileColors.current.codeBorder
internal val mobileCodeAccent: Color @Composable get() = LocalMobileColors.current.codeAccent
// Background gradient light fades white→gray, dark fades near-black→dark-gray
internal val mobileBackgroundGradient: Brush
@Composable get() {
val colors = LocalMobileColors.current
return Brush.verticalGradient(
internal val mobileBackgroundGradient =
Brush.verticalGradient(
listOf(
colors.surface,
colors.surfaceStrong,
colors.surfaceStrong,
Color(0xFFFFFFFF),
Color(0xFFF7F8FA),
Color(0xFFEFF1F5),
),
)
}
// ---------------------------------------------------------------------------
// Typography tokens (theme-independent)
// ---------------------------------------------------------------------------
internal val mobileSurface = Color(0xFFF6F7FA)
internal val mobileSurfaceStrong = Color(0xFFECEEF3)
internal val mobileBorder = Color(0xFFE5E7EC)
internal val mobileBorderStrong = Color(0xFFD6DAE2)
internal val mobileText = Color(0xFF17181C)
internal val mobileTextSecondary = Color(0xFF5D6472)
internal val mobileTextTertiary = Color(0xFF99A0AE)
internal val mobileAccent = Color(0xFF1D5DD8)
internal val mobileAccentSoft = Color(0xFFECF3FF)
internal val mobileSuccess = Color(0xFF2F8C5A)
internal val mobileSuccessSoft = Color(0xFFEEF9F3)
internal val mobileWarning = Color(0xFFC8841A)
internal val mobileWarningSoft = Color(0xFFFFF8EC)
internal val mobileDanger = Color(0xFFD04B4B)
internal val mobileDangerSoft = Color(0xFFFFF2F2)
internal val mobileCodeBg = Color(0xFF15171B)
internal val mobileCodeText = Color(0xFFE8EAEE)
internal val mobileFontFamily =
FontFamily(
@ -161,15 +44,6 @@ internal val mobileFontFamily =
Font(resId = R.font.manrope_700_bold, weight = FontWeight.Bold),
)
internal val mobileDisplay =
TextStyle(
fontFamily = mobileFontFamily,
fontWeight = FontWeight.Bold,
fontSize = 34.sp,
lineHeight = 40.sp,
letterSpacing = (-0.8).sp,
)
internal val mobileTitle1 =
TextStyle(
fontFamily = mobileFontFamily,

View File

@ -81,6 +81,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
@ -93,6 +94,7 @@ import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import ai.openclaw.app.LocationMode
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.R
import ai.openclaw.app.node.DeviceNotificationListenerService
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
@ -121,87 +123,101 @@ private enum class PermissionToggle {
Calendar,
Motion,
Sms,
CallLog,
}
private enum class SpecialAccessToggle {
NotificationListener,
}
private val onboardingBackgroundGradient: Brush
@Composable get() = mobileBackgroundGradient
private val onboardingBackgroundGradient =
listOf(
Color(0xFFFFFFFF),
Color(0xFFF7F8FA),
Color(0xFFEFF1F5),
)
private val onboardingSurface = Color(0xFFF6F7FA)
private val onboardingBorder = Color(0xFFE5E7EC)
private val onboardingBorderStrong = Color(0xFFD6DAE2)
private val onboardingText = Color(0xFF17181C)
private val onboardingTextSecondary = Color(0xFF4D5563)
private val onboardingTextTertiary = Color(0xFF8A92A2)
private val onboardingAccent = Color(0xFF1D5DD8)
private val onboardingAccentSoft = Color(0xFFECF3FF)
private val onboardingSuccess = Color(0xFF2F8C5A)
private val onboardingWarning = Color(0xFFC8841A)
private val onboardingCommandBg = Color(0xFF15171B)
private val onboardingCommandBorder = Color(0xFF2B2E35)
private val onboardingCommandAccent = Color(0xFF3FC97A)
private val onboardingCommandText = Color(0xFFE8EAEE)
private val onboardingSurface: Color
@Composable get() = mobileCardSurface
private val onboardingFontFamily =
FontFamily(
Font(resId = R.font.manrope_400_regular, weight = FontWeight.Normal),
Font(resId = R.font.manrope_500_medium, weight = FontWeight.Medium),
Font(resId = R.font.manrope_600_semibold, weight = FontWeight.SemiBold),
Font(resId = R.font.manrope_700_bold, weight = FontWeight.Bold),
)
private val onboardingBorder: Color
@Composable get() = mobileBorder
private val onboardingDisplayStyle =
TextStyle(
fontFamily = onboardingFontFamily,
fontWeight = FontWeight.Bold,
fontSize = 34.sp,
lineHeight = 40.sp,
letterSpacing = (-0.8).sp,
)
private val onboardingBorderStrong: Color
@Composable get() = mobileBorderStrong
private val onboardingTitle1Style =
TextStyle(
fontFamily = onboardingFontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 30.sp,
letterSpacing = (-0.5).sp,
)
private val onboardingText: Color
@Composable get() = mobileText
private val onboardingHeadlineStyle =
TextStyle(
fontFamily = onboardingFontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
lineHeight = 22.sp,
letterSpacing = (-0.1).sp,
)
private val onboardingTextSecondary: Color
@Composable get() = mobileTextSecondary
private val onboardingBodyStyle =
TextStyle(
fontFamily = onboardingFontFamily,
fontWeight = FontWeight.Medium,
fontSize = 15.sp,
lineHeight = 22.sp,
)
private val onboardingTextTertiary: Color
@Composable get() = mobileTextTertiary
private val onboardingCalloutStyle =
TextStyle(
fontFamily = onboardingFontFamily,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
)
private val onboardingAccent: Color
@Composable get() = mobileAccent
private val onboardingCaption1Style =
TextStyle(
fontFamily = onboardingFontFamily,
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.2.sp,
)
private val onboardingAccentSoft: Color
@Composable get() = mobileAccentSoft
private val onboardingAccentBorderStrong: Color
@Composable get() = mobileAccentBorderStrong
private val onboardingSuccess: Color
@Composable get() = mobileSuccess
private val onboardingSuccessSoft: Color
@Composable get() = mobileSuccessSoft
private val onboardingWarning: Color
@Composable get() = mobileWarning
private val onboardingWarningSoft: Color
@Composable get() = mobileWarningSoft
private val onboardingCommandBg: Color
@Composable get() = mobileCodeBg
private val onboardingCommandBorder: Color
@Composable get() = mobileCodeBorder
private val onboardingCommandAccent: Color
@Composable get() = mobileCodeAccent
private val onboardingCommandText: Color
@Composable get() = mobileCodeText
private val onboardingDisplayStyle: TextStyle
get() = mobileDisplay
private val onboardingTitle1Style: TextStyle
get() = mobileTitle1
private val onboardingHeadlineStyle: TextStyle
get() = mobileHeadline
private val onboardingBodyStyle: TextStyle
get() = mobileBody
private val onboardingCalloutStyle: TextStyle
get() = mobileCallout
private val onboardingCaption1Style: TextStyle
get() = mobileCaption1
private val onboardingCaption2Style: TextStyle
get() = mobileCaption2
private val onboardingCaption2Style =
TextStyle(
fontFamily = onboardingFontFamily,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 14.sp,
letterSpacing = 0.4.sp,
)
@Composable
fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
@ -289,10 +305,6 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
rememberSaveable {
mutableStateOf(smsAvailable && isPermissionGranted(context, Manifest.permission.SEND_SMS))
}
var enableCallLog by
rememberSaveable {
mutableStateOf(isPermissionGranted(context, Manifest.permission.READ_CALL_LOG))
}
var pendingPermissionToggle by remember { mutableStateOf<PermissionToggle?>(null) }
var pendingSpecialAccessToggle by remember { mutableStateOf<SpecialAccessToggle?>(null) }
@ -309,7 +321,6 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
PermissionToggle.Calendar -> enableCalendar = enabled
PermissionToggle.Motion -> enableMotion = enabled && motionAvailable
PermissionToggle.Sms -> enableSms = enabled && smsAvailable
PermissionToggle.CallLog -> enableCallLog = enabled
}
}
@ -337,7 +348,6 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
isPermissionGranted(context, Manifest.permission.ACTIVITY_RECOGNITION)
PermissionToggle.Sms ->
!smsAvailable || isPermissionGranted(context, Manifest.permission.SEND_SMS)
PermissionToggle.CallLog -> isPermissionGranted(context, Manifest.permission.READ_CALL_LOG)
}
fun setSpecialAccessToggleEnabled(toggle: SpecialAccessToggle, enabled: Boolean) {
@ -359,7 +369,6 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
enableCalendar,
enableMotion,
enableSms,
enableCallLog,
smsAvailable,
motionAvailable,
) {
@ -375,7 +384,6 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
if (enableCalendar) enabled += "Calendar"
if (enableMotion && motionAvailable) enabled += "Motion"
if (smsAvailable && enableSms) enabled += "SMS"
if (enableCallLog) enabled += "Call Log"
if (enabled.isEmpty()) "None selected" else enabled.joinToString(", ")
}
@ -464,28 +472,19 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
val prompt = pendingTrust!!
AlertDialog(
onDismissRequest = { viewModel.declineGatewayTrustPrompt() },
containerColor = onboardingSurface,
title = { Text("Trust this gateway?", style = onboardingHeadlineStyle, color = onboardingText) },
title = { Text("Trust this gateway?") },
text = {
Text(
"First-time TLS connection.\n\nVerify this SHA-256 fingerprint before trusting:\n${prompt.fingerprintSha256}",
style = onboardingCalloutStyle,
color = onboardingText,
)
},
confirmButton = {
TextButton(
onClick = { viewModel.acceptGatewayTrustPrompt() },
colors = ButtonDefaults.textButtonColors(contentColor = onboardingAccent),
) {
TextButton(onClick = { viewModel.acceptGatewayTrustPrompt() }) {
Text("Trust and continue")
}
},
dismissButton = {
TextButton(
onClick = { viewModel.declineGatewayTrustPrompt() },
colors = ButtonDefaults.textButtonColors(contentColor = onboardingTextSecondary),
) {
TextButton(onClick = { viewModel.declineGatewayTrustPrompt() }) {
Text("Cancel")
}
},
@ -496,7 +495,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
modifier =
modifier
.fillMaxSize()
.background(onboardingBackgroundGradient),
.background(Brush.verticalGradient(onboardingBackgroundGradient)),
) {
Column(
modifier =
@ -604,7 +603,6 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
motionPermissionRequired = motionPermissionRequired,
enableSms = enableSms,
smsAvailable = smsAvailable,
enableCallLog = enableCallLog,
context = context,
onDiscoveryChange = { checked ->
requestPermissionToggle(
@ -702,13 +700,6 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
)
}
},
onCallLogChange = { checked ->
requestPermissionToggle(
PermissionToggle.CallLog,
checked,
listOf(Manifest.permission.READ_CALL_LOG),
)
},
)
OnboardingStep.FinalCheck ->
FinalStep(
@ -764,7 +755,13 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
onClick = { step = OnboardingStep.Gateway },
modifier = Modifier.weight(1f).height(52.dp),
shape = RoundedCornerShape(14.dp),
colors = onboardingPrimaryButtonColors(),
colors =
ButtonDefaults.buttonColors(
containerColor = onboardingAccent,
contentColor = Color.White,
disabledContainerColor = onboardingAccent.copy(alpha = 0.45f),
disabledContentColor = Color.White,
),
) {
Text("Next", style = onboardingHeadlineStyle.copy(fontWeight = FontWeight.Bold))
}
@ -810,7 +807,13 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
},
modifier = Modifier.weight(1f).height(52.dp),
shape = RoundedCornerShape(14.dp),
colors = onboardingPrimaryButtonColors(),
colors =
ButtonDefaults.buttonColors(
containerColor = onboardingAccent,
contentColor = Color.White,
disabledContainerColor = onboardingAccent.copy(alpha = 0.45f),
disabledContentColor = Color.White,
),
) {
Text("Next", style = onboardingHeadlineStyle.copy(fontWeight = FontWeight.Bold))
}
@ -824,7 +827,13 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
},
modifier = Modifier.weight(1f).height(52.dp),
shape = RoundedCornerShape(14.dp),
colors = onboardingPrimaryButtonColors(),
colors =
ButtonDefaults.buttonColors(
containerColor = onboardingAccent,
contentColor = Color.White,
disabledContainerColor = onboardingAccent.copy(alpha = 0.45f),
disabledContentColor = Color.White,
),
) {
Text("Next", style = onboardingHeadlineStyle.copy(fontWeight = FontWeight.Bold))
}
@ -835,7 +844,13 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
onClick = { viewModel.setOnboardingCompleted(true) },
modifier = Modifier.weight(1f).height(52.dp),
shape = RoundedCornerShape(14.dp),
colors = onboardingPrimaryButtonColors(),
colors =
ButtonDefaults.buttonColors(
containerColor = onboardingAccent,
contentColor = Color.White,
disabledContainerColor = onboardingAccent.copy(alpha = 0.45f),
disabledContentColor = Color.White,
),
) {
Text("Finish", style = onboardingHeadlineStyle.copy(fontWeight = FontWeight.Bold))
}
@ -868,7 +883,13 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
},
modifier = Modifier.weight(1f).height(52.dp),
shape = RoundedCornerShape(14.dp),
colors = onboardingPrimaryButtonColors(),
colors =
ButtonDefaults.buttonColors(
containerColor = onboardingAccent,
contentColor = Color.White,
disabledContainerColor = onboardingAccent.copy(alpha = 0.45f),
disabledContentColor = Color.White,
),
) {
Text("Connect", style = onboardingHeadlineStyle.copy(fontWeight = FontWeight.Bold))
}
@ -880,36 +901,6 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
}
}
@Composable
private fun onboardingPrimaryButtonColors() =
ButtonDefaults.buttonColors(
containerColor = onboardingAccent,
contentColor = Color.White,
disabledContainerColor = onboardingAccent.copy(alpha = 0.45f),
disabledContentColor = Color.White.copy(alpha = 0.9f),
)
@Composable
private fun onboardingTextFieldColors() =
OutlinedTextFieldDefaults.colors(
focusedContainerColor = onboardingSurface,
unfocusedContainerColor = onboardingSurface,
focusedBorderColor = onboardingAccent,
unfocusedBorderColor = onboardingBorder,
focusedTextColor = onboardingText,
unfocusedTextColor = onboardingText,
cursorColor = onboardingAccent,
)
@Composable
private fun onboardingSwitchColors() =
SwitchDefaults.colors(
checkedTrackColor = onboardingAccent,
uncheckedTrackColor = onboardingBorderStrong,
checkedThumbColor = Color.White,
uncheckedThumbColor = Color.White,
)
@Composable
private fun StepRail(current: OnboardingStep) {
val steps = OnboardingStep.entries
@ -1014,7 +1005,11 @@ private fun GatewayStep(
onClick = onScanQrClick,
modifier = Modifier.fillMaxWidth().height(48.dp),
shape = RoundedCornerShape(12.dp),
colors = onboardingPrimaryButtonColors(),
colors =
ButtonDefaults.buttonColors(
containerColor = onboardingAccent,
contentColor = Color.White,
),
) {
Text("Scan QR code", style = onboardingHeadlineStyle.copy(fontWeight = FontWeight.Bold))
}
@ -1064,7 +1059,15 @@ private fun GatewayStep(
textStyle = onboardingBodyStyle.copy(fontFamily = FontFamily.Monospace, color = onboardingText),
shape = RoundedCornerShape(14.dp),
colors =
onboardingTextFieldColors(),
OutlinedTextFieldDefaults.colors(
focusedContainerColor = onboardingSurface,
unfocusedContainerColor = onboardingSurface,
focusedBorderColor = onboardingAccent,
unfocusedBorderColor = onboardingBorder,
focusedTextColor = onboardingText,
unfocusedTextColor = onboardingText,
cursorColor = onboardingAccent,
),
)
if (!resolvedEndpoint.isNullOrBlank()) {
ResolvedEndpoint(endpoint = resolvedEndpoint)
@ -1094,7 +1097,15 @@ private fun GatewayStep(
textStyle = onboardingBodyStyle.copy(color = onboardingText),
shape = RoundedCornerShape(14.dp),
colors =
onboardingTextFieldColors(),
OutlinedTextFieldDefaults.colors(
focusedContainerColor = onboardingSurface,
unfocusedContainerColor = onboardingSurface,
focusedBorderColor = onboardingAccent,
unfocusedBorderColor = onboardingBorder,
focusedTextColor = onboardingText,
unfocusedTextColor = onboardingText,
cursorColor = onboardingAccent,
),
)
Text("PORT", style = onboardingCaption1Style.copy(letterSpacing = 0.9.sp), color = onboardingTextSecondary)
@ -1108,7 +1119,15 @@ private fun GatewayStep(
textStyle = onboardingBodyStyle.copy(fontFamily = FontFamily.Monospace, color = onboardingText),
shape = RoundedCornerShape(14.dp),
colors =
onboardingTextFieldColors(),
OutlinedTextFieldDefaults.colors(
focusedContainerColor = onboardingSurface,
unfocusedContainerColor = onboardingSurface,
focusedBorderColor = onboardingAccent,
unfocusedBorderColor = onboardingBorder,
focusedTextColor = onboardingText,
unfocusedTextColor = onboardingText,
cursorColor = onboardingAccent,
),
)
Row(
@ -1124,7 +1143,12 @@ private fun GatewayStep(
checked = manualTls,
onCheckedChange = onManualTlsChange,
colors =
onboardingSwitchColors(),
SwitchDefaults.colors(
checkedTrackColor = onboardingAccent,
uncheckedTrackColor = onboardingBorderStrong,
checkedThumbColor = Color.White,
uncheckedThumbColor = Color.White,
),
)
}
@ -1139,7 +1163,15 @@ private fun GatewayStep(
textStyle = onboardingBodyStyle.copy(color = onboardingText),
shape = RoundedCornerShape(14.dp),
colors =
onboardingTextFieldColors(),
OutlinedTextFieldDefaults.colors(
focusedContainerColor = onboardingSurface,
unfocusedContainerColor = onboardingSurface,
focusedBorderColor = onboardingAccent,
unfocusedBorderColor = onboardingBorder,
focusedTextColor = onboardingText,
unfocusedTextColor = onboardingText,
cursorColor = onboardingAccent,
),
)
Text("PASSWORD (OPTIONAL)", style = onboardingCaption1Style.copy(letterSpacing = 0.9.sp), color = onboardingTextSecondary)
@ -1153,7 +1185,15 @@ private fun GatewayStep(
textStyle = onboardingBodyStyle.copy(color = onboardingText),
shape = RoundedCornerShape(14.dp),
colors =
onboardingTextFieldColors(),
OutlinedTextFieldDefaults.colors(
focusedContainerColor = onboardingSurface,
unfocusedContainerColor = onboardingSurface,
focusedBorderColor = onboardingAccent,
unfocusedBorderColor = onboardingBorder,
focusedTextColor = onboardingText,
unfocusedTextColor = onboardingText,
cursorColor = onboardingAccent,
),
)
if (!manualResolvedEndpoint.isNullOrBlank()) {
@ -1221,7 +1261,7 @@ private fun GatewayModeChip(
containerColor = if (active) onboardingAccent else onboardingSurface,
contentColor = if (active) Color.White else onboardingText,
),
border = androidx.compose.foundation.BorderStroke(1.dp, if (active) onboardingAccentBorderStrong else onboardingBorderStrong),
border = androidx.compose.foundation.BorderStroke(1.dp, if (active) Color(0xFF184DAF) else onboardingBorderStrong),
) {
Text(
text = label,
@ -1299,7 +1339,6 @@ private fun PermissionsStep(
motionPermissionRequired: Boolean,
enableSms: Boolean,
smsAvailable: Boolean,
enableCallLog: Boolean,
context: Context,
onDiscoveryChange: (Boolean) -> Unit,
onLocationChange: (Boolean) -> Unit,
@ -1312,7 +1351,6 @@ private fun PermissionsStep(
onCalendarChange: (Boolean) -> Unit,
onMotionChange: (Boolean) -> Unit,
onSmsChange: (Boolean) -> Unit,
onCallLogChange: (Boolean) -> Unit,
) {
val discoveryPermission = if (Build.VERSION.SDK_INT >= 33) Manifest.permission.NEARBY_WIFI_DEVICES else Manifest.permission.ACCESS_FINE_LOCATION
val locationGranted =
@ -1443,15 +1481,6 @@ private fun PermissionsStep(
onCheckedChange = onSmsChange,
)
}
InlineDivider()
PermissionToggleRow(
title = "Call Log",
subtitle = "callLog.search",
checked = enableCallLog,
granted = isPermissionGranted(context, Manifest.permission.READ_CALL_LOG),
onCheckedChange = onCallLogChange,
)
Text("All settings can be changed later in Settings.", style = onboardingCalloutStyle, color = onboardingTextSecondary)
}
}
@ -1495,7 +1524,13 @@ private fun PermissionToggleRow(
checked = checked,
onCheckedChange = onCheckedChange,
enabled = enabled,
colors = onboardingSwitchColors(),
colors =
SwitchDefaults.colors(
checkedTrackColor = onboardingAccent,
uncheckedTrackColor = onboardingBorderStrong,
checkedThumbColor = Color.White,
uncheckedThumbColor = Color.White,
),
)
}
}
@ -1570,7 +1605,7 @@ private fun FinalStep(
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
color = onboardingSuccessSoft,
color = Color(0xFFEEF9F3),
border = androidx.compose.foundation.BorderStroke(1.dp, onboardingSuccess.copy(alpha = 0.2f)),
) {
Row(
@ -1606,7 +1641,7 @@ private fun FinalStep(
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
color = onboardingWarningSoft,
color = Color(0xFFFFF8EC),
border = androidx.compose.foundation.BorderStroke(1.dp, onboardingWarning.copy(alpha = 0.2f)),
) {
Column(

View File

@ -5,7 +5,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
@ -14,12 +13,9 @@ fun OpenClawTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val isDark = isSystemInDarkTheme()
val colorScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
val mobileColors = if (isDark) darkMobileColors() else lightMobileColors()
CompositionLocalProvider(LocalMobileColors provides mobileColors) {
MaterialTheme(colorScheme = colorScheme, content = content)
}
}
@Composable
fun overlayContainerColor(): Color {

View File

@ -134,14 +134,43 @@ fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier)
@Composable
private fun ScreenTabScreen(viewModel: MainViewModel) {
val isConnected by viewModel.isConnected.collectAsState()
LaunchedEffect(isConnected) {
if (isConnected) {
viewModel.refreshHomeCanvasOverviewIfConnected()
}
val isNodeConnected by viewModel.isNodeConnected.collectAsState()
val canvasUrl by viewModel.canvasCurrentUrl.collectAsState()
val canvasA2uiHydrated by viewModel.canvasA2uiHydrated.collectAsState()
val canvasRehydratePending by viewModel.canvasRehydratePending.collectAsState()
val canvasRehydrateErrorText by viewModel.canvasRehydrateErrorText.collectAsState()
val isA2uiUrl = canvasUrl?.contains("/__openclaw__/a2ui/") == true
val showRestoreCta = isConnected && isNodeConnected && (canvasUrl.isNullOrBlank() || (isA2uiUrl && !canvasA2uiHydrated))
val restoreCtaText =
when {
canvasRehydratePending -> "Restore requested. Waiting for agent…"
!canvasRehydrateErrorText.isNullOrBlank() -> canvasRehydrateErrorText!!
else -> "Canvas reset. Tap to restore dashboard."
}
Box(modifier = Modifier.fillMaxSize()) {
CanvasScreen(viewModel = viewModel, modifier = Modifier.fillMaxSize())
if (showRestoreCta) {
Surface(
onClick = {
if (canvasRehydratePending) return@Surface
viewModel.requestCanvasRehydrate(source = "screen_tab_cta")
},
modifier = Modifier.align(Alignment.TopCenter).padding(horizontal = 16.dp, vertical = 16.dp),
shape = RoundedCornerShape(12.dp),
color = mobileSurface.copy(alpha = 0.9f),
border = BorderStroke(1.dp, mobileBorder),
shadowElevation = 4.dp,
) {
Text(
text = restoreCtaText,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
style = mobileCallout.copy(fontWeight = FontWeight.Medium),
color = mobileText,
)
}
}
}
}
@ -159,28 +188,28 @@ private fun TopStatusBar(
mobileSuccessSoft,
mobileSuccess,
mobileSuccess,
LocalMobileColors.current.chipBorderConnected,
Color(0xFFCFEBD8),
)
StatusVisual.Connecting ->
listOf(
mobileAccentSoft,
mobileAccent,
mobileAccent,
LocalMobileColors.current.chipBorderConnecting,
Color(0xFFD5E2FA),
)
StatusVisual.Warning ->
listOf(
mobileWarningSoft,
mobileWarning,
mobileWarning,
LocalMobileColors.current.chipBorderWarning,
Color(0xFFEED8B8),
)
StatusVisual.Error ->
listOf(
mobileDangerSoft,
mobileDanger,
mobileDanger,
LocalMobileColors.current.chipBorderError,
Color(0xFFF3C8C8),
)
StatusVisual.Offline ->
listOf(
@ -249,7 +278,7 @@ private fun BottomTabBar(
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = mobileCardSurface.copy(alpha = 0.97f),
color = Color.White.copy(alpha = 0.97f),
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
border = BorderStroke(1.dp, mobileBorder),
shadowElevation = 6.dp,
@ -270,7 +299,7 @@ private fun BottomTabBar(
modifier = Modifier.weight(1f).heightIn(min = 58.dp),
shape = RoundedCornerShape(16.dp),
color = if (active) mobileAccentSoft else Color.Transparent,
border = if (active) BorderStroke(1.dp, LocalMobileColors.current.chipBorderConnecting) else null,
border = if (active) BorderStroke(1.dp, Color(0xFFD5E2FA)) else null,
shadowElevation = 0.dp,
) {
Column(

View File

@ -218,18 +218,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
calendarPermissionGranted = readOk && writeOk
}
var callLogPermissionGranted by
remember {
mutableStateOf(
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALL_LOG) ==
PackageManager.PERMISSION_GRANTED,
)
}
val callLogPermissionLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
callLogPermissionGranted = granted
}
var motionPermissionGranted by
remember {
mutableStateOf(
@ -278,9 +266,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR) ==
PackageManager.PERMISSION_GRANTED
callLogPermissionGranted =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALL_LOG) ==
PackageManager.PERMISSION_GRANTED
motionPermissionGranted =
!motionPermissionRequired ||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) ==
@ -616,31 +601,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
}
},
)
HorizontalDivider(color = mobileBorder)
ListItem(
modifier = Modifier.fillMaxWidth(),
colors = listItemColors,
headlineContent = { Text("Call Log", style = mobileHeadline) },
supportingContent = { Text("Search recent call history.", style = mobileCallout) },
trailingContent = {
Button(
onClick = {
if (callLogPermissionGranted) {
openAppSettings(context)
} else {
callLogPermissionLauncher.launch(Manifest.permission.READ_CALL_LOG)
}
},
colors = settingsPrimaryButtonColors(),
shape = RoundedCornerShape(14.dp),
) {
Text(
if (callLogPermissionGranted) "Manage" else "Grant",
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
)
}
},
)
if (motionAvailable) {
HorizontalDivider(color = mobileBorder)
ListItem(
@ -776,12 +736,11 @@ private fun settingsTextFieldColors() =
cursorColor = mobileAccent,
)
@Composable
private fun Modifier.settingsRowModifier() =
this
.fillMaxWidth()
.border(width = 1.dp, color = mobileBorder, shape = RoundedCornerShape(14.dp))
.background(mobileCardSurface, RoundedCornerShape(14.dp))
.background(Color.White, RoundedCornerShape(14.dp))
@Composable
private fun settingsPrimaryButtonColors() =

View File

@ -363,7 +363,7 @@ private fun VoiceTurnBubble(entry: VoiceConversationEntry) {
Surface(
modifier = Modifier.fillMaxWidth(0.90f),
shape = RoundedCornerShape(12.dp),
color = if (isUser) mobileAccentSoft else mobileCardSurface,
color = if (isUser) mobileAccentSoft else Color.White,
border = BorderStroke(1.dp, if (isUser) mobileAccent else mobileBorderStrong),
) {
Column(
@ -391,7 +391,7 @@ private fun VoiceThinkingBubble() {
Surface(
modifier = Modifier.fillMaxWidth(0.68f),
shape = RoundedCornerShape(12.dp),
color = mobileCardSurface,
color = Color.White,
border = BorderStroke(1.dp, mobileBorderStrong),
) {
Row(

View File

@ -46,13 +46,11 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ai.openclaw.app.ui.mobileAccent
import ai.openclaw.app.ui.mobileAccentBorderStrong
import ai.openclaw.app.ui.mobileAccentSoft
import ai.openclaw.app.ui.mobileBorder
import ai.openclaw.app.ui.mobileBorderStrong
import ai.openclaw.app.ui.mobileCallout
import ai.openclaw.app.ui.mobileCaption1
import ai.openclaw.app.ui.mobileCardSurface
import ai.openclaw.app.ui.mobileHeadline
import ai.openclaw.app.ui.mobileSurface
import ai.openclaw.app.ui.mobileText
@ -112,7 +110,7 @@ fun ChatComposer(
Surface(
onClick = { showThinkingMenu = true },
shape = RoundedCornerShape(14.dp),
color = mobileCardSurface,
color = Color.White,
border = BorderStroke(1.dp, mobileBorderStrong),
) {
Row(
@ -128,15 +126,7 @@ fun ChatComposer(
}
}
DropdownMenu(
expanded = showThinkingMenu,
onDismissRequest = { showThinkingMenu = false },
shape = RoundedCornerShape(16.dp),
containerColor = mobileCardSurface,
tonalElevation = 0.dp,
shadowElevation = 8.dp,
border = BorderStroke(1.dp, mobileBorder),
) {
DropdownMenu(expanded = showThinkingMenu, onDismissRequest = { showThinkingMenu = false }) {
ThinkingMenuItem("off", thinkingLevel, onSetThinkingLevel) { showThinkingMenu = false }
ThinkingMenuItem("low", thinkingLevel, onSetThinkingLevel) { showThinkingMenu = false }
ThinkingMenuItem("medium", thinkingLevel, onSetThinkingLevel) { showThinkingMenu = false }
@ -187,7 +177,7 @@ fun ChatComposer(
disabledContainerColor = mobileBorderStrong,
disabledContentColor = mobileTextTertiary,
),
border = BorderStroke(1.dp, if (canSend) mobileAccentBorderStrong else mobileBorderStrong),
border = BorderStroke(1.dp, if (canSend) Color(0xFF154CAD) else mobileBorderStrong),
) {
if (sendBusy) {
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp, color = Color.White)
@ -221,9 +211,9 @@ private fun SecondaryActionButton(
shape = RoundedCornerShape(14.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = mobileCardSurface,
containerColor = Color.White,
contentColor = mobileTextSecondary,
disabledContainerColor = mobileCardSurface,
disabledContainerColor = Color.White,
disabledContentColor = mobileTextTertiary,
),
border = BorderStroke(1.dp, mobileBorderStrong),
@ -313,7 +303,7 @@ private fun AttachmentChip(fileName: String, onRemove: () -> Unit) {
Surface(
onClick = onRemove,
shape = RoundedCornerShape(999.dp),
color = mobileCardSurface,
color = Color.White,
border = BorderStroke(1.dp, mobileBorderStrong),
) {
Text(

View File

@ -94,7 +94,7 @@ private val markdownParser: Parser by lazy {
@Composable
fun ChatMarkdown(text: String, textColor: Color) {
val document = remember(text) { markdownParser.parse(text) as Document }
val inlineStyles = InlineStyles(inlineCodeBg = mobileCodeBg, inlineCodeColor = mobileCodeText, linkColor = mobileAccent, baseCallout = mobileCallout)
val inlineStyles = InlineStyles(inlineCodeBg = mobileCodeBg, inlineCodeColor = mobileCodeText)
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
RenderMarkdownBlocks(
@ -124,7 +124,7 @@ private fun RenderMarkdownBlocks(
val headingText = remember(current) { buildInlineMarkdown(current.firstChild, inlineStyles) }
Text(
text = headingText,
style = headingStyle(current.level, inlineStyles.baseCallout),
style = headingStyle(current.level),
color = textColor,
)
}
@ -231,7 +231,7 @@ private fun RenderParagraph(
Text(
text = annotated,
style = inlineStyles.baseCallout,
style = mobileCallout,
color = textColor,
)
}
@ -315,7 +315,7 @@ private fun RenderListItem(
) {
Text(
text = marker,
style = inlineStyles.baseCallout.copy(fontWeight = FontWeight.SemiBold),
style = mobileCallout.copy(fontWeight = FontWeight.SemiBold),
color = textColor,
modifier = Modifier.width(24.dp),
)
@ -360,7 +360,7 @@ private fun RenderTableBlock(
val cell = row.cells.getOrNull(index) ?: AnnotatedString("")
Text(
text = cell,
style = if (row.isHeader) mobileCaption1.copy(fontWeight = FontWeight.SemiBold) else inlineStyles.baseCallout,
style = if (row.isHeader) mobileCaption1.copy(fontWeight = FontWeight.SemiBold) else mobileCallout,
color = textColor,
modifier = Modifier
.border(1.dp, mobileTextSecondary.copy(alpha = 0.22f))
@ -417,7 +417,6 @@ private fun buildInlineMarkdown(start: Node?, inlineStyles: InlineStyles): Annot
node = start,
inlineCodeBg = inlineStyles.inlineCodeBg,
inlineCodeColor = inlineStyles.inlineCodeColor,
linkColor = inlineStyles.linkColor,
)
}
}
@ -426,7 +425,6 @@ private fun AnnotatedString.Builder.appendInlineNode(
node: Node?,
inlineCodeBg: Color,
inlineCodeColor: Color,
linkColor: Color,
) {
var current = node
while (current != null) {
@ -447,27 +445,27 @@ private fun AnnotatedString.Builder.appendInlineNode(
}
is Emphasis -> {
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
appendInlineNode(current.firstChild, inlineCodeBg = inlineCodeBg, inlineCodeColor = inlineCodeColor, linkColor = linkColor)
appendInlineNode(current.firstChild, inlineCodeBg = inlineCodeBg, inlineCodeColor = inlineCodeColor)
}
}
is StrongEmphasis -> {
withStyle(SpanStyle(fontWeight = FontWeight.SemiBold)) {
appendInlineNode(current.firstChild, inlineCodeBg = inlineCodeBg, inlineCodeColor = inlineCodeColor, linkColor = linkColor)
appendInlineNode(current.firstChild, inlineCodeBg = inlineCodeBg, inlineCodeColor = inlineCodeColor)
}
}
is Strikethrough -> {
withStyle(SpanStyle(textDecoration = TextDecoration.LineThrough)) {
appendInlineNode(current.firstChild, inlineCodeBg = inlineCodeBg, inlineCodeColor = inlineCodeColor, linkColor = linkColor)
appendInlineNode(current.firstChild, inlineCodeBg = inlineCodeBg, inlineCodeColor = inlineCodeColor)
}
}
is Link -> {
withStyle(
SpanStyle(
color = linkColor,
color = mobileAccent,
textDecoration = TextDecoration.Underline,
),
) {
appendInlineNode(current.firstChild, inlineCodeBg = inlineCodeBg, inlineCodeColor = inlineCodeColor, linkColor = linkColor)
appendInlineNode(current.firstChild, inlineCodeBg = inlineCodeBg, inlineCodeColor = inlineCodeColor)
}
}
is MarkdownImage -> {
@ -484,7 +482,7 @@ private fun AnnotatedString.Builder.appendInlineNode(
}
}
else -> {
appendInlineNode(current.firstChild, inlineCodeBg = inlineCodeBg, inlineCodeColor = inlineCodeColor, linkColor = linkColor)
appendInlineNode(current.firstChild, inlineCodeBg = inlineCodeBg, inlineCodeColor = inlineCodeColor)
}
}
current = current.next
@ -521,21 +519,19 @@ private fun parseDataImageDestination(destination: String?): ParsedDataImage? {
return ParsedDataImage(mimeType = "image/$subtype", base64 = base64)
}
private fun headingStyle(level: Int, baseCallout: TextStyle): TextStyle {
private fun headingStyle(level: Int): TextStyle {
return when (level.coerceIn(1, 6)) {
1 -> baseCallout.copy(fontSize = 22.sp, lineHeight = 28.sp, fontWeight = FontWeight.Bold)
2 -> baseCallout.copy(fontSize = 20.sp, lineHeight = 26.sp, fontWeight = FontWeight.Bold)
3 -> baseCallout.copy(fontSize = 18.sp, lineHeight = 24.sp, fontWeight = FontWeight.SemiBold)
4 -> baseCallout.copy(fontSize = 16.sp, lineHeight = 22.sp, fontWeight = FontWeight.SemiBold)
else -> baseCallout.copy(fontWeight = FontWeight.SemiBold)
1 -> mobileCallout.copy(fontSize = 22.sp, lineHeight = 28.sp, fontWeight = FontWeight.Bold)
2 -> mobileCallout.copy(fontSize = 20.sp, lineHeight = 26.sp, fontWeight = FontWeight.Bold)
3 -> mobileCallout.copy(fontSize = 18.sp, lineHeight = 24.sp, fontWeight = FontWeight.SemiBold)
4 -> mobileCallout.copy(fontSize = 16.sp, lineHeight = 22.sp, fontWeight = FontWeight.SemiBold)
else -> mobileCallout.copy(fontWeight = FontWeight.SemiBold)
}
}
private data class InlineStyles(
val inlineCodeBg: Color,
val inlineCodeColor: Color,
val linkColor: Color,
val baseCallout: TextStyle,
)
private data class TableRenderRow(

View File

@ -19,7 +19,6 @@ import ai.openclaw.app.chat.ChatMessage
import ai.openclaw.app.chat.ChatPendingToolCall
import ai.openclaw.app.ui.mobileBorder
import ai.openclaw.app.ui.mobileCallout
import ai.openclaw.app.ui.mobileCardSurface
import ai.openclaw.app.ui.mobileHeadline
import ai.openclaw.app.ui.mobileText
import ai.openclaw.app.ui.mobileTextSecondary
@ -86,7 +85,7 @@ private fun EmptyChatHint(modifier: Modifier = Modifier, healthOk: Boolean) {
Surface(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
color = mobileCardSurface.copy(alpha = 0.9f),
color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.9f),
border = androidx.compose.foundation.BorderStroke(1.dp, mobileBorder),
) {
androidx.compose.foundation.layout.Column(

View File

@ -36,9 +36,7 @@ import ai.openclaw.app.ui.mobileBorderStrong
import ai.openclaw.app.ui.mobileCallout
import ai.openclaw.app.ui.mobileCaption1
import ai.openclaw.app.ui.mobileCaption2
import ai.openclaw.app.ui.mobileCardSurface
import ai.openclaw.app.ui.mobileCodeBg
import ai.openclaw.app.ui.mobileCodeBorder
import ai.openclaw.app.ui.mobileCodeText
import ai.openclaw.app.ui.mobileHeadline
import ai.openclaw.app.ui.mobileText
@ -196,7 +194,6 @@ fun ChatStreamingAssistantBubble(text: String) {
}
}
@Composable
private fun bubbleStyle(role: String): ChatBubbleStyle {
return when (role) {
"user" ->
@ -218,7 +215,7 @@ private fun bubbleStyle(role: String): ChatBubbleStyle {
else ->
ChatBubbleStyle(
alignEnd = false,
containerColor = mobileCardSurface,
containerColor = Color.White,
borderColor = mobileBorderStrong,
roleColor = mobileTextSecondary,
)
@ -242,7 +239,7 @@ private fun ChatBase64Image(base64: String, mimeType: String?) {
Surface(
shape = RoundedCornerShape(10.dp),
border = BorderStroke(1.dp, mobileBorder),
color = mobileCardSurface,
color = Color.White,
modifier = Modifier.fillMaxWidth(),
) {
Image(
@ -280,7 +277,7 @@ fun ChatCodeBlock(code: String, language: String?) {
Surface(
shape = RoundedCornerShape(8.dp),
color = mobileCodeBg,
border = BorderStroke(1.dp, mobileCodeBorder),
border = BorderStroke(1.dp, Color(0xFF2B2E35)),
modifier = Modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {

View File

@ -36,15 +36,12 @@ import ai.openclaw.app.MainViewModel
import ai.openclaw.app.chat.ChatSessionEntry
import ai.openclaw.app.chat.OutgoingAttachment
import ai.openclaw.app.ui.mobileAccent
import ai.openclaw.app.ui.mobileAccentBorderStrong
import ai.openclaw.app.ui.mobileBorder
import ai.openclaw.app.ui.mobileBorderStrong
import ai.openclaw.app.ui.mobileCallout
import ai.openclaw.app.ui.mobileCardSurface
import ai.openclaw.app.ui.mobileCaption1
import ai.openclaw.app.ui.mobileCaption2
import ai.openclaw.app.ui.mobileDanger
import ai.openclaw.app.ui.mobileDangerSoft
import ai.openclaw.app.ui.mobileText
import ai.openclaw.app.ui.mobileTextSecondary
import java.io.ByteArrayOutputStream
@ -171,8 +168,8 @@ private fun ChatThreadSelector(
Surface(
onClick = { onSelectSession(entry.key) },
shape = RoundedCornerShape(14.dp),
color = if (active) mobileAccent else mobileCardSurface,
border = BorderStroke(1.dp, if (active) mobileAccentBorderStrong else mobileBorderStrong),
color = if (active) mobileAccent else Color.White,
border = BorderStroke(1.dp, if (active) Color(0xFF154CAD) else mobileBorderStrong),
tonalElevation = 0.dp,
shadowElevation = 0.dp,
) {
@ -193,7 +190,7 @@ private fun ChatThreadSelector(
private fun ChatErrorRail(errorText: String) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = mobileDangerSoft,
color = androidx.compose.ui.graphics.Color.White,
shape = RoundedCornerShape(12.dp),
border = androidx.compose.foundation.BorderStroke(1.dp, mobileDanger),
) {

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.OpenClawNode" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item>
</style>
</resources>

View File

@ -1,193 +0,0 @@
package ai.openclaw.app.node
import android.content.Context
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class CallLogHandlerTest : NodeHandlerRobolectricTest() {
@Test
fun handleCallLogSearch_requiresPermission() {
val handler = CallLogHandler.forTesting(appContext(), FakeCallLogDataSource(canRead = false))
val result = handler.handleCallLogSearch(null)
assertFalse(result.ok)
assertEquals("CALL_LOG_PERMISSION_REQUIRED", result.error?.code)
}
@Test
fun handleCallLogSearch_rejectsInvalidJson() {
val handler = CallLogHandler.forTesting(appContext(), FakeCallLogDataSource(canRead = true))
val result = handler.handleCallLogSearch("invalid json")
assertFalse(result.ok)
assertEquals("INVALID_REQUEST", result.error?.code)
}
@Test
fun handleCallLogSearch_returnsCallLogs() {
val callLog =
CallLogRecord(
number = "+123456",
cachedName = "lixuankai",
date = 1709280000000L,
duration = 60L,
type = 1,
)
val handler =
CallLogHandler.forTesting(
appContext(),
FakeCallLogDataSource(canRead = true, searchResults = listOf(callLog)),
)
val result = handler.handleCallLogSearch("""{"limit":1}""")
assertTrue(result.ok)
val payload = Json.parseToJsonElement(result.payloadJson ?: error("missing payload")).jsonObject
val callLogs = payload.getValue("callLogs").jsonArray
assertEquals(1, callLogs.size)
assertEquals("+123456", callLogs.first().jsonObject.getValue("number").jsonPrimitive.content)
assertEquals("lixuankai", callLogs.first().jsonObject.getValue("cachedName").jsonPrimitive.content)
assertEquals(1709280000000L, callLogs.first().jsonObject.getValue("date").jsonPrimitive.content.toLong())
assertEquals(60L, callLogs.first().jsonObject.getValue("duration").jsonPrimitive.content.toLong())
assertEquals(1, callLogs.first().jsonObject.getValue("type").jsonPrimitive.content.toInt())
}
@Test
fun handleCallLogSearch_withFilters() {
val callLog =
CallLogRecord(
number = "+123456",
cachedName = "lixuankai",
date = 1709280000000L,
duration = 120L,
type = 2,
)
val handler =
CallLogHandler.forTesting(
appContext(),
FakeCallLogDataSource(canRead = true, searchResults = listOf(callLog)),
)
val result = handler.handleCallLogSearch(
"""{"number":"123456","cachedName":"lixuankai","dateStart":1709270000000,"dateEnd":1709290000000,"duration":120,"type":2}"""
)
assertTrue(result.ok)
val payload = Json.parseToJsonElement(result.payloadJson ?: error("missing payload")).jsonObject
val callLogs = payload.getValue("callLogs").jsonArray
assertEquals(1, callLogs.size)
assertEquals("lixuankai", callLogs.first().jsonObject.getValue("cachedName").jsonPrimitive.content)
}
@Test
fun handleCallLogSearch_withPagination() {
val callLogs =
listOf(
CallLogRecord(
number = "+123456",
cachedName = "lixuankai",
date = 1709280000000L,
duration = 60L,
type = 1,
),
CallLogRecord(
number = "+654321",
cachedName = "lixuankai2",
date = 1709280001000L,
duration = 120L,
type = 2,
),
)
val handler =
CallLogHandler.forTesting(
appContext(),
FakeCallLogDataSource(canRead = true, searchResults = callLogs),
)
val result = handler.handleCallLogSearch("""{"limit":1,"offset":1}""")
assertTrue(result.ok)
val payload = Json.parseToJsonElement(result.payloadJson ?: error("missing payload")).jsonObject
val callLogsResult = payload.getValue("callLogs").jsonArray
assertEquals(1, callLogsResult.size)
assertEquals("lixuankai2", callLogsResult.first().jsonObject.getValue("cachedName").jsonPrimitive.content)
}
@Test
fun handleCallLogSearch_withDefaultParams() {
val callLog =
CallLogRecord(
number = "+123456",
cachedName = "lixuankai",
date = 1709280000000L,
duration = 60L,
type = 1,
)
val handler =
CallLogHandler.forTesting(
appContext(),
FakeCallLogDataSource(canRead = true, searchResults = listOf(callLog)),
)
val result = handler.handleCallLogSearch(null)
assertTrue(result.ok)
val payload = Json.parseToJsonElement(result.payloadJson ?: error("missing payload")).jsonObject
val callLogs = payload.getValue("callLogs").jsonArray
assertEquals(1, callLogs.size)
assertEquals("+123456", callLogs.first().jsonObject.getValue("number").jsonPrimitive.content)
}
@Test
fun handleCallLogSearch_withNullFields() {
val callLog =
CallLogRecord(
number = null,
cachedName = null,
date = 1709280000000L,
duration = 60L,
type = 1,
)
val handler =
CallLogHandler.forTesting(
appContext(),
FakeCallLogDataSource(canRead = true, searchResults = listOf(callLog)),
)
val result = handler.handleCallLogSearch("""{"limit":1}""")
assertTrue(result.ok)
val payload = Json.parseToJsonElement(result.payloadJson ?: error("missing payload")).jsonObject
val callLogs = payload.getValue("callLogs").jsonArray
assertEquals(1, callLogs.size)
// Verify null values are properly serialized
val callLogObj = callLogs.first().jsonObject
assertTrue(callLogObj.containsKey("number"))
assertTrue(callLogObj.containsKey("cachedName"))
}
}
private class FakeCallLogDataSource(
private val canRead: Boolean,
private val searchResults: List<CallLogRecord> = emptyList(),
) : CallLogDataSource {
override fun hasReadPermission(context: Context): Boolean = canRead
override fun search(context: Context, request: CallLogSearchRequest): List<CallLogRecord> {
val startIndex = request.offset.coerceAtLeast(0)
val endIndex = (startIndex + request.limit).coerceAtMost(searchResults.size)
return if (startIndex < searchResults.size) {
searchResults.subList(startIndex, endIndex)
} else {
emptyList()
}
}
}

View File

@ -93,7 +93,6 @@ class DeviceHandlerTest {
"photos",
"contacts",
"calendar",
"callLog",
"motion",
)
for (key in expected) {

View File

@ -2,7 +2,6 @@ package ai.openclaw.app.node
import ai.openclaw.app.protocol.OpenClawCalendarCommand
import ai.openclaw.app.protocol.OpenClawCameraCommand
import ai.openclaw.app.protocol.OpenClawCallLogCommand
import ai.openclaw.app.protocol.OpenClawCapability
import ai.openclaw.app.protocol.OpenClawContactsCommand
import ai.openclaw.app.protocol.OpenClawDeviceCommand
@ -26,7 +25,6 @@ class InvokeCommandRegistryTest {
OpenClawCapability.Photos.rawValue,
OpenClawCapability.Contacts.rawValue,
OpenClawCapability.Calendar.rawValue,
OpenClawCapability.CallLog.rawValue,
)
private val optionalCapabilities =
@ -52,7 +50,6 @@ class InvokeCommandRegistryTest {
OpenClawContactsCommand.Add.rawValue,
OpenClawCalendarCommand.Events.rawValue,
OpenClawCalendarCommand.Add.rawValue,
OpenClawCallLogCommand.Search.rawValue,
)
private val optionalCommands =

View File

@ -34,7 +34,6 @@ class OpenClawProtocolConstantsTest {
assertEquals("contacts", OpenClawCapability.Contacts.rawValue)
assertEquals("calendar", OpenClawCapability.Calendar.rawValue)
assertEquals("motion", OpenClawCapability.Motion.rawValue)
assertEquals("callLog", OpenClawCapability.CallLog.rawValue)
}
@Test
@ -85,9 +84,4 @@ class OpenClawProtocolConstantsTest {
assertEquals("motion.activity", OpenClawMotionCommand.Activity.rawValue)
assertEquals("motion.pedometer", OpenClawMotionCommand.Pedometer.rawValue)
}
@Test
fun callLogCommandsUseStableStrings() {
assertEquals("callLog.search", OpenClawCallLogCommand.Search.rawValue)
}
}

View File

@ -92,30 +92,6 @@ class GatewayConfigResolverTest {
assertNull(resolved?.password?.takeIf { it.isNotEmpty() })
}
@Test
fun resolveGatewayConnectConfigDefaultsPortlessWssSetupCodeTo443() {
val setupCode =
encodeSetupCode("""{"url":"wss://gateway.example","bootstrapToken":"bootstrap-1"}""")
val resolved =
resolveGatewayConnectConfig(
useSetupCode = true,
setupCode = setupCode,
manualHost = "",
manualPort = "",
manualTls = true,
fallbackToken = "shared-token",
fallbackPassword = "shared-password",
)
assertEquals("gateway.example", resolved?.host)
assertEquals(443, resolved?.port)
assertEquals(true, resolved?.tls)
assertEquals("bootstrap-1", resolved?.bootstrapToken)
assertNull(resolved?.token?.takeIf { it.isNotEmpty() })
assertNull(resolved?.password?.takeIf { it.isNotEmpty() })
}
private fun encodeSetupCode(payloadJson: String): String {
return Base64.getUrlEncoder().withoutPadding().encodeToString(payloadJson.toByteArray(Charsets.UTF_8))
}

View File

@ -1,125 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const androidDir = join(scriptDir, "..");
const buildGradlePath = join(androidDir, "app", "build.gradle.kts");
const bundlePath = join(androidDir, "app", "build", "outputs", "bundle", "release", "app-release.aab");
type VersionState = {
versionName: string;
versionCode: number;
};
type ParsedVersionMatches = {
versionNameMatch: RegExpMatchArray;
versionCodeMatch: RegExpMatchArray;
};
function formatVersionName(date: Date): string {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}.${month}.${day}`;
}
function formatVersionCodePrefix(date: Date): string {
const year = date.getFullYear().toString();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return `${year}${month}${day}`;
}
function parseVersionMatches(buildGradleText: string): ParsedVersionMatches {
const versionCodeMatch = buildGradleText.match(/versionCode = (\d+)/);
const versionNameMatch = buildGradleText.match(/versionName = "([^"]+)"/);
if (!versionCodeMatch || !versionNameMatch) {
throw new Error(`Couldn't parse versionName/versionCode from ${buildGradlePath}`);
}
return { versionCodeMatch, versionNameMatch };
}
function resolveNextVersionCode(currentVersionCode: number, todayPrefix: string): number {
const currentRaw = currentVersionCode.toString();
let nextSuffix = 0;
if (currentRaw.startsWith(todayPrefix)) {
const suffixRaw = currentRaw.slice(todayPrefix.length);
nextSuffix = (suffixRaw ? Number.parseInt(suffixRaw, 10) : 0) + 1;
}
if (!Number.isInteger(nextSuffix) || nextSuffix < 0 || nextSuffix > 99) {
throw new Error(
`Can't auto-bump Android versionCode for ${todayPrefix}: next suffix ${nextSuffix} is invalid`,
);
}
return Number.parseInt(`${todayPrefix}${nextSuffix.toString().padStart(2, "0")}`, 10);
}
function resolveNextVersion(buildGradleText: string, date: Date): VersionState {
const { versionCodeMatch } = parseVersionMatches(buildGradleText);
const currentVersionCode = Number.parseInt(versionCodeMatch[1] ?? "", 10);
if (!Number.isInteger(currentVersionCode)) {
throw new Error(`Invalid Android versionCode in ${buildGradlePath}`);
}
const versionName = formatVersionName(date);
const versionCode = resolveNextVersionCode(currentVersionCode, formatVersionCodePrefix(date));
return { versionName, versionCode };
}
function updateBuildGradleVersions(buildGradleText: string, nextVersion: VersionState): string {
return buildGradleText
.replace(/versionCode = \d+/, `versionCode = ${nextVersion.versionCode}`)
.replace(/versionName = "[^"]+"/, `versionName = "${nextVersion.versionName}"`);
}
async function sha256Hex(path: string): Promise<string> {
const buffer = await Bun.file(path).arrayBuffer();
const digest = await crypto.subtle.digest("SHA-256", buffer);
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
}
async function verifyBundleSignature(path: string): Promise<void> {
await $`jarsigner -verify ${path}`.quiet();
}
async function main() {
const buildGradleFile = Bun.file(buildGradlePath);
const originalText = await buildGradleFile.text();
const nextVersion = resolveNextVersion(originalText, new Date());
const updatedText = updateBuildGradleVersions(originalText, nextVersion);
if (updatedText === originalText) {
throw new Error("Android version bump produced no change");
}
console.log(`Android versionName -> ${nextVersion.versionName}`);
console.log(`Android versionCode -> ${nextVersion.versionCode}`);
await Bun.write(buildGradlePath, updatedText);
try {
await $`./gradlew :app:bundleRelease`.cwd(androidDir);
} catch (error) {
await Bun.write(buildGradlePath, originalText);
throw error;
}
const bundleFile = Bun.file(bundlePath);
if (!(await bundleFile.exists())) {
throw new Error(`Signed bundle missing at ${bundlePath}`);
}
await verifyBundleSignature(bundlePath);
const hash = await sha256Hex(bundlePath);
console.log(`Signed AAB: ${bundlePath}`);
console.log(`SHA-256: ${hash}`);
}
await main();

View File

@ -1,8 +1,8 @@
// Shared iOS version defaults.
// Generated overrides live in build/Version.xcconfig (git-ignored).
OPENCLAW_GATEWAY_VERSION = 2026.3.14
OPENCLAW_MARKETING_VERSION = 2026.3.14
OPENCLAW_BUILD_VERSION = 202603140
OPENCLAW_GATEWAY_VERSION = 0.0.0
OPENCLAW_MARKETING_VERSION = 0.0.0
OPENCLAW_BUILD_VERSION = 0
#include? "../build/Version.xcconfig"

View File

@ -18,10 +18,13 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
guard Self.allMessageNames.contains(message.name) else { return }
// Only accept actions from the in-app canvas scheme. Local-network HTTP
// pages are regular web content and must not get direct agent dispatch.
// Only accept actions from local Canvas content (not arbitrary web pages).
guard let webView = message.webView, let url = webView.url else { return }
guard let scheme = url.scheme, CanvasScheme.allSchemes.contains(scheme) else {
if let scheme = url.scheme, CanvasScheme.allSchemes.contains(scheme) {
// ok
} else if Self.isLocalNetworkCanvasURL(url) {
// ok
} else {
return
}
@ -104,5 +107,10 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
}
}
}
static func isLocalNetworkCanvasURL(_ url: URL) -> Bool {
LocalNetworkURLSupport.isLocalNetworkHTTPURL(url)
}
// Formatting helpers live in OpenClawKit (`OpenClawCanvasA2UIAction`).
}

View File

@ -50,24 +50,21 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
// Bridge A2UI "a2uiaction" DOM events back into the native agent loop.
//
// Keep the bridge on the trusted in-app canvas scheme only, and do not
// expose unattended deep-link credentials to page JavaScript.
// Prefer WKScriptMessageHandler when WebKit exposes it, otherwise fall back to an unattended deep link
// (includes the app-generated key so it won't prompt).
canvasWindowLogger.debug("CanvasWindowController init building A2UI bridge script")
let deepLinkKey = DeepLinkHandler.currentCanvasKey()
let injectedSessionKey = sessionKey.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "main"
let allowedSchemesJSON = (
try? String(
data: JSONSerialization.data(withJSONObject: CanvasScheme.allSchemes),
encoding: .utf8)
) ?? "[]"
let bridgeScript = """
(() => {
try {
const allowedSchemes = \(allowedSchemesJSON);
const allowedSchemes = \(String(describing: CanvasScheme.allSchemes));
const protocol = location.protocol.replace(':', '');
if (!allowedSchemes.includes(protocol)) return;
if (globalThis.__openclawA2UIBridgeInstalled) return;
globalThis.__openclawA2UIBridgeInstalled = true;
const deepLinkKey = \(Self.jsStringLiteral(deepLinkKey));
const sessionKey = \(Self.jsStringLiteral(injectedSessionKey));
const machineName = \(Self.jsStringLiteral(InstanceIdentity.displayName));
const instanceId = \(Self.jsStringLiteral(InstanceIdentity.instanceId));
@ -107,8 +104,24 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
return;
}
// Without the native handler, fail closed instead of exposing an
// unattended deep-link credential to page JavaScript.
const ctx = userAction.context ? (' ctx=' + JSON.stringify(userAction.context)) : '';
const message =
'CANVAS_A2UI action=' + userAction.name +
' session=' + sessionKey +
' surface=' + userAction.surfaceId +
' component=' + (userAction.sourceComponentId || '-') +
' host=' + machineName.replace(/\\s+/g, '_') +
' instance=' + instanceId +
ctx +
' default=update_canvas';
const params = new URLSearchParams();
params.set('message', message);
params.set('sessionKey', sessionKey);
params.set('thinking', 'low');
params.set('deliver', 'false');
params.set('channel', 'last');
params.set('key', deepLinkKey);
location.href = 'openclaw://agent?' + params.toString();
} catch {}
}, true);
} catch {}

View File

@ -16,14 +16,7 @@ extension CronJobEditor {
self.agentId = job.agentId ?? ""
self.enabled = job.enabled
self.deleteAfterRun = job.deleteAfterRun ?? false
switch job.parsedSessionTarget {
case .predefined(let target):
self.sessionTarget = target
self.preservedSessionTargetRaw = nil
case .session(let id):
self.sessionTarget = .isolated
self.preservedSessionTargetRaw = "session:\(id)"
}
self.sessionTarget = job.sessionTarget
self.wakeMode = job.wakeMode
switch job.schedule {
@ -58,7 +51,7 @@ extension CronJobEditor {
self.channel = trimmed.isEmpty ? "last" : trimmed
self.to = delivery.to ?? ""
self.bestEffortDeliver = delivery.bestEffort ?? false
} else if self.isIsolatedLikeSessionTarget {
} else if self.sessionTarget == .isolated {
self.deliveryMode = .announce
}
}
@ -87,7 +80,7 @@ extension CronJobEditor {
"name": name,
"enabled": self.enabled,
"schedule": schedule,
"sessionTarget": self.effectiveSessionTargetRaw,
"sessionTarget": self.sessionTarget.rawValue,
"wakeMode": self.wakeMode.rawValue,
"payload": payload,
]
@ -99,7 +92,7 @@ extension CronJobEditor {
root["agentId"] = NSNull()
}
if self.isIsolatedLikeSessionTarget {
if self.sessionTarget == .isolated {
root["delivery"] = self.buildDelivery()
}
@ -167,7 +160,7 @@ extension CronJobEditor {
}
func buildSelectedPayload() throws -> [String: Any] {
if self.isIsolatedLikeSessionTarget { return self.buildAgentTurnPayload() }
if self.sessionTarget == .isolated { return self.buildAgentTurnPayload() }
switch self.payloadKind {
case .systemEvent:
let text = self.trimmed(self.systemEventText)
@ -178,7 +171,7 @@ extension CronJobEditor {
}
func validateSessionTarget(_ payload: [String: Any]) throws {
if self.effectiveSessionTargetRaw == "main", payload["kind"] as? String == "agentTurn" {
if self.sessionTarget == .main, payload["kind"] as? String == "agentTurn" {
throw NSError(
domain: "Cron",
code: 0,
@ -188,7 +181,7 @@ extension CronJobEditor {
])
}
if self.effectiveSessionTargetRaw != "main", payload["kind"] as? String == "systemEvent" {
if self.sessionTarget == .isolated, payload["kind"] as? String == "systemEvent" {
throw NSError(
domain: "Cron",
code: 0,
@ -264,17 +257,6 @@ extension CronJobEditor {
return Int(floor(n * factor))
}
var effectiveSessionTargetRaw: String {
if self.sessionTarget == .isolated, let preserved = self.preservedSessionTargetRaw?.trimmingCharacters(in: .whitespacesAndNewlines), !preserved.isEmpty {
return preserved
}
return self.sessionTarget.rawValue
}
var isIsolatedLikeSessionTarget: Bool {
self.effectiveSessionTargetRaw != "main"
}
func formatDuration(ms: Int) -> String {
DurationFormattingSupport.conciseDuration(ms: ms)
}

View File

@ -16,7 +16,7 @@ struct CronJobEditor: View {
+ "Use an isolated session for agent turns so your main chat stays clean."
static let sessionTargetNote =
"Main jobs post a system event into the current main session. "
+ "Current and isolated-style jobs run agent turns and can announce results to a channel."
+ "Isolated jobs run OpenClaw in a dedicated session and can announce results to a channel."
static let scheduleKindNote =
"“At” runs once, “Every” repeats with a duration, “Cron” uses a 5-field Unix expression."
static let isolatedPayloadNote =
@ -29,7 +29,6 @@ struct CronJobEditor: View {
@State var agentId: String = ""
@State var enabled: Bool = true
@State var sessionTarget: CronSessionTarget = .main
@State var preservedSessionTargetRaw: String?
@State var wakeMode: CronWakeMode = .now
@State var deleteAfterRun: Bool = false
@ -118,7 +117,6 @@ struct CronJobEditor: View {
Picker("", selection: self.$sessionTarget) {
Text("main").tag(CronSessionTarget.main)
Text("isolated").tag(CronSessionTarget.isolated)
Text("current").tag(CronSessionTarget.current)
}
.labelsHidden()
.pickerStyle(.segmented)
@ -211,7 +209,7 @@ struct CronJobEditor: View {
GroupBox("Payload") {
VStack(alignment: .leading, spacing: 10) {
if self.isIsolatedLikeSessionTarget {
if self.sessionTarget == .isolated {
Text(Self.isolatedPayloadNote)
.font(.footnote)
.foregroundStyle(.secondary)
@ -291,11 +289,8 @@ struct CronJobEditor: View {
self.sessionTarget = .isolated
}
}
.onChange(of: self.sessionTarget) { oldValue, newValue in
if oldValue != newValue {
self.preservedSessionTargetRaw = nil
}
if newValue != .main {
.onChange(of: self.sessionTarget) { _, newValue in
if newValue == .isolated {
self.payloadKind = .agentTurn
} else if newValue == .main, self.payloadKind == .agentTurn {
self.payloadKind = .systemEvent

View File

@ -3,39 +3,12 @@ import Foundation
enum CronSessionTarget: String, CaseIterable, Identifiable, Codable {
case main
case isolated
case current
var id: String {
self.rawValue
}
}
enum CronCustomSessionTarget: Codable, Equatable {
case predefined(CronSessionTarget)
case session(id: String)
var rawValue: String {
switch self {
case .predefined(let target):
return target.rawValue
case .session(let id):
return "session:\(id)"
}
}
static func from(_ value: String) -> CronCustomSessionTarget {
if let predefined = CronSessionTarget(rawValue: value) {
return .predefined(predefined)
}
if value.hasPrefix("session:") {
let sessionId = String(value.dropFirst(8))
return .session(id: sessionId)
}
// Fallback to isolated for unknown values
return .predefined(.isolated)
}
}
enum CronWakeMode: String, CaseIterable, Identifiable, Codable {
case now
case nextHeartbeat = "next-heartbeat"
@ -231,69 +204,12 @@ struct CronJob: Identifiable, Codable, Equatable {
let createdAtMs: Int
let updatedAtMs: Int
let schedule: CronSchedule
private let sessionTargetRaw: String
let sessionTarget: CronSessionTarget
let wakeMode: CronWakeMode
let payload: CronPayload
let delivery: CronDelivery?
let state: CronJobState
enum CodingKeys: String, CodingKey {
case id
case agentId
case name
case description
case enabled
case deleteAfterRun
case createdAtMs
case updatedAtMs
case schedule
case sessionTargetRaw = "sessionTarget"
case wakeMode
case payload
case delivery
case state
}
/// Parsed session target (predefined or custom session ID)
var parsedSessionTarget: CronCustomSessionTarget {
CronCustomSessionTarget.from(self.sessionTargetRaw)
}
/// Compatibility shim for existing editor/UI code paths that still use the
/// predefined enum.
var sessionTarget: CronSessionTarget {
switch self.parsedSessionTarget {
case .predefined(let target):
return target
case .session:
return .isolated
}
}
var sessionTargetDisplayValue: String {
self.parsedSessionTarget.rawValue
}
var transcriptSessionKey: String? {
switch self.parsedSessionTarget {
case .predefined(.main):
return nil
case .predefined(.isolated), .predefined(.current):
return "cron:\(self.id)"
case .session(let id):
return id
}
}
var supportsAnnounceDelivery: Bool {
switch self.parsedSessionTarget {
case .predefined(.main):
return false
case .predefined(.isolated), .predefined(.current), .session:
return true
}
}
var displayName: String {
let trimmed = self.name.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? "Untitled job" : trimmed

View File

@ -18,7 +18,7 @@ extension CronSettings {
}
}
HStack(spacing: 6) {
StatusPill(text: job.sessionTargetDisplayValue, tint: .secondary)
StatusPill(text: job.sessionTarget.rawValue, tint: .secondary)
StatusPill(text: job.wakeMode.rawValue, tint: .secondary)
if let agentId = job.agentId, !agentId.isEmpty {
StatusPill(text: "agent \(agentId)", tint: .secondary)
@ -34,9 +34,9 @@ extension CronSettings {
@ViewBuilder
func jobContextMenu(_ job: CronJob) -> some View {
Button("Run now") { Task { await self.store.runJob(id: job.id, force: true) } }
if let transcriptSessionKey = job.transcriptSessionKey {
if job.sessionTarget == .isolated {
Button("Open transcript") {
WebChatManager.shared.show(sessionKey: transcriptSessionKey)
WebChatManager.shared.show(sessionKey: "cron:\(job.id)")
}
}
Divider()
@ -75,9 +75,9 @@ extension CronSettings {
.labelsHidden()
Button("Run") { Task { await self.store.runJob(id: job.id, force: true) } }
.buttonStyle(.borderedProminent)
if let transcriptSessionKey = job.transcriptSessionKey {
if job.sessionTarget == .isolated {
Button("Transcript") {
WebChatManager.shared.show(sessionKey: transcriptSessionKey)
WebChatManager.shared.show(sessionKey: "cron:\(job.id)")
}
.buttonStyle(.bordered)
}
@ -103,7 +103,7 @@ extension CronSettings {
if let agentId = job.agentId, !agentId.isEmpty {
LabeledContent("Agent") { Text(agentId) }
}
LabeledContent("Session") { Text(job.sessionTargetDisplayValue) }
LabeledContent("Session") { Text(job.sessionTarget.rawValue) }
LabeledContent("Wake") { Text(job.wakeMode.rawValue) }
LabeledContent("Next run") {
if let date = job.nextRunDate {
@ -224,7 +224,7 @@ extension CronSettings {
HStack(spacing: 8) {
if let thinking, !thinking.isEmpty { StatusPill(text: "think \(thinking)", tint: .secondary) }
if let timeoutSeconds { StatusPill(text: "\(timeoutSeconds)s", tint: .secondary) }
if job.supportsAnnounceDelivery {
if job.sessionTarget == .isolated {
let delivery = job.delivery
if let delivery {
if delivery.mode == .announce {

View File

@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.14</string>
<string>2026.3.13</string>
<key>CFBundleVersion</key>
<string>202603140</string>
<string>202603130</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@ -54,7 +54,7 @@ enum RuntimeResolutionError: Error {
enum RuntimeLocator {
private static let logger = Logger(subsystem: "ai.openclaw", category: "runtime")
private static let minNode = RuntimeVersion(major: 22, minor: 16, patch: 0)
private static let minNode = RuntimeVersion(major: 22, minor: 0, patch: 0)
static func resolve(
searchPaths: [String] = CommandResolver.preferredPaths()) -> Result<RuntimeResolution, RuntimeResolutionError>
@ -91,7 +91,7 @@ enum RuntimeLocator {
switch error {
case let .notFound(searchPaths):
[
"openclaw needs Node >=22.16.0 but found no runtime.",
"openclaw needs Node >=22.0.0 but found no runtime.",
"PATH searched: \(searchPaths.joined(separator: ":"))",
"Install Node: https://nodejs.org/en/download",
].joined(separator: "\n")
@ -105,7 +105,7 @@ enum RuntimeLocator {
[
"Could not parse \(kind.rawValue) version output \"\(raw)\" from \(path).",
"PATH searched: \(searchPaths.joined(separator: ":"))",
"Try reinstalling or pinning a supported version (Node >=22.16.0).",
"Try reinstalling or pinning a supported version (Node >=22.0.0).",
].joined(separator: "\n")
}
}

View File

@ -16,7 +16,7 @@ struct RuntimeLocatorTests {
@Test func `resolve succeeds with valid node`() throws {
let script = """
#!/bin/sh
echo v22.16.0
echo v22.5.0
"""
let node = try self.makeTempExecutable(contents: script)
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
@ -25,23 +25,7 @@ struct RuntimeLocatorTests {
return
}
#expect(res.path == node.path)
#expect(res.version == RuntimeVersion(major: 22, minor: 16, patch: 0))
}
@Test func `resolve fails on boundary below minimum`() throws {
let script = """
#!/bin/sh
echo v22.15.9
"""
let node = try self.makeTempExecutable(contents: script)
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
guard case let .failure(.unsupported(_, found, required, path, _)) = result else {
Issue.record("Expected unsupported error, got \(result)")
return
}
#expect(found == RuntimeVersion(major: 22, minor: 15, patch: 9))
#expect(required == RuntimeVersion(major: 22, minor: 16, patch: 0))
#expect(path == node.path)
#expect(res.version == RuntimeVersion(major: 22, minor: 5, patch: 0))
}
@Test func `resolve fails when too old`() throws {
@ -76,17 +60,7 @@ struct RuntimeLocatorTests {
@Test func `describe failure includes paths`() {
let msg = RuntimeLocator.describeFailure(.notFound(searchPaths: ["/tmp/a", "/tmp/b"]))
#expect(msg.contains("Node >=22.16.0"))
#expect(msg.contains("PATH searched: /tmp/a:/tmp/b"))
let parseMsg = RuntimeLocator.describeFailure(
.versionParse(
kind: .node,
raw: "garbage",
path: "/usr/local/bin/node",
searchPaths: ["/usr/local/bin"],
))
#expect(parseMsg.contains("Node >=22.16.0"))
}
@Test func `runtime version parses with leading V and metadata`() {

View File

@ -1,8 +0,0 @@
# Generated Docs Artifacts
These baseline artifacts are generated from the repo-owned OpenClaw config schema and bundled channel/plugin metadata.
- Do not edit `config-baseline.json` by hand.
- Do not edit `config-baseline.jsonl` by hand.
- Regenerate it with `pnpm config:docs:gen`.
- Validate it in CI or locally with `pnpm config:docs:check`.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 64 KiB

View File

@ -25,9 +25,7 @@ Troubleshooting: [/automation/troubleshooting](/automation/troubleshooting)
- Jobs persist under `~/.openclaw/cron/` so restarts dont lose schedules.
- Two execution styles:
- **Main session**: enqueue a system event, then run on the next heartbeat.
- **Isolated**: run a dedicated agent turn in `cron:<jobId>` or a custom session, with delivery (announce by default or none).
- **Current session**: bind to the session where the cron is created (`sessionTarget: "current"`).
- **Custom session**: run in a persistent named session (`sessionTarget: "session:custom-id"`).
- **Isolated**: run a dedicated agent turn in `cron:<jobId>`, with delivery (announce by default or none).
- Wakeups are first-class: a job can request “wake now” vs “next heartbeat”.
- Webhook posting is per job via `delivery.mode = "webhook"` + `delivery.to = "<url>"`.
- Legacy fallback remains for stored jobs with `notify: true` when `cron.webhook` is set, migrate those jobs to webhook delivery mode.
@ -88,14 +86,6 @@ Think of a cron job as: **when** to run + **what** to do.
2. **Choose where it runs**
- `sessionTarget: "main"` → run during the next heartbeat with main context.
- `sessionTarget: "isolated"` → run a dedicated agent turn in `cron:<jobId>`.
- `sessionTarget: "current"` → bind to the current session (resolved at creation time to `session:<sessionKey>`).
- `sessionTarget: "session:custom-id"` → run in a persistent named session that maintains context across runs.
Default behavior (unchanged):
- `systemEvent` payloads default to `main`
- `agentTurn` payloads default to `isolated`
To use current session binding, explicitly set `sessionTarget: "current"`.
3. **Choose the payload**
- Main session → `payload.kind = "systemEvent"`
@ -157,13 +147,12 @@ See [Heartbeat](/gateway/heartbeat).
#### Isolated jobs (dedicated cron sessions)
Isolated jobs run a dedicated agent turn in session `cron:<jobId>` or a custom session.
Isolated jobs run a dedicated agent turn in session `cron:<jobId>`.
Key behaviors:
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
- Each run starts a **fresh session id** (no prior conversation carry-over), unless using a custom session.
- Custom sessions (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries.
- Each run starts a **fresh session id** (no prior conversation carry-over).
- Default behavior: if `delivery` is omitted, isolated jobs announce a summary (`delivery.mode = "announce"`).
- `delivery.mode` chooses what happens:
- `announce`: deliver a summary to the target channel and post a brief summary to the main session.
@ -332,42 +321,12 @@ Recurring, isolated job with delivery:
}
```
Recurring job bound to current session (auto-resolved at creation):
```json
{
"name": "Daily standup",
"schedule": { "kind": "cron", "expr": "0 9 * * *" },
"sessionTarget": "current",
"payload": {
"kind": "agentTurn",
"message": "Summarize yesterday's progress."
}
}
```
Recurring job in a custom persistent session:
```json
{
"name": "Project monitor",
"schedule": { "kind": "every", "everyMs": 300000 },
"sessionTarget": "session:project-alpha-monitor",
"payload": {
"kind": "agentTurn",
"message": "Check project status and update the running log."
}
}
```
Notes:
- `schedule.kind`: `at` (`at`), `every` (`everyMs`), or `cron` (`expr`, optional `tz`).
- `schedule.at` accepts ISO 8601 (timezone optional; treated as UTC when omitted).
- `everyMs` is milliseconds.
- `sessionTarget`: `"main"`, `"isolated"`, `"current"`, or `"session:<custom-id>"`.
- `"current"` is resolved to `"session:<sessionKey>"` at creation time.
- Custom sessions (`session:xxx`) maintain persistent context across runs.
- `sessionTarget` must be `"main"` or `"isolated"` and must match `payload.kind`.
- Optional fields: `agentId`, `description`, `enabled`, `deleteAfterRun` (defaults to true for `at`),
`delivery`.
- `wakeMode` defaults to `"now"` when omitted.

View File

@ -220,10 +220,10 @@ See [Lobster](/tools/lobster) for full usage and examples.
Both heartbeat and cron can interact with the main session, but differently:
| | Heartbeat | Cron (main) | Cron (isolated) |
| ------- | ------------------------------- | ------------------------ | ----------------------------------------------- |
| Session | Main | Main (via system event) | `cron:<jobId>` or custom session |
| History | Shared | Shared | Fresh each run (isolated) / Persistent (custom) |
| Context | Full | Full | None (isolated) / Cumulative (custom) |
| ------- | ------------------------------- | ------------------------ | -------------------------- |
| Session | Main | Main (via system event) | `cron:<jobId>` |
| History | Shared | Shared | Fresh each run |
| Context | Full | Full | None (starts clean) |
| Model | Main session model | Main session model | Can override |
| Output | Delivered if not `HEARTBEAT_OK` | Heartbeat prompt + event | Announce summary (default) |

View File

@ -782,11 +782,6 @@ openclaw message poll --channel telegram --target -1001234567890:topic:42 \
- `--poll-public`
- `--thread-id` for forum topics (or use a `:topic:` target)
Telegram send also supports:
- `--buttons` for inline keyboards when `channels.telegram.capabilities.inlineButtons` allows it
- `--force-document` to send outbound images and GIFs as documents instead of compressed photo or animated-media uploads
Action gating:
- `channels.telegram.actions.sendMessage=false` disables outbound Telegram messages, including polls

View File

@ -27,7 +27,7 @@ Related:
## Quick start (local)
```bash
openclaw browser profiles
openclaw browser --browser-profile chrome tabs
openclaw browser --browser-profile openclaw start
openclaw browser --browser-profile openclaw open https://example.com
openclaw browser --browser-profile openclaw snapshot
@ -38,8 +38,7 @@ openclaw browser --browser-profile openclaw snapshot
Profiles are named browser routing configs. In practice:
- `openclaw`: launches/attaches to a dedicated OpenClaw-managed Chrome instance (isolated user data dir).
- `user`: controls your existing signed-in Chrome session via Chrome DevTools MCP.
- `chrome-relay`: controls your existing Chrome tab(s) via the Chrome extension relay.
- `chrome`: controls your existing Chrome tab(s) via the Chrome extension relay.
```bash
openclaw browser profiles

View File

@ -95,7 +95,6 @@ openclaw gateway health --url ws://127.0.0.1:18789
```bash
openclaw gateway status
openclaw gateway status --json
openclaw gateway status --require-rpc
```
Options:
@ -106,13 +105,11 @@ Options:
- `--timeout <ms>`: probe timeout (default `10000`).
- `--no-probe`: skip the RPC probe (service-only view).
- `--deep`: scan system-level services too.
- `--require-rpc`: exit non-zero when the RPC probe fails. Cannot be combined with `--no-probe`.
Notes:
- `gateway status` resolves configured auth SecretRefs for probe auth when possible.
- If a required auth SecretRef is unresolved in this command path, probe auth can fail; pass `--token`/`--password` explicitly or resolve the secret source first.
- Use `--require-rpc` in scripts and automation when a listening service is not enough and you need the Gateway RPC itself to be healthy.
- On Linux systemd installs, service auth drift checks read both `Environment=` and `EnvironmentFile=` values from the unit (including `%h`, quoted paths, multiple files, and optional `-` files).
### `gateway probe`
@ -129,23 +126,6 @@ openclaw gateway probe
openclaw gateway probe --json
```
Interpretation:
- `Reachable: yes` means at least one target accepted a WebSocket connect.
- `RPC: ok` means detail RPC calls (`health`/`status`/`system-presence`/`config.get`) also succeeded.
- `RPC: limited - missing scope: operator.read` means connect succeeded but detail RPC is scope-limited. This is reported as **degraded** reachability, not full failure.
- Exit code is non-zero only when no probed target is reachable.
JSON notes (`--json`):
- Top level:
- `ok`: at least one target is reachable.
- `degraded`: at least one target had scope-limited detail RPC.
- Per target (`targets[].connect`):
- `ok`: reachability after connect + degraded classification.
- `rpcOk`: full detail RPC success.
- `scopeLimited`: detail RPC failed due to missing operator scope.
#### Remote over SSH (Mac app parity)
The macOS app “Remote over SSH” mode uses a local port-forward so the remote gateway (which may be bound to loopback only) becomes reachable at `ws://127.0.0.1:<port>`.

View File

@ -780,7 +780,7 @@ Subcommands:
Notes:
- `gateway status` probes the Gateway RPC by default using the services resolved port/config (override with `--url/--token/--password`).
- `gateway status` supports `--no-probe`, `--deep`, `--require-rpc`, and `--json` for scripting.
- `gateway status` supports `--no-probe`, `--deep`, and `--json` for scripting.
- `gateway status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans). Profile-named OpenClaw services are treated as first-class and aren't flagged as "extra".
- `gateway status` prints which config path the CLI uses vs which config the service likely uses (service env), plus the resolved probe target URL.
- On Linux systemd installs, status token-drift checks include both `Environment=` and `EnvironmentFile=` unit sources.

View File

@ -59,7 +59,6 @@ Name lookup:
- Required: `--target`, plus `--message` or `--media`
- Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback`
- Telegram only: `--buttons` (requires `channels.telegram.capabilities.inlineButtons` to allow it)
- Telegram only: `--force-document` (send images and GIFs as documents to avoid Telegram compression)
- Telegram only: `--thread-id` (forum topic id)
- Slack only: `--thread-id` (thread timestamp; `--reply-to` uses the same field)
- WhatsApp only: `--gif-playback`
@ -259,10 +258,3 @@ Send Telegram inline buttons:
openclaw message send --channel telegram --target @mychat --message "Choose:" \
--buttons '[ [{"text":"Yes","callback_data":"cmd:yes"}], [{"text":"No","callback_data":"cmd:no"}] ]'
```
Send a Telegram image as a document to avoid compression:
```bash
openclaw message send --channel telegram --target @mychat \
--media ./diagram.png --force-document
```

View File

@ -23,8 +23,6 @@ The default workspace layout uses two memory layers:
- Read today + yesterday at session start.
- `MEMORY.md` (optional)
- Curated long-term memory.
- If both `MEMORY.md` and `memory.md` exist at the workspace root, OpenClaw only loads `MEMORY.md`.
- Lowercase `memory.md` is only used as a fallback when `MEMORY.md` is absent.
- **Only load in the main, private session** (never in group contexts).
These files live under the workspace (`agents.defaults.workspace`, default

View File

@ -186,15 +186,20 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
Kimi K2 model IDs:
[//]: # "moonshot-kimi-k2-model-refs:start"
<!-- markdownlint-disable MD037 -->
{/_ moonshot-kimi-k2-model-refs:start _/ && null}
<!-- markdownlint-enable MD037 -->
- `moonshot/kimi-k2.5`
- `moonshot/kimi-k2-0905-preview`
- `moonshot/kimi-k2-turbo-preview`
- `moonshot/kimi-k2-thinking`
- `moonshot/kimi-k2-thinking-turbo`
[//]: # "moonshot-kimi-k2-model-refs:end"
<!-- markdownlint-disable MD037 -->
{/_ moonshot-kimi-k2-model-refs:end _/ && null}
<!-- markdownlint-enable MD037 -->
```json5
{

View File

@ -200,7 +200,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
- Legacy `group:<id>` keys are still recognized for migration.
- Inbound contexts may still use `group:<id>`; the channel is inferred from `Provider` and normalized to the canonical `agent:<agentId>:<channel>:group:<id>` form.
- Other sources:
- Cron jobs: `cron:<job.id>` (isolated) or custom `session:<custom-id>` (persistent)
- Cron jobs: `cron:<job.id>`
- Webhooks: `hook:<uuid>` (unless explicitly set by the hook)
- Node runs: `node-<nodeId>`

View File

@ -1009,8 +1009,7 @@
"tools/loop-detection",
"tools/reactions",
"tools/thinking",
"tools/web",
"tools/btw"
"tools/web"
]
},
{
@ -1242,6 +1241,7 @@
"group": "Security",
"pages": [
"security/formal-verification",
"security/README",
"security/THREAT-MODEL-ATLAS",
"security/CONTRIBUTING-THREAT-MODEL"
]
@ -1597,6 +1597,7 @@
"zh-CN/tools/apply-patch",
"zh-CN/brave-search",
"zh-CN/perplexity",
"zh-CN/tools/diffs",
"zh-CN/tools/elevated",
"zh-CN/tools/exec",
"zh-CN/tools/exec-approvals",

View File

@ -975,7 +975,6 @@ Periodic heartbeat runs.
model: "openai/gpt-5.2-mini",
includeReasoning: false,
lightContext: false, // default: false; true keeps only HEARTBEAT.md from workspace bootstrap files
isolatedSession: false, // default: false; true runs each heartbeat in a fresh session (no conversation history)
session: "main",
to: "+15555550123",
directPolicy: "allow", // allow (default) | block
@ -993,7 +992,6 @@ Periodic heartbeat runs.
- `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
- `directPolicy`: direct/DM delivery policy. `allow` (default) permits direct-target delivery. `block` suppresses direct-target delivery and emits `reason=dm-blocked`.
- `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
- `isolatedSession`: when true, each heartbeat runs in a fresh session with no prior conversation history. Same isolation pattern as cron `sessionTarget: "isolated"`. Reduces per-heartbeat token cost from ~100K to ~2-5K tokens.
- Per-agent: set `agents.list[].heartbeat`. When any agent defines `heartbeat`, **only those agents** run heartbeats.
- Heartbeats run full agent turns — shorter intervals burn more tokens.
@ -2344,7 +2342,7 @@ See [Plugins](/tools/plugin).
browser: {
enabled: true,
evaluateEnabled: true,
defaultProfile: "user",
defaultProfile: "chrome",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true, // default trusted-network mode
// allowPrivateNetwork: true, // legacy alias

View File

@ -22,8 +22,7 @@ Troubleshooting: [/automation/troubleshooting](/automation/troubleshooting)
3. Decide where heartbeat messages should go (`target: "none"` is the default; set `target: "last"` to route to the last contact).
4. Optional: enable heartbeat reasoning delivery for transparency.
5. Optional: use lightweight bootstrap context if heartbeat runs only need `HEARTBEAT.md`.
6. Optional: enable isolated sessions to avoid sending full conversation history each heartbeat.
7. Optional: restrict heartbeats to active hours (local time).
6. Optional: restrict heartbeats to active hours (local time).
Example config:
@ -36,7 +35,6 @@ Example config:
target: "last", // explicit delivery to last contact (default is "none")
directPolicy: "allow", // default: allow direct/DM targets; set "block" to suppress
lightContext: true, // optional: only inject HEARTBEAT.md from bootstrap files
isolatedSession: true, // optional: fresh session each run (no conversation history)
// activeHours: { start: "08:00", end: "24:00" },
// includeReasoning: true, // optional: send separate `Reasoning:` message too
},
@ -93,7 +91,6 @@ and logged; a message that is only `HEARTBEAT_OK` is dropped.
model: "anthropic/claude-opus-4-6",
includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
lightContext: false, // default: false; true keeps only HEARTBEAT.md from workspace bootstrap files
isolatedSession: false, // default: false; true runs each heartbeat in a fresh session (no conversation history)
target: "last", // default: none | options: last | none | <channel id> (core or plugin, e.g. "bluebubbles")
to: "+15551234567", // optional channel-specific override
accountId: "ops-bot", // optional multi-account channel id
@ -215,7 +212,6 @@ Use `accountId` to target a specific account on multi-account channels like Tele
- `model`: optional model override for heartbeat runs (`provider/model`).
- `includeReasoning`: when enabled, also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`).
- `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
- `isolatedSession`: when true, each heartbeat runs in a fresh session with no prior conversation history. Uses the same isolation pattern as cron `sessionTarget: "isolated"`. Dramatically reduces per-heartbeat token cost. Combine with `lightContext: true` for maximum savings. Delivery routing still uses the main session context.
- `session`: optional session key for heartbeat runs.
- `main` (default): agent main session.
- Explicit session key (copy from `openclaw sessions --json` or the [sessions CLI](/cli/sessions)).
@ -384,10 +380,6 @@ off in group chats.
## Cost awareness
Heartbeats run full agent turns. Shorter intervals burn more tokens. To reduce cost:
- Use `isolatedSession: true` to avoid sending full conversation history (~100K tokens down to ~2-5K per run).
- Use `lightContext: true` to limit bootstrap files to just `HEARTBEAT.md`.
- Set a cheaper `model` (e.g. `ollama/llama3.2:1b`).
- Keep `HEARTBEAT.md` small.
- Use `target: "none"` if you only want internal state updates.
Heartbeats run full agent turns. Shorter intervals burn more tokens. Keep
`HEARTBEAT.md` small and consider a cheaper `model` or `target: "none"` if you
only want internal state updates.

View File

@ -289,7 +289,7 @@ Look for:
- Valid browser executable path.
- CDP profile reachability.
- Extension relay tab attachment (if an extension relay profile is configured).
- Extension relay tab attachment for `profile="chrome"`.
Common signatures:

View File

@ -1358,8 +1358,7 @@ Your **workspace** (AGENTS.md, memory files, skills, etc.) is separate and confi
These files live in the **agent workspace**, not `~/.openclaw`.
- **Workspace (per agent)**: `AGENTS.md`, `SOUL.md`, `IDENTITY.md`, `USER.md`,
`MEMORY.md` (or legacy fallback `memory.md` when `MEMORY.md` is absent),
`memory/YYYY-MM-DD.md`, optional `HEARTBEAT.md`.
`MEMORY.md` (or `memory.md`), `memory/YYYY-MM-DD.md`, optional `HEARTBEAT.md`.
- **State dir (`~/.openclaw`)**: config, credentials, auth profiles, sessions, logs,
and shared skills (`~/.openclaw/skills`).

View File

@ -28,7 +28,7 @@ Good output in one line:
- `openclaw status` → shows configured channels and no obvious auth errors.
- `openclaw status --all` → full report is present and shareable.
- `openclaw gateway probe` → expected gateway target is reachable (`Reachable: yes`). `RPC: limited - missing scope: operator.read` is degraded diagnostics, not a connect failure.
- `openclaw gateway probe` → expected gateway target is reachable.
- `openclaw gateway status``Runtime: running` and `RPC probe: ok`.
- `openclaw doctor` → no blocking config/service errors.
- `openclaw channels status --probe` → channels report `connected` or `ready`.

View File

@ -285,7 +285,6 @@ Available families:
- `photos.latest`
- `contacts.search`, `contacts.add`
- `calendar.events`, `calendar.add`
- `callLog.search`
- `motion.activity`, `motion.pedometer`
Example invokes:

View File

@ -16,7 +16,7 @@ If you use `OPENROUTER_API_KEY`, an `sk-or-...` key in `tools.web.search.perplex
## Getting a Perplexity API key
1. Create a Perplexity account at [perplexity.ai/settings/api](https://www.perplexity.ai/settings/api)
1. Create a Perplexity account at <https://www.perplexity.ai/settings/api>
2. Generate an API key in the dashboard
3. Store the key in config or set `PERPLEXITY_API_KEY` in the Gateway environment.

View File

@ -163,5 +163,4 @@ See [Camera node](/nodes/camera) for parameters and CLI helpers.
- `photos.latest`
- `contacts.search`, `contacts.add`
- `calendar.events`, `calendar.add`
- `callLog.search`
- `motion.activity`, `motion.pedometer`

View File

@ -14,17 +14,7 @@ models are accessed via the `zai` provider and model IDs like `zai/glm-5`.
## CLI setup
```bash
# Coding Plan Global, recommended for Coding Plan users
openclaw onboard --auth-choice zai-coding-global
# Coding Plan CN (China region), recommended for Coding Plan users
openclaw onboard --auth-choice zai-coding-cn
# General API
openclaw onboard --auth-choice zai-global
# General API CN (China region)
openclaw onboard --auth-choice zai-cn
openclaw onboard --auth-choice zai-api-key
```
## Config snippet

View File

@ -15,15 +15,20 @@ Kimi Coding with `kimi-coding/k2p5`.
Current Kimi K2 model IDs:
[//]: # "moonshot-kimi-k2-ids:start"
<!-- markdownlint-disable MD037 -->
{/_ moonshot-kimi-k2-ids:start _/ && null}
<!-- markdownlint-enable MD037 -->
- `kimi-k2.5`
- `kimi-k2-0905-preview`
- `kimi-k2-turbo-preview`
- `kimi-k2-thinking`
- `kimi-k2-thinking-turbo`
[//]: # "moonshot-kimi-k2-ids:end"
<!-- markdownlint-disable MD037 -->
{/_ moonshot-kimi-k2-ids:end _/ && null}
<!-- markdownlint-enable MD037 -->
```bash
openclaw onboard --auth-choice moonshot-api-key

View File

@ -15,17 +15,9 @@ with a Z.AI API key.
## CLI setup
```bash
# Coding Plan Global, recommended for Coding Plan users
openclaw onboard --auth-choice zai-coding-global
# Coding Plan CN (China region), recommended for Coding Plan users
openclaw onboard --auth-choice zai-coding-cn
# General API
openclaw onboard --auth-choice zai-global
# General API CN (China region)
openclaw onboard --auth-choice zai-cn
openclaw onboard --auth-choice zai-api-key
# or non-interactive
openclaw onboard --zai-api-key "$ZAI_API_KEY"
```
## Config snippet

View File

@ -48,8 +48,7 @@ cp docs/reference/AGENTS.default.md ~/.openclaw/workspace/AGENTS.md
## Session start (required)
- Read `SOUL.md`, `USER.md`, and today+yesterday in `memory/`.
- Read `MEMORY.md` when present; only fall back to lowercase `memory.md` when `MEMORY.md` is absent.
- Read `SOUL.md`, `USER.md`, `memory.md`, and today+yesterday in `memory/`.
- Do it before responding.
## Soul (required)
@ -66,9 +65,8 @@ cp docs/reference/AGENTS.default.md ~/.openclaw/workspace/AGENTS.md
## Memory system (recommended)
- Daily log: `memory/YYYY-MM-DD.md` (create `memory/` if needed).
- Long-term memory: `MEMORY.md` for durable facts, preferences, and decisions.
- Lowercase `memory.md` is legacy fallback only; do not keep both root files on purpose.
- On session start, read today + yesterday + `MEMORY.md` when present, otherwise `memory.md`.
- Long-term memory: `memory.md` for durable facts, preferences, and decisions.
- On session start, read today + yesterday + `memory.md` if present.
- Capture: decisions, preferences, constraints, open loops.
- Avoid secrets unless explicitly requested.

View File

@ -29,10 +29,6 @@ Current OpenClaw releases use date-based versioning.
- Beta prerelease version: `YYYY.M.D-beta.N`
- Git tag: `vYYYY.M.D-beta.N`
- Examples from repo history: `v2026.2.15-beta.1`, `v2026.3.8-beta.1`
- Fallback correction tag: `vYYYY.M.D-N`
- Use only as a last-resort recovery tag when a published immutable release burned the original stable tag and you cannot reuse it.
- The npm package version stays `YYYY.M.D`; the `-N` suffix is only for the git tag and GitHub release.
- Prefer betas for normal pre-release iteration, then cut a clean stable tag once ready.
- Use the same version string everywhere, minus the leading `v` where Git tags are not used:
- `package.json`: `2026.3.8`
- Git tag: `v2026.3.8`
@ -42,12 +38,12 @@ Current OpenClaw releases use date-based versioning.
- `latest` = stable
- `beta` = prerelease/testing
- Dev is the moving head of `main`, not a normal git-tagged release.
- The tag-triggered preview run accepts stable, beta, and fallback correction tags, and rejects versions whose CalVer date is more than 2 UTC calendar days away from the release date.
- The release workflow enforces the current stable/beta tag formats and rejects versions whose CalVer date is more than 2 UTC calendar days away from the release date.
Historical note:
- Older tags such as `v2026.1.11-1`, `v2026.2.6-3`, and `v2.0.0-beta2` exist in repo history.
- Treat correction tags as a fallback-only escape hatch. New releases should still use `vYYYY.M.D` for stable and `vYYYY.M.D-beta.N` for beta.
- Treat those as legacy tag patterns. New releases should use `vYYYY.M.D` for stable and `vYYYY.M.D-beta.N` for beta.
1. **Version & metadata**
@ -76,7 +72,6 @@ Historical note:
- [ ] `pnpm check`
- [ ] `pnpm test` (or `pnpm test:coverage` if you need coverage output)
- [ ] `pnpm release:check` (verifies npm pack contents)
- [ ] If `pnpm config:docs:check` fails as part of release validation and the config-surface change is intentional, run `pnpm config:docs:gen`, review `docs/.generated/config-baseline.json` and `docs/.generated/config-baseline.jsonl`, commit the updated baselines, then rerun `pnpm release:check`.
- [ ] `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke` (Docker install smoke test, fast path; required before release)
- If the immediate previous npm release is known broken, set `OPENCLAW_INSTALL_SMOKE_PREVIOUS=<last-good-version>` or `OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS=1` for the preinstall step.
- [ ] (Optional) Full installer smoke (adds non-root + CLI coverage): `pnpm test:install:smoke`
@ -99,14 +94,10 @@ Historical note:
- [ ] Confirm git status is clean; commit and push as needed.
- [ ] Confirm npm trusted publishing is configured for the `openclaw` package.
- [ ] Do not rely on an `NPM_TOKEN` secret for this workflow; the publish job uses GitHub OIDC trusted publishing.
- [ ] Push the matching git tag to trigger the preview run in `.github/workflows/openclaw-npm-release.yml`.
- [ ] Run `OpenClaw NPM Release` manually with the same tag to publish after `npm-release` environment approval.
- [ ] Push the matching git tag to trigger `.github/workflows/openclaw-npm-release.yml`.
- Stable tags publish to npm `latest`.
- Beta tags publish to npm `beta`.
- Fallback correction tags like `v2026.3.13-1` map to npm version `2026.3.13`.
- Both the preview run and the manual publish run reject tags that do not map back to `package.json`, are not on `main`, or whose CalVer date is more than 2 UTC calendar days away from the release date.
- If `openclaw@YYYY.M.D` is already published, a fallback correction tag is still useful for GitHub release and Docker recovery, but npm publish will not republish that version.
- The workflow rejects tags that do not match `package.json`, are not on `main`, or whose CalVer date is more than 2 UTC calendar days away from the release date.
- [ ] Verify the registry: `npm view openclaw version`, `npm view openclaw dist-tags`, and `npx -y openclaw@X.Y.Z --version` (or `--help`).
### Troubleshooting (notes from 2.0.0-beta2 release)
@ -116,9 +107,8 @@ Historical note:
- `NPM_CONFIG_AUTH_TYPE=legacy npm dist-tag add openclaw@X.Y.Z latest`
- **`npx` verification fails with `ECOMPROMISED: Lock compromised`**: retry with a fresh cache:
- `NPM_CONFIG_CACHE=/tmp/npm-cache-$(date +%s) npx -y openclaw@X.Y.Z --version`
- **Tag needs recovery after a late fix**: if the original stable tag is tied to an immutable GitHub release, mint a fallback correction tag like `vX.Y.Z-1` instead of trying to force-update `vX.Y.Z`.
- Keep the npm package version at `X.Y.Z`; the correction suffix is for the git tag and GitHub release only.
- Use this only as a last resort. For normal iteration, prefer beta tags and then cut a clean stable release.
- **Tag needs repointing after a late fix**: force-update and push the tag, then ensure the GitHub release assets still match:
- `git tag -f vX.Y.Z && git push -f origin vX.Y.Z`
7. **GitHub release + appcast**

17
docs/security/README.md Normal file
View File

@ -0,0 +1,17 @@
# OpenClaw Security & Trust
**Live:** [trust.openclaw.ai](https://trust.openclaw.ai)
## Documents
- [Threat Model](/security/THREAT-MODEL-ATLAS) - MITRE ATLAS-based threat model for the OpenClaw ecosystem
- [Contributing to the Threat Model](/security/CONTRIBUTING-THREAT-MODEL) - How to add threats, mitigations, and attack chains
## Reporting Vulnerabilities
See the [Trust page](https://trust.openclaw.ai) for full reporting instructions covering all repos.
## Contact
- **Jamieson O'Reilly** ([@theonejvo](https://twitter.com/theonejvo)) - Security & Trust
- Discord: #security channel

View File

@ -25,7 +25,7 @@ Note, selecting 'chromium-browser' instead of 'chromium'
chromium-browser is already the newest version (2:1snap1-0ubuntu2).
```
This is NOT a real browser - it's just a wrapper.
This is NOT a real browser it's just a wrapper.
### Solution 1: Install Google Chrome (Recommended)
@ -123,7 +123,7 @@ curl -s http://127.0.0.1:18791/tabs
### Problem: "Chrome extension relay is running, but no tab is connected"
You're using an extension relay profile. It expects the OpenClaw
Youre using the `chrome` profile (extension relay). It expects the OpenClaw
browser extension to be attached to a live tab.
Fix options:
@ -135,5 +135,5 @@ Fix options:
Notes:
- The `chrome-relay` profile uses your **system default Chromium browser** when possible.
- The `chrome` profile uses your **system default Chromium browser** when possible.
- Local `openclaw` profiles auto-assign `cdpPort`/`cdpUrl`; only set those for remote CDP.

View File

@ -20,13 +20,6 @@ Back to the main browser docs: [Browser](/tools/browser).
OpenClaw controls a **dedicated Chrome profile** (named `openclaw`, orangetinted UI). This is separate from your daily browser profile.
For agent browser tool calls:
- Default choice: the agent should use its isolated `openclaw` browser.
- Use `profile="user"` only when existing logged-in sessions matter and the user is at the computer to click/approve any attach prompt.
- Use `profile="chrome-relay"` only for the Chrome extension / toolbar-button attach flow.
- If you have multiple user-browser profiles, specify the profile explicitly instead of guessing.
Two easy ways to access it:
1. **Ask the agent to open the browser** and then log in yourself.

View File

@ -33,7 +33,7 @@ Choose this when:
### Option 2: Chrome extension relay
Use the built-in `chrome-relay` profile plus the OpenClaw Chrome extension.
Use the built-in `chrome` profile plus the OpenClaw Chrome extension.
Choose this when:
@ -155,7 +155,7 @@ Example:
{
browser: {
enabled: true,
defaultProfile: "chrome-relay",
defaultProfile: "chrome",
relayBindHost: "0.0.0.0",
},
}
@ -197,7 +197,7 @@ openclaw browser tabs --browser-profile remote
For the extension relay:
```bash
openclaw browser tabs --browser-profile chrome-relay
openclaw browser tabs --browser-profile chrome
```
Good result:

View File

@ -18,8 +18,8 @@ Beginner view:
- Think of it as a **separate, agent-only browser**.
- The `openclaw` profile does **not** touch your personal browser profile.
- The agent can **open tabs, read pages, click, and type** in a safe lane.
- The built-in `user` profile attaches to your real signed-in Chrome session;
`chrome-relay` is the explicit extension-relay profile.
- The default `chrome` profile uses the **system default Chromium browser** via the
extension relay; switch to `openclaw` for the isolated managed browser.
## What you get
@ -43,22 +43,13 @@ openclaw browser --browser-profile openclaw snapshot
If you get “Browser disabled”, enable it in config (see below) and restart the
Gateway.
## Profiles: `openclaw` vs `user` vs `chrome-relay`
## Profiles: `openclaw` vs `chrome`
- `openclaw`: managed, isolated browser (no extension required).
- `user`: built-in Chrome MCP attach profile for your **real signed-in Chrome**
session.
- `chrome-relay`: extension relay to your **system browser** (requires the
OpenClaw extension to be attached to a tab).
For agent browser tool calls:
- Default: use the isolated `openclaw` browser.
- Prefer `profile="user"` when existing logged-in sessions matter and the user
is at the computer to click/approve any attach prompt.
- Use `profile="chrome-relay"` only when the user explicitly wants the Chrome
extension / toolbar-button attach flow.
- `profile` is the explicit override when you want a specific browser mode.
- `chrome`: extension relay to your **system browser** (requires the OpenClaw
extension to be attached to a tab).
- `existing-session`: official Chrome MCP attach flow for a running Chrome
profile.
Set `browser.defaultProfile: "openclaw"` if you want managed mode by default.
@ -79,7 +70,7 @@ Browser settings live in `~/.openclaw/openclaw.json`.
// cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override
remoteCdpTimeoutMs: 1500, // remote CDP HTTP timeout (ms)
remoteCdpHandshakeTimeoutMs: 3000, // remote CDP WebSocket handshake timeout (ms)
defaultProfile: "openclaw",
defaultProfile: "chrome",
color: "#FF4500",
headless: false,
noSandbox: false,
@ -88,16 +79,12 @@ Browser settings live in `~/.openclaw/openclaw.json`.
profiles: {
openclaw: { cdpPort: 18800, color: "#FF4500" },
work: { cdpPort: 18801, color: "#0066CC" },
user: {
chromeLive: {
cdpPort: 18802,
driver: "existing-session",
attachOnly: true,
color: "#00AA00",
},
"chrome-relay": {
driver: "extension",
cdpUrl: "http://127.0.0.1:18792",
color: "#00AA00",
},
remote: { cdpUrl: "http://10.0.0.42:9222", color: "#00AA00" },
},
},
@ -118,7 +105,7 @@ Notes:
- `browser.ssrfPolicy.allowPrivateNetwork` remains supported as a legacy alias for compatibility.
- `attachOnly: true` means “never launch a local browser; only attach if it is already running.”
- `color` + per-profile `color` tint the browser UI so you can see which profile is active.
- Default profile is `openclaw` (OpenClaw-managed standalone browser). Use `defaultProfile: "user"` to opt into the signed-in user browser, or `defaultProfile: "chrome-relay"` for the extension relay.
- Default profile is `openclaw` (OpenClaw-managed standalone browser). Use `defaultProfile: "chrome"` to opt into the Chrome extension relay.
- Auto-detect order: system default browser if Chromium-based; otherwise Chrome → Brave → Edge → Chromium → Chrome Canary.
- Local `openclaw` profiles auto-assign `cdpPort`/`cdpUrl` — set those only for remote CDP.
- `driver: "existing-session"` uses Chrome DevTools MCP instead of raw CDP. Do
@ -292,7 +279,7 @@ OpenClaw supports multiple named profiles (routing configs). Profiles can be:
Defaults:
- The `openclaw` profile is auto-created if missing.
- The `chrome-relay` profile is built-in for the Chrome extension relay (points at `http://127.0.0.1:18792` by default).
- The `chrome` profile is built-in for the Chrome extension relay (points at `http://127.0.0.1:18792` by default).
- Existing-session profiles are opt-in; create them with `--driver existing-session`.
- Local CDP ports allocate from **1880018899** by default.
- Deleting a profile moves its local data directory to Trash.
@ -336,8 +323,8 @@ openclaw browser extension install
2. Use it:
- CLI: `openclaw browser --browser-profile chrome-relay tabs`
- Agent tool: `browser` with `profile="chrome-relay"`
- CLI: `openclaw browser --browser-profile chrome tabs`
- Agent tool: `browser` with `profile="chrome"`
Optional: if you want a different name or relay port, create your own profile:
@ -353,9 +340,6 @@ Notes:
- This mode relies on Playwright-on-CDP for most operations (screenshots/snapshots/actions).
- Detach by clicking the extension icon again.
- Agent use: prefer `profile="user"` for logged-in sites. Use `profile="chrome-relay"`
only when you specifically want the extension flow. The user must be present
to click the extension and attach the tab.
## Chrome existing-session via MCP
@ -368,12 +352,14 @@ Official background and setup references:
- [Chrome for Developers: Use Chrome DevTools MCP with your browser session](https://developer.chrome.com/blog/chrome-devtools-mcp-debug-your-browser-session)
- [Chrome DevTools MCP README](https://github.com/ChromeDevTools/chrome-devtools-mcp)
Built-in profile:
Create a profile:
- `user`
Optional: create your own custom existing-session profile if you want a
different name or color.
```bash
openclaw browser create-profile \
--name chrome-live \
--driver existing-session \
--color "#00AA00"
```
Then in Chrome:
@ -384,16 +370,15 @@ Then in Chrome:
Live attach smoke test:
```bash
openclaw browser --browser-profile user start
openclaw browser --browser-profile user status
openclaw browser --browser-profile user tabs
openclaw browser --browser-profile user snapshot --format ai
openclaw browser --browser-profile chrome-live start
openclaw browser --browser-profile chrome-live status
openclaw browser --browser-profile chrome-live tabs
openclaw browser --browser-profile chrome-live snapshot --format ai
```
What success looks like:
- `status` shows `driver: existing-session`
- `status` shows `transport: chrome-mcp`
- `status` shows `running: true`
- `tabs` lists your already-open Chrome tabs
- `snapshot` returns refs from the selected live tab
@ -403,15 +388,6 @@ What to check if attach does not work:
- Chrome is version `144+`
- remote debugging is enabled at `chrome://inspect/#remote-debugging`
- Chrome showed and you accepted the attach consent prompt
Agent use:
- Use `profile="user"` when you need the users logged-in browser state.
- If you use a custom existing-session profile, pass that explicit profile name.
- Prefer `profile="user"` over `profile="chrome-relay"` unless the user
explicitly wants the extension / attach-tab flow.
- Only choose this mode when the user is at the computer to approve the attach
prompt.
- the Gateway or node host can spawn `npx chrome-devtools-mcp@latest --autoConnect`
Notes:
@ -437,7 +413,7 @@ WSL2 / cross-namespace example:
browser: {
enabled: true,
relayBindHost: "0.0.0.0",
defaultProfile: "chrome-relay",
defaultProfile: "chrome",
},
}
```

View File

@ -1,142 +0,0 @@
---
summary: "Ephemeral side questions with /btw"
read_when:
- You want to ask a quick side question about the current session
- You are implementing or debugging BTW behavior across clients
title: "BTW Side Questions"
---
# BTW Side Questions
`/btw` lets you ask a quick side question about the **current session** without
turning that question into normal conversation history.
It is modeled after Claude Code's `/btw` behavior, but adapted to OpenClaw's
Gateway and multi-channel architecture.
## What it does
When you send:
```text
/btw what changed?
```
OpenClaw:
1. snapshots the current session context,
2. runs a separate **tool-less** model call,
3. answers only the side question,
4. leaves the main run alone,
5. does **not** write the BTW question or answer to session history,
6. emits the answer as a **live side result** rather than a normal assistant message.
The important mental model is:
- same session context
- separate one-shot side query
- no tool calls
- no future context pollution
- no transcript persistence
## What it does not do
`/btw` does **not**:
- create a new durable session,
- continue the unfinished main task,
- run tools or agent tool loops,
- write BTW question/answer data to transcript history,
- appear in `chat.history`,
- survive a reload.
It is intentionally **ephemeral**.
## How context works
BTW uses the current session as **background context only**.
If the main run is currently active, OpenClaw snapshots the current message
state and includes the in-flight main prompt as background context, while
explicitly telling the model:
- answer only the side question,
- do not resume or complete the unfinished main task,
- do not emit tool calls or pseudo-tool calls.
That keeps BTW isolated from the main run while still making it aware of what
the session is about.
## Delivery model
BTW is **not** delivered as a normal assistant transcript message.
At the Gateway protocol level:
- normal assistant chat uses the `chat` event
- BTW uses the `chat.side_result` event
This separation is intentional. If BTW reused the normal `chat` event path,
clients would treat it like regular conversation history.
Because BTW uses a separate live event and is not replayed from
`chat.history`, it disappears after reload.
## Surface behavior
### TUI
In TUI, BTW is rendered inline in the current session view, but it remains
ephemeral:
- visibly distinct from a normal assistant reply
- dismissible with `Enter` or `Esc`
- not replayed on reload
### External channels
On channels like Telegram, WhatsApp, and Discord, BTW is delivered as a
clearly labeled one-off reply because those surfaces do not have a local
ephemeral overlay concept.
The answer is still treated as a side result, not normal session history.
### Control UI / web
The Gateway emits BTW correctly as `chat.side_result`, and BTW is not included
in `chat.history`, so the persistence contract is already correct for web.
The current Control UI still needs a dedicated `chat.side_result` consumer to
render BTW live in the browser. Until that client-side support lands, BTW is a
Gateway-level feature with full TUI and external-channel behavior, but not yet
a complete browser UX.
## When to use BTW
Use `/btw` when you want:
- a quick clarification about the current work,
- a factual side answer while a long run is still in progress,
- a temporary answer that should not become part of future session context.
Examples:
```text
/btw what file are we editing?
/btw what does this error mean?
/btw summarize the current task in one sentence
/btw what is 17 * 19?
```
## When not to use BTW
Do not use `/btw` when you want the answer to become part of the session's
future working context.
In that case, ask normally in the main session instead of using BTW.
## Related
- [Slash commands](/tools/slash-commands)
- [Thinking Levels](/tools/thinking)
- [Session](/concepts/session)

View File

@ -62,14 +62,19 @@ After upgrading OpenClaw:
## Use it (set gateway token once)
To use the extension relay, create a browser profile for it:
OpenClaw ships with a built-in browser profile named `chrome` that targets the extension relay on the default port.
Before first attach, open extension Options and set:
- `Port` (default `18792`)
- `Gateway token` (must match `gateway.auth.token` / `OPENCLAW_GATEWAY_TOKEN`)
Then create a profile:
Use it:
- CLI: `openclaw browser --browser-profile chrome tabs`
- Agent tool: `browser` with `profile="chrome"`
If you want a different name or a different relay port, create your own profile:
```bash
openclaw browser create-profile \
@ -79,11 +84,6 @@ openclaw browser create-profile \
--color "#00AA00"
```
Use it:
- CLI: `openclaw browser --browser-profile my-chrome tabs`
- Agent tool: `browser` with `profile="my-chrome"`
### Custom Gateway ports
If you're using a custom gateway port, the extension relay port is automatically derived:

View File

@ -160,14 +160,13 @@ Long options are validated fail-closed in safe-bin mode: unknown flags and ambig
abbreviations are rejected.
Denied flags by safe-bin profile:
[//]: # "SAFE_BIN_DENIED_FLAGS:START"
<!-- SAFE_BIN_DENIED_FLAGS:START -->
- `grep`: `--dereference-recursive`, `--directories`, `--exclude-from`, `--file`, `--recursive`, `-R`, `-d`, `-f`, `-r`
- `jq`: `--argfile`, `--from-file`, `--library-path`, `--rawfile`, `--slurpfile`, `-L`, `-f`
- `sort`: `--compress-program`, `--files0-from`, `--output`, `--random-source`, `--temporary-directory`, `-T`, `-o`
- `wc`: `--files0-from`
[//]: # "SAFE_BIN_DENIED_FLAGS:END"
<!-- SAFE_BIN_DENIED_FLAGS:END -->
Safe bins also force argv tokens to be treated as **literal text** at execution time (no globbing
and no `$VARS` expansion) for stdin-only segments, so patterns like `*` or `$HOME/...` cannot be

View File

@ -316,11 +316,7 @@ Common parameters:
Notes:
- Requires `browser.enabled=true` (default is `true`; set `false` to disable).
- All actions accept optional `profile` parameter for multi-instance support.
- Omit `profile` for the safe default: isolated OpenClaw-managed browser (`openclaw`).
- Use `profile="user"` for the real local host browser when existing logins/cookies matter and the user is present to click/approve any attach prompt.
- Use `profile="chrome-relay"` only for the Chrome extension / toolbar-button attach flow.
- `profile="user"` and `profile="chrome-relay"` are host-only; do not combine them with sandbox/node targets.
- When `profile` is omitted, uses `browser.defaultProfile` (defaults to `openclaw`).
- When `profile` is omitted, uses `browser.defaultProfile` (defaults to "chrome").
- Profile names: lowercase alphanumeric + hyphens only (max 64 chars).
- Port range: 18800-18899 (~100 profiles max).
- Remote profiles are attach-only (no start/stop/reset).

View File

@ -76,7 +76,6 @@ Text + native (when enabled):
- `/allowlist` (list/add/remove allowlist entries)
- `/approve <id> allow-once|allow-always|deny` (resolve exec approval prompts)
- `/context [list|detail|json]` (explain “context”; `detail` shows per-file + per-tool + per-skill + system prompt size)
- `/btw <question>` (ask an ephemeral side question about the current session without changing future session context; see [/tools/btw](/tools/btw))
- `/export-session [path]` (alias: `/export`) (export current session to HTML with full system prompt)
- `/whoami` (show your sender id; alias: `/id`)
- `/session idle <duration|off>` (manage inactivity auto-unfocus for focused thread bindings)
@ -224,27 +223,3 @@ Notes:
- **`/stop`** targets the active chat session so it can abort the current run.
- **Slack:** `channels.slack.slashCommand` is still supported for a single `/openclaw`-style command. If you enable `commands.native`, you must create one Slack slash command per built-in command (same names as `/help`). Command argument menus for Slack are delivered as ephemeral Block Kit buttons.
- Slack native exception: register `/agentstatus` (not `/status`) because Slack reserves `/status`. Text `/status` still works in Slack messages.
## BTW side questions
`/btw` is a quick **side question** about the current session.
Unlike normal chat:
- it uses the current session as background context,
- it runs as a separate **tool-less** one-shot call,
- it does not change future session context,
- it is not written to transcript history,
- it is delivered as a live side result instead of a normal assistant message.
That makes `/btw` useful when you want a temporary clarification while the main
task keeps going.
Example:
```text
/btw what are we doing right now?
```
See [BTW Side Questions](/tools/btw) for the full behavior and client UX
details.

View File

@ -28,9 +28,7 @@ x-i18n:
- 任务持久化存储在 `~/.openclaw/cron/` 下,因此重启不会丢失计划。
- 两种执行方式:
- **主会话**:入队一个系统事件,然后在下一次心跳时运行。
- **隔离式**:在 `cron:<jobId>` 或自定义会话中运行专用智能体轮次,可投递摘要(默认 announce或不投递。
- **当前会话**:绑定到创建定时任务时的会话 (`sessionTarget: "current"`)。
- **自定义会话**:在持久化的命名会话中运行 (`sessionTarget: "session:custom-id"`)。
- **隔离式**:在 `cron:<jobId>` 中运行专用智能体轮次,可投递摘要(默认 announce或不投递。
- 唤醒是一等功能:任务可以请求"立即唤醒"或"下次心跳时"。
## 快速开始(可操作)
@ -85,14 +83,6 @@ openclaw cron add \
2. **选择运行位置**
- `sessionTarget: "main"` → 在下一次心跳时使用主会话上下文运行。
- `sessionTarget: "isolated"` → 在 `cron:<jobId>` 中运行专用智能体轮次。
- `sessionTarget: "current"` → 绑定到当前会话(创建时解析为 `session:<sessionKey>`)。
- `sessionTarget: "session:custom-id"` → 在持久化的命名会话中运行,跨运行保持上下文。
默认行为(保持不变):
- `systemEvent` 负载默认使用 `main`
- `agentTurn` 负载默认使用 `isolated`
要使用当前会话绑定,需显式设置 `sessionTarget: "current"`
3. **选择负载**
- 主会话 → `payload.kind = "systemEvent"`
@ -139,13 +129,12 @@ Cron 表达式使用 `croner`。如果省略时区,将使用 Gateway网关主
#### 隔离任务(专用定时会话)
隔离任务在会话 `cron:<jobId>` 或自定义会话中运行专用智能体轮次。
隔离任务在会话 `cron:<jobId>` 中运行专用智能体轮次。
关键行为:
- 提示以 `[cron:<jobId> <任务名称>]` 为前缀,便于追踪。
- 每次运行都会启动一个**全新的会话 ID**(不继承之前的对话),除非使用自定义会话。
- 自定义会话(`session:xxx`)可跨运行保持上下文,适用于如每日站会等需要基于前次摘要的工作流。
- 每次运行都会启动一个**全新的会话 ID**(不继承之前的对话)。
- 如果未指定 `delivery`隔离任务会默认以“announce”方式投递摘要。
- `delivery.mode` 可选 `announce`(投递摘要)或 `none`(内部运行)。

View File

@ -149,11 +149,7 @@ Lark国际版请使用 https://open.larksuite.com/app并在配置中设
**事件订阅** 页面:
1. 选择 **使用长连接接收事件**WebSocket 模式)
2. 添加事件:
- `im.message.receive_v1`
- `im.message.reaction.created_v1`
- `im.message.reaction.deleted_v1`
- `application.bot.menu_v6`
2. 添加事件:`im.message.receive_v1`(接收消息)
⚠️ **注意**:如果网关未启动或渠道未添加,长连接设置将保存失败。
@ -439,7 +435,7 @@ openclaw pairing list feishu
| `/reset` | 重置对话会话 |
| `/model` | 查看/切换模型 |
飞书机器人菜单建议直接在飞书开放平台的机器人能力页面配置。OpenClaw 当前支持接收 `application.bot.menu_v6` 事件,并把点击事件转换成普通文本命令(例如 `/menu <eventKey>`)继续走现有消息路由,但不通过渠道配置自动创建或同步菜单
> 注意:飞书目前不支持原生命令菜单,命令需要以文本形式发送
## 网关管理命令
@ -530,11 +526,7 @@ openclaw pairing list feishu
channels: {
feishu: {
streaming: true, // 启用流式卡片输出(默认 true
blockStreamingCoalesce: {
enabled: true,
minDelayMs: 50,
maxDelayMs: 250,
},
blockStreaming: true, // 启用块级流式(默认 true
},
},
}
@ -542,40 +534,6 @@ openclaw pairing list feishu
如需禁用流式输出(等待完整回复后一次性发送),可设置 `streaming: false`
### 交互式卡片
OpenClaw 默认会在需要时发送 Markdown 卡片;如果你需要完整的 Feishu 原生交互式卡片,也可以显式发送原始 `card` payload。
- 默认路径:文本自动渲染或 Markdown 卡片
- 显式卡片:通过消息动作的 `card` 参数发送原始交互卡片
- 更新卡片:同一消息支持后续 patch/update
卡片按钮回调当前走文本回退路径:
- 若 `action.value.text` 存在,则作为入站文本继续处理
- 若 `action.value.command` 存在,则作为命令文本继续处理
- 其他对象值会序列化为 JSON 文本
这样可以保持与现有消息/命令路由兼容,而不要求下游先理解 Feishu 专有的交互 payload。
### 表情反应
飞书渠道现已完整支持表情反应生命周期:
- 接收 `reaction created`
- 接收 `reaction deleted`
- 主动添加反应
- 主动删除自身反应
- 查询消息上的反应列表
是否把入站反应转成内部消息,可通过 `reactionNotifications` 控制:
| 值 | 行为 |
| ----- | ---------------------------- |
| `off` | 不生成反应通知 |
| `own` | 仅当反应发生在机器人消息上时 |
| `all` | 所有可验证的反应都生成通知 |
### 消息引用
在群聊中,机器人的回复可以引用用户发送的原始消息,让对话上下文更加清晰。
@ -695,19 +653,14 @@ OpenClaw 默认会在需要时发送 Markdown 卡片;如果你需要完整的
| `channels.feishu.accounts.<id>.domain` | 单账号 API 域名覆盖 | `feishu` |
| `channels.feishu.dmPolicy` | 私聊策略 | `pairing` |
| `channels.feishu.allowFrom` | 私聊白名单open_id 列表) | - |
| `channels.feishu.groupPolicy` | 群组策略 | `allowlist` |
| `channels.feishu.groupPolicy` | 群组策略 | `open` |
| `channels.feishu.groupAllowFrom` | 群组白名单 | - |
| `channels.feishu.groups.<chat_id>.requireMention` | 是否需要 @提及 | `true` |
| `channels.feishu.groups.<chat_id>.enabled` | 是否启用该群组 | `true` |
| `channels.feishu.replyInThread` | 群聊回复是否进入飞书话题线程 | `disabled` |
| `channels.feishu.groupSessionScope` | 群聊会话隔离粒度 | `group` |
| `channels.feishu.textChunkLimit` | 消息分块大小 | `2000` |
| `channels.feishu.mediaMaxMb` | 媒体大小限制 | `30` |
| `channels.feishu.streaming` | 启用流式卡片输出 | `true` |
| `channels.feishu.blockStreamingCoalesce.enabled` | 启用块级流式合并 | `true` |
| `channels.feishu.typingIndicator` | 发送“正在输入”状态 | `true` |
| `channels.feishu.resolveSenderNames` | 拉取发送者名称 | `true` |
| `channels.feishu.reactionNotifications` | 入站反应通知策略 | `own` |
| `channels.feishu.blockStreaming` | 启用块级流式 | `true` |
---

View File

@ -1,6 +1,6 @@
{
"name": "@openclaw/acpx",
"version": "2026.3.14",
"version": "2026.3.13",
"description": "OpenClaw ACP runtime backend via acpx",
"type": "module",
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "@openclaw/bluebubbles",
"version": "2026.3.14",
"version": "2026.3.13",
"description": "OpenClaw BlueBubbles channel plugin",
"type": "module",
"dependencies": {

View File

@ -57,10 +57,6 @@ export type BlueBubblesAccountConfig = {
allowPrivateNetwork?: boolean;
/** Per-group configuration keyed by chat GUID or identifier. */
groups?: Record<string, BlueBubblesGroupConfig>;
/** Channel health monitor overrides for this channel/account. */
healthMonitor?: {
enabled?: boolean;
};
};
export type BlueBubblesActionConfig = {

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