Compare commits

..

4 Commits

Author SHA1 Message Date
Onur Solmaz
fd247bc20e docs: clarify namespace cycle prevention 2026-03-25 14:22:25 +01:00
Onur Solmaz
49df1773fd docs: make namespace plan a cutover 2026-03-25 14:18:53 +01:00
Onur Solmaz
7bb4105ac5 docs: add namespace plan tldr 2026-03-25 14:13:58 +01:00
Onur Solmaz
623cf440d1 docs: add plugin sdk namespace plan 2026-03-25 13:44:20 +01:00
305 changed files with 4219 additions and 15474 deletions

View File

@@ -1,7 +1,7 @@
name: Setup Node environment
description: >
Install Node 24 by default, pnpm, optionally Bun, and optionally run pnpm
install. Requires actions/checkout to run first.
Initialize submodules with retry, install Node 24 by default, pnpm, optionally Bun,
and optionally run pnpm install. Requires actions/checkout to run first.
inputs:
node-version:
description: Node.js version to install.
@@ -34,6 +34,20 @@ inputs:
runs:
using: composite
steps:
- name: Checkout submodules (retry)
shell: bash
run: |
set -euo pipefail
git submodule sync --recursive
for attempt in 1 2 3 4 5; do
if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then
exit 0
fi
echo "Submodule update failed (attempt $attempt/5). Retrying…"
sleep $((attempt * 10))
done
exit 1
- name: Setup Node.js
uses: actions/setup-node@v6
with:

View File

@@ -12,42 +12,7 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
preflight:
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
outputs:
run_bun_checks: ${{ steps.manifest.outputs.run_bun_checks }}
bun_checks_matrix: ${{ steps.manifest.outputs.bun_checks_matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-deps: "false"
use-sticky-disk: "false"
- name: Build Bun CI manifest
id: manifest
env:
OPENCLAW_CI_DOCS_ONLY: "false"
OPENCLAW_CI_DOCS_CHANGED: "false"
OPENCLAW_CI_RUN_NODE: "true"
OPENCLAW_CI_RUN_MACOS: "false"
OPENCLAW_CI_RUN_ANDROID: "false"
OPENCLAW_CI_RUN_WINDOWS: "false"
OPENCLAW_CI_RUN_SKILLS_PYTHON: "false"
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: "false"
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: '{"include":[]}'
run: node scripts/ci-write-manifest-outputs.mjs --workflow ci-bun
build-bun-artifacts:
needs: [preflight]
if: needs.preflight.outputs.run_bun_checks == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
steps:
@@ -72,14 +37,19 @@ jobs:
path: src/canvas-host/a2ui/
bun-checks:
name: ${{ matrix.check_name }}
needs: [preflight, build-bun-artifacts]
if: needs.preflight.outputs.run_bun_checks == 'true'
needs: [build-bun-artifacts]
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.bun_checks_matrix) }}
matrix:
include:
- shard_index: 1
shard_count: 2
command: OPENCLAW_TEST_ISOLATE=1 bunx vitest run --config vitest.unit.config.ts --shard 1/2
- shard_index: 2
shard_count: 2
command: OPENCLAW_TEST_ISOLATE=1 bunx vitest run --config vitest.unit.config.ts --shard 2/2
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -99,10 +69,4 @@ jobs:
path: src/canvas-host/a2ui/
- name: Run Bun test shard
env:
SHARD_COUNT: ${{ matrix.shard_count }}
SHARD_INDEX: ${{ matrix.shard_index }}
shell: bash
run: |
set -euo pipefail
OPENCLAW_TEST_ISOLATE=1 bunx vitest run --config vitest.unit.config.ts --shard "$SHARD_INDEX/$SHARD_COUNT"
run: ${{ matrix.command }}

View File

@@ -17,42 +17,26 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
# Preflight: establish routing truth and planner-owned matrices once, then let
# real work fan out from a single source of truth.
preflight:
# Scope: establish the fast global truth for this revision before the
# expensive platform and platform-specific lanes fan out.
# Detect docs-only changes to skip heavy jobs (test, build, Windows, macOS, Android).
# Keep this job focused on routing decisions so the rest of CI can fan out sooner.
# Fail-safe: if detection steps are skipped, downstream outputs fall back to
# conservative defaults that keep heavy lanes enabled.
scope:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
outputs:
docs_only: ${{ steps.manifest.outputs.docs_only }}
docs_changed: ${{ steps.manifest.outputs.docs_changed }}
run_node: ${{ steps.manifest.outputs.run_node }}
run_macos: ${{ steps.manifest.outputs.run_macos }}
run_android: ${{ steps.manifest.outputs.run_android }}
run_skills_python: ${{ steps.manifest.outputs.run_skills_python }}
run_skills_python_job: ${{ steps.manifest.outputs.run_skills_python_job }}
run_windows: ${{ steps.manifest.outputs.run_windows }}
has_changed_extensions: ${{ steps.manifest.outputs.has_changed_extensions }}
changed_extensions_matrix: ${{ steps.manifest.outputs.changed_extensions_matrix }}
run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }}
run_release_check: ${{ steps.manifest.outputs.run_release_check }}
run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }}
checks_fast_matrix: ${{ steps.manifest.outputs.checks_fast_matrix }}
run_checks: ${{ steps.manifest.outputs.run_checks }}
checks_matrix: ${{ steps.manifest.outputs.checks_matrix }}
run_extension_fast: ${{ steps.manifest.outputs.run_extension_fast }}
extension_fast_matrix: ${{ steps.manifest.outputs.extension_fast_matrix }}
run_check: ${{ steps.manifest.outputs.run_check }}
run_check_additional: ${{ steps.manifest.outputs.run_check_additional }}
run_build_smoke: ${{ steps.manifest.outputs.run_build_smoke }}
run_check_docs: ${{ steps.manifest.outputs.run_check_docs }}
run_checks_windows: ${{ steps.manifest.outputs.run_checks_windows }}
checks_windows_matrix: ${{ steps.manifest.outputs.checks_windows_matrix }}
run_macos_node: ${{ steps.manifest.outputs.run_macos_node }}
macos_node_matrix: ${{ steps.manifest.outputs.macos_node_matrix }}
run_macos_swift: ${{ steps.manifest.outputs.run_macos_swift }}
run_android_job: ${{ steps.manifest.outputs.run_android_job }}
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
docs_only: ${{ steps.docs_scope.outputs.docs_only }}
docs_changed: ${{ steps.docs_scope.outputs.docs_changed }}
run_node: ${{ steps.changed_scope.outputs.run_node || 'false' }}
run_macos: ${{ steps.changed_scope.outputs.run_macos || 'false' }}
run_android: ${{ steps.changed_scope.outputs.run_android || 'false' }}
run_skills_python: ${{ steps.changed_scope.outputs.run_skills_python || 'false' }}
run_windows: ${{ steps.changed_scope.outputs.run_windows || 'false' }}
has_changed_extensions: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
changed_extensions_matrix: ${{ steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -72,6 +56,8 @@ jobs:
id: docs_scope
uses: ./.github/actions/detect-docs-changes
# Detect which heavy areas are touched so CI can skip unrelated expensive jobs.
# Fail-safe: if skipped, downstream lanes run.
- name: Detect changed scopes
id: changed_scope
if: steps.docs_scope.outputs.docs_only != 'true'
@@ -118,20 +104,6 @@ jobs:
appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8");
EOF
- name: Build CI manifest
id: manifest
env:
OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_DOCS_CHANGED: ${{ steps.docs_scope.outputs.docs_changed }}
OPENCLAW_CI_RUN_NODE: ${{ steps.changed_scope.outputs.run_node || 'false' }}
OPENCLAW_CI_RUN_MACOS: ${{ steps.changed_scope.outputs.run_macos || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ steps.changed_scope.outputs.run_skills_python || 'false' }}
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
run: node scripts/ci-write-manifest-outputs.mjs --workflow ci
# Run the fast security/SCM checks in parallel with scope detection so the
# main Node jobs do not have to wait for Python/pre-commit setup.
security-fast:
@@ -229,12 +201,12 @@ jobs:
- name: Audit production dependencies
run: pre-commit run --config "${PRE_COMMIT_CONFIG_PATH:-.pre-commit-config.yaml}" --all-files pnpm-audit-prod
# Fanout: downstream lanes branch from preflight outputs instead of waiting
# on unrelated Linux checks.
# Build dist once for Node-relevant changes and share it with downstream jobs.
# Keep this overlapping with the fast correctness lanes so green PRs get heavy
# test/build feedback sooner instead of waiting behind a full `check` pass.
build-artifacts:
needs: [preflight]
if: needs.preflight.outputs.run_build_artifacts == 'true'
needs: [scope]
if: needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_node == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
steps:
@@ -280,8 +252,8 @@ jobs:
# Validate npm pack contents after build (only on push to main, not PRs).
release-check:
needs: [preflight, build-artifacts]
if: needs.preflight.outputs.run_release_check == 'true'
needs: [scope, build-artifacts]
if: github.event_name == 'push' && needs.scope.outputs.docs_only != 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
steps:
@@ -307,14 +279,22 @@ jobs:
run: pnpm release:check
checks-fast:
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
needs: [scope]
if: always() && needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_node == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_matrix) }}
matrix:
include:
- runtime: node
task: extensions
command: pnpm test:extensions
- runtime: node
task: contracts-protocol
command: |
pnpm test:contracts
pnpm protocol:check
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -329,34 +309,64 @@ jobs:
use-sticky-disk: "false"
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
env:
TASK: ${{ matrix.task }}
shell: bash
run: |
set -euo pipefail
case "$TASK" in
extensions)
pnpm test:extensions
;;
contracts|contracts-protocol)
pnpm test:contracts
pnpm protocol:check
;;
*)
echo "Unsupported checks-fast task: $TASK" >&2
exit 1
;;
esac
run: ${{ matrix.command }}
checks:
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success'
needs: [scope, build-artifacts]
if: always() && needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_node == 'true' && needs.build-artifacts.result == 'success'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.checks_matrix) }}
matrix:
include:
- runtime: node
task: test
shard_index: 1
shard_count: 4
command: pnpm test
- runtime: node
task: test
shard_index: 2
shard_count: 4
command: pnpm test
- runtime: node
task: test
shard_index: 3
shard_count: 4
command: pnpm test
- runtime: node
task: test
shard_index: 4
shard_count: 4
command: pnpm test
- runtime: node
task: channels
shard_index: 1
shard_count: 3
command: pnpm test:channels
- runtime: node
task: channels
shard_index: 2
shard_count: 3
command: pnpm test:channels
- runtime: node
task: channels
shard_index: 3
shard_count: 3
command: pnpm test:channels
- runtime: node
task: compat-node22
node_version: "22.x"
cache_key_suffix: "node22"
command: |
pnpm build
pnpm ui:build
node openclaw.mjs --help
node openclaw.mjs status --json --timeout 1
pnpm test:build:singleton
node scripts/stage-bundled-plugin-runtime-deps.mjs
node --import tsx scripts/release-check.ts
steps:
- name: Skip compatibility lanes on pull requests
if: github.event_name == 'pull_request' && matrix.task == 'compat-node22'
@@ -381,7 +391,6 @@ jobs:
- name: Configure Node test resources
if: (github.event_name != 'pull_request' || matrix.task != 'compat-node22') && matrix.runtime == 'node' && (matrix.task == 'test' || matrix.task == 'channels' || matrix.task == 'compat-node22')
env:
TASK: ${{ matrix.task }}
SHARD_COUNT: ${{ matrix.shard_count || '' }}
SHARD_INDEX: ${{ matrix.shard_index || '' }}
run: |
@@ -389,7 +398,7 @@ jobs:
# Default heap limits have been too low on Linux CI (V8 OOM near 4GB).
echo "OPENCLAW_TEST_WORKERS=2" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144" >> "$GITHUB_ENV"
if [ "$TASK" = "channels" ]; then
if [ "${{ matrix.task }}" = "channels" ]; then
echo "OPENCLAW_TEST_WORKERS=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_ISOLATE=1" >> "$GITHUB_ENV"
fi
@@ -414,42 +423,17 @@ jobs:
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
if: github.event_name != 'pull_request' || matrix.task != 'compat-node22'
env:
TASK: ${{ matrix.task }}
shell: bash
run: |
set -euo pipefail
case "$TASK" in
test)
pnpm test
;;
channels)
pnpm test:channels
;;
compat-node22)
pnpm build
pnpm ui:build
node openclaw.mjs --help
node openclaw.mjs status --json --timeout 1
pnpm test:build:singleton
node scripts/stage-bundled-plugin-runtime-deps.mjs
node --import tsx scripts/release-check.ts
;;
*)
echo "Unsupported checks task: $TASK" >&2
exit 1
;;
esac
run: ${{ matrix.command }}
extension-fast:
name: "extension-fast"
needs: [preflight]
if: needs.preflight.outputs.run_extension_fast == 'true'
needs: [scope]
if: needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_node == 'true' && needs.scope.outputs.has_changed_extensions == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.extension_fast_matrix) }}
matrix: ${{ fromJson(needs.scope.outputs.changed_extensions_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -471,8 +455,8 @@ jobs:
# Types, lint, and format check.
check:
name: "check"
needs: [preflight]
if: always() && needs.preflight.outputs.run_check == 'true'
needs: [scope]
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && needs.scope.outputs.docs_only != 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
steps:
@@ -496,8 +480,8 @@ jobs:
check-additional:
name: "check-additional"
needs: [preflight]
if: always() && needs.preflight.outputs.run_check_additional == 'true'
needs: [scope]
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && needs.scope.outputs.docs_only != 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
steps:
@@ -595,8 +579,8 @@ jobs:
build-smoke:
name: "build-smoke"
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_build_smoke == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success')
needs: [scope, build-artifacts]
if: always() && needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_node == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success')
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
steps:
@@ -637,8 +621,8 @@ jobs:
# Validate docs (format, lint, broken links) only when docs files changed.
check-docs:
needs: [preflight]
if: needs.preflight.outputs.run_check_docs == 'true'
needs: [scope]
if: needs.scope.outputs.docs_changed == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
steps:
@@ -658,8 +642,8 @@ jobs:
run: pnpm check:docs
skills-python:
needs: [preflight]
if: needs.preflight.outputs.run_skills_python_job == 'true'
needs: [scope]
if: needs.scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.scope.outputs.run_skills_python == 'true')
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
steps:
@@ -686,9 +670,8 @@ jobs:
run: python -m pytest -q skills
checks-windows:
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_checks_windows == 'true' && needs.build-artifacts.result == 'success'
needs: [scope, build-artifacts]
if: always() && needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_windows == 'true' && needs.build-artifacts.result == 'success'
runs-on: blacksmith-32vcpu-windows-2025
timeout-minutes: 20
env:
@@ -701,7 +684,53 @@ jobs:
shell: bash
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.checks_windows_matrix) }}
matrix:
include:
- runtime: node
task: test
shard_index: 1
shard_count: 9
command: pnpm test
- runtime: node
task: test
shard_index: 2
shard_count: 9
command: pnpm test
- runtime: node
task: test
shard_index: 3
shard_count: 9
command: pnpm test
- runtime: node
task: test
shard_index: 4
shard_count: 9
command: pnpm test
- runtime: node
task: test
shard_index: 5
shard_count: 9
command: pnpm test
- runtime: node
task: test
shard_index: 6
shard_count: 9
command: pnpm test
- runtime: node
task: test
shard_index: 7
shard_count: 9
command: pnpm test
- runtime: node
task: test
shard_index: 8
shard_count: 9
command: pnpm test
- runtime: node
task: test
shard_index: 9
shard_count: 9
command: pnpm test
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -770,12 +799,9 @@ jobs:
- name: Configure test shard (Windows)
if: matrix.task == 'test'
env:
SHARD_COUNT: ${{ matrix.shard_count }}
SHARD_INDEX: ${{ matrix.shard_index }}
run: |
echo "OPENCLAW_TEST_SHARDS=$SHARD_COUNT" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_SHARD_INDEX=$SHARD_INDEX" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_SHARDS=${{ matrix.shard_count }}" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_SHARD_INDEX=${{ matrix.shard_index }}" >> "$GITHUB_ENV"
- name: Download dist artifact
if: matrix.task == 'test'
@@ -792,30 +818,17 @@ jobs:
path: src/canvas-host/a2ui/
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
env:
TASK: ${{ matrix.task }}
shell: bash
run: |
set -euo pipefail
case "$TASK" in
test)
pnpm test
;;
*)
echo "Unsupported Windows checks task: $TASK" >&2
exit 1
;;
esac
run: ${{ matrix.command }}
macos-node:
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_macos_node == 'true' && needs.build-artifacts.result == 'success'
# Consolidated macOS job: runs TS tests + Swift lint/build/test sequentially
# on a single runner. GitHub limits macOS concurrent jobs to 5 per org;
# running 4 separate jobs per PR (as before) starved the queue. One job
# per PR allows 5 PRs to run macOS checks simultaneously.
macos:
needs: [scope]
if: github.event_name == 'pull_request' && needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_macos == 'true'
runs-on: macos-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.macos_node_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -828,56 +841,16 @@ jobs:
with:
install-bun: "false"
- name: Download dist artifact
uses: actions/download-artifact@v8
with:
name: dist-build
path: dist/
- name: Download A2UI bundle artifact
uses: actions/download-artifact@v8
with:
name: canvas-a2ui-bundle
path: src/canvas-host/a2ui/
- name: Configure test shard (macOS)
env:
SHARD_COUNT: ${{ matrix.shard_count }}
SHARD_INDEX: ${{ matrix.shard_index }}
run: |
echo "OPENCLAW_TEST_SHARDS=$SHARD_COUNT" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_SHARD_INDEX=$SHARD_INDEX" >> "$GITHUB_ENV"
- name: Build dist (macOS)
run: pnpm build
# --- Run all checks sequentially (fast gates first) ---
- name: TS tests (macOS)
env:
NODE_OPTIONS: --max-old-space-size=4096
TASK: ${{ matrix.task }}
shell: bash
run: |
set -euo pipefail
case "$TASK" in
test)
pnpm test
;;
*)
echo "Unsupported macOS node task: $TASK" >&2
exit 1
;;
esac
macos-swift:
name: "macos-swift"
needs: [preflight]
if: needs.preflight.outputs.run_macos_swift == 'true'
runs-on: macos-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: false
run: pnpm test
# --- Xcode/Swift setup ---
- name: Select Xcode 26.1
run: |
sudo xcode-select -s /Applications/Xcode_26.1.app
@@ -886,14 +859,6 @@ jobs:
- name: Install XcodeGen / SwiftLint / SwiftFormat
run: brew install xcodegen swiftlint swiftformat
- name: Cache SwiftPM
uses: actions/cache@v5
with:
path: ~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-swiftpm-${{ hashFiles('apps/macos/Package.resolved') }}
restore-keys: |
${{ runner.os }}-swiftpm-
- name: Show toolchain
run: |
sw_vers
@@ -905,6 +870,14 @@ jobs:
swiftlint --config .swiftlint.yml
swiftformat --lint apps/macos/Sources --config .swiftformat
- name: Cache SwiftPM
uses: actions/cache@v5
with:
path: ~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-swiftpm-${{ hashFiles('apps/macos/Package.resolved') }}
restore-keys: |
${{ runner.os }}-swiftpm-
- name: Swift build (release)
run: |
set -euo pipefail
@@ -930,14 +903,22 @@ jobs:
exit 1
android:
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_android_job == 'true'
needs: [scope]
if: needs.scope.outputs.docs_only != 'true' && needs.scope.outputs.run_android == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 20
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.android_matrix) }}
matrix:
include:
- task: test-play
command: ./gradlew --no-daemon :app:testPlayDebugUnitTest
- task: test-third-party
command: ./gradlew --no-daemon :app:testThirdPartyDebugUnitTest
- task: build-play
command: ./gradlew --no-daemon :app:assemblePlayDebug
- task: build-third-party
command: ./gradlew --no-daemon :app:assembleThirdPartyDebug
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -986,26 +967,4 @@ jobs:
- name: Run Android ${{ matrix.task }}
working-directory: apps/android
env:
TASK: ${{ matrix.task }}
shell: bash
run: |
set -euo pipefail
case "$TASK" in
test-play)
./gradlew --no-daemon :app:testPlayDebugUnitTest
;;
test-third-party)
./gradlew --no-daemon :app:testThirdPartyDebugUnitTest
;;
build-play)
./gradlew --no-daemon :app:assemblePlayDebug
;;
build-third-party)
./gradlew --no-daemon :app:assembleThirdPartyDebug
;;
*)
echo "Unsupported Android task: $TASK" >&2
exit 1
;;
esac
run: ${{ matrix.command }}

View File

@@ -15,34 +15,49 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
preflight:
docs-scope:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-16vcpu-ubuntu-2404
outputs:
docs_only: ${{ steps.manifest.outputs.docs_only }}
run_install_smoke: ${{ steps.manifest.outputs.run_install_smoke }}
docs_only: ${{ steps.check.outputs.docs_only }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Ensure preflight base commit
- name: Ensure docs-scope base commit
uses: ./.github/actions/ensure-base-commit
with:
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
- name: Detect docs-only changes
id: docs_scope
id: check
uses: ./.github/actions/detect-docs-changes
changed-smoke:
needs: [docs-scope]
if: needs.docs-scope.outputs.docs_only != 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
outputs:
run_changed_smoke: ${{ steps.scope.outputs.run_changed_smoke }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
fetch-tags: false
- name: Ensure changed-smoke base commit
uses: ./.github/actions/ensure-base-commit
with:
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
- name: Detect changed smoke scope
id: changed_scope
if: steps.docs_scope.outputs.docs_only != 'true'
id: scope
shell: bash
run: |
set -euo pipefail
@@ -55,32 +70,9 @@ jobs:
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
- name: Setup Node environment
if: steps.docs_scope.outputs.docs_only != 'true'
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-deps: "false"
use-sticky-disk: "false"
- name: Build install-smoke CI manifest
id: manifest
env:
OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_DOCS_CHANGED: "false"
OPENCLAW_CI_RUN_NODE: "false"
OPENCLAW_CI_RUN_MACOS: "false"
OPENCLAW_CI_RUN_ANDROID: "false"
OPENCLAW_CI_RUN_WINDOWS: "false"
OPENCLAW_CI_RUN_SKILLS_PYTHON: "false"
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: "false"
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: '{"include":[]}'
OPENCLAW_CI_RUN_CHANGED_SMOKE: ${{ steps.changed_scope.outputs.run_changed_smoke || 'false' }}
run: node scripts/ci-write-manifest-outputs.mjs --workflow install-smoke
install-smoke:
needs: [preflight]
if: needs.preflight.outputs.run_install_smoke == 'true'
needs: [docs-scope, changed-smoke]
if: (github.event_name != 'pull_request' || !github.event.pull_request.draft) && needs.docs-scope.outputs.docs_only != 'true' && needs.changed-smoke.outputs.run_changed_smoke == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
env:
DOCKER_BUILD_SUMMARY: "false"

View File

@@ -1,108 +0,0 @@
name: Thread Expansion Experiment
on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
compare:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
strategy:
fail-fast: false
matrix:
include:
- label: linux
runner: blacksmith-16vcpu-ubuntu-2404
mode: forks
- label: linux
runner: blacksmith-16vcpu-ubuntu-2404
mode: threads
- label: macos
runner: macos-latest
mode: forks
- label: macos
runner: macos-latest
mode: threads
name: compare (${{ matrix.label }}, ${{ matrix.mode }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
env:
EXPERIMENT_MODE: ${{ matrix.mode }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "false"
- name: Show runtime info
run: |
node -v
pnpm -v
node -e 'const os=require("node:os"); console.log(JSON.stringify({ platform: process.platform, cpuCount: os.cpus().length, totalMemGiB: Math.round((os.totalmem() / 1024 / 1024 / 1024) * 10) / 10, loadavg: os.loadavg() }, null, 2));'
- name: Run thread expansion experiment
shell: bash
run: |
set -euo pipefail
case "$EXPERIMENT_MODE" in
forks)
export OPENCLAW_TEST_FORCE_FORKS=1
POOL="forks"
;;
threads)
export OPENCLAW_TEST_FORCE_THREADS=1
POOL="threads"
;;
*)
echo "Unsupported EXPERIMENT_MODE=$EXPERIMENT_MODE" >&2
exit 1
;;
esac
POLICY_FILES=(
src/commands/agents.test.ts
src/commands/text-format.test.ts
src/auto-reply/chunk.test.ts
)
EVIDENCE_FILES=(
src/commands/backup.test.ts
src/auto-reply/reply/commands-acp/install-hints.test.ts
src/agents/pi-extensions/compaction-safeguard.test.ts
)
echo "## ${RUNNER_OS} / ${EXPERIMENT_MODE}" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- Wrapper policy sample files: \`${POLICY_FILES[*]}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Direct evidence sample files: \`${EVIDENCE_FILES[*]}\`" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "=== Wrapper policy sample (${EXPERIMENT_MODE}) ==="
SECONDS=0
OPENCLAW_TEST_SHOW_POOL_DECISION=1 node scripts/test-parallel.mjs "${POLICY_FILES[@]}"
wrapper_elapsed=$SECONDS
echo "- wrapper policy sample (${EXPERIMENT_MODE}): ${wrapper_elapsed}s" >> "$GITHUB_STEP_SUMMARY"
echo "=== Direct vitest evidence sample (${POOL}) ==="
SECONDS=0
pnpm vitest run --config vitest.config.ts "--pool=${POOL}" "${EVIDENCE_FILES[@]}"
direct_elapsed=$SECONDS
echo "- direct evidence sample (${POOL}): ${direct_elapsed}s" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -119,8 +119,8 @@
- Agents MUST NOT modify baseline, inventory, ignore, snapshot, or expected-failure files to silence failing checks without explicit approval in this chat.
- For targeted/local debugging, keep using the wrapper: `pnpm test -- <path-or-filter> [vitest args...]` (for example `pnpm test -- src/commands/onboard-search.test.ts -t "shows registered plugin providers"`); do not default to raw `pnpm vitest run ...` because it bypasses wrapper config/profile/pool routing.
- Do not set test workers above 16; tried already.
- Keep Vitest on `forks` only. Do not introduce or reintroduce any non-`forks` Vitest pool or alternate execution mode in configs, wrapper scripts, or default test commands without explicit approval in this chat. This includes `threads`, `vmThreads`, `vmForks`, and any future/nonstandard pool variant.
- If local Vitest runs cause memory pressure, the wrapper now derives budgets from host capabilities (CPU, memory band, current load). For a conservative explicit override during land/gate runs, use `OPENCLAW_TEST_PROFILE=serial OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test`.
- Do not reintroduce Vitest VM pools by default without fresh green evidence on current `main`; keep CI on `forks`.
- If local Vitest runs cause memory pressure (common on non-Mac-Studio hosts), use `OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test` for land/gate runs.
- Live tests (real keys): `OPENCLAW_LIVE_TEST=1 pnpm test:live` (OpenClaw-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
- Full kit + whats covered: `docs/help/testing.md`.
- Changelog: user-facing changes only; no internal/meta notes (version alignment, appcast reminders, release process).

View File

@@ -8,88 +8,8 @@ Docs: https://docs.openclaw.ai
### Changes
- MiniMax: add image generation provider for `image-01` model, supporting generate and image-to-image editing with aspect ratio control. (#54487) Thanks @liyuan97.
- MiniMax: trim model catalog to M2.7 only, removing legacy M2, M2.1, M2.5, and VL-01 models. (#54487) Thanks @liyuan97.
- Plugins/runtime: expose `runHeartbeatOnce` in the plugin runtime `system` namespace so plugins can trigger a single heartbeat cycle with an explicit delivery target override (e.g. `heartbeat: { target: "last" }`). (#40299) Thanks @loveyana.
- Agents/compaction: surface safeguard-specific cancel reasons and relabel benign manual `/compact` no-op cases as skipped instead of failed. (#51072) Thanks @afurm.
- Agents/compaction: preserve the post-compaction AGENTS refresh on stale-usage preflight compaction for both immediate replies and queued followups. (#49479) Thanks @jared596.
- CLI: add `openclaw config schema` to print the generated JSON schema for `openclaw.json`. (#54523) Thanks @kvokka.
### Fixes
- Telegram: deliver verbose tool summaries inside forum topic sessions again, so threaded topic chats now match DM verbose behavior. (#43236) Thanks @frankbuild.
- Agents/sandbox: honor `tools.sandbox.tools.alsoAllow`, let explicit sandbox re-allows remove matching built-in default-deny tools, and keep sandbox explain/error guidance aligned with the effective sandbox tool policy. (#54492) Thanks @ngutman.
- Agents/sandbox: make blocked-tool guidance glob-aware again, redact/sanitize session-specific explain hints for safer copy-paste, and avoid leaking control-character session keys in those hints. (#54684) Thanks @ngutman.
- Feishu: close WebSocket connections on monitor stop/abort so ghost connections no longer persist, preventing duplicate event processing and resource leaks across restart cycles. (#52844) Thanks @schumilin.
- Feishu: use the original message `create_time` instead of `Date.now()` for inbound timestamps so offline-retried messages carry the correct authoring time, preventing mis-targeted agent actions on stale instructions. (#52809) Thanks @schumilin.
- Daemon/Linux: stop flagging non-gateway systemd services as duplicate gateways just because their unit files mention OpenClaw, reducing false-positive doctor/log noise. (#45328) Thanks @gregretkowski.
- Plugins/SDK: thread `moduleUrl` through plugin-sdk alias resolution so user-installed plugins outside the openclaw directory (e.g. `~/.openclaw/extensions/`) correctly resolve `openclaw/plugin-sdk/*` subpath imports, and gate `plugin-sdk:check-exports` in `release:check`. (#54283) Thanks @xieyongliang.
- Telegram/pairing: ignore self-authored DM `message` updates so bot-pinned status cards and similar service updates do not trigger bogus pairing requests or re-enter inbound dispatch. (#54530) thanks @huntharo
- iMessage: stop leaking inline `[[reply_to:...]]` tags into delivered text by sending `reply_to` as RPC metadata and stripping stray directive tags from outbound messages. (#39512) Thanks @mvanhorn.
- Agents/embedded replies: surface mid-turn 429 and overload failures when embedded runs end without a user-visible reply, while preserving successful media-only replies that still use legacy `mediaUrl`. (#50930) Thanks @infichen.
- Agents/compaction: trigger timeout recovery compaction before retrying high-context LLM timeouts so embedded runs stop repeating oversized requests. (#46417) thanks @joeykrug.
- Microsoft Teams/config: accept the existing `welcomeCard`, `groupWelcomeCard`, `promptStarters`, and feedback/reflection keys in strict config validation so already-supported Teams runtime settings stop failing schema checks. (#54679) Thanks @gumclaw.
- Agents/status: use provider-aware context window lookup for fresh Anthropic 4.6 model overrides so `/status` shows the correct 1.0m window instead of an underreported shared-cache minimum. (#54796) Thanks @neeravmakwana.
- CLI/message send: write manual `openclaw message send` deliveries into the resolved agent session transcript again by always threading the default CLI agent through outbound mirroring. (#54187) Thanks @KevInTheCloud5617.
- CLI/onboarding: show the Kimi Code API key option again in the Moonshot setup menu so the interactive picker includes all Kimi setup paths together. Fixes #54412 Thanks @sparkyrider
## 2026.3.24
### Breaking
### Changes
- Gateway/OpenAI compatibility: add `/v1/models` and `/v1/embeddings`, and forward explicit model overrides through `/v1/chat/completions` and `/v1/responses` for broader client and RAG compatibility. Thanks @vincentkoc.
- Agents/tools: make `/tools` show the tools the current agent can actually use right now, add a compact default view with an optional detailed mode, and add a live "Available Right Now" section in the Control UI so it is easier to see what will work before you ask.
- Microsoft Teams: migrate to the official Teams SDK and add AI-agent UX best practices including streaming 1:1 replies, welcome cards with prompt starters, feedback/reflection, informative status updates, typing indicators, and native AI labeling. (#51808)
- Microsoft Teams: add message edit and delete support for sent messages, including in-thread fallbacks when no explicit target is provided. (#49925)
- Skills/install metadata: add one-click install recipes to bundled skills (coding-agent, gh-issues, openai-whisper-api, session-logs, tmux, trello, weather) so the CLI and Control UI can offer dependency installation when requirements are missing. (#53411) Thanks @BunsDev.
- Control UI/skills: add status-filter tabs (All / Ready / Needs Setup / Disabled) with counts, replace inline skill cards with a click-to-detail dialog showing requirements, toggle switch, install action, API key entry, source metadata, and homepage link. (#53411) Thanks @BunsDev.
- Slack/interactive replies: restore rich reply parity for direct deliveries, auto-render simple trailing `Options:` lines as buttons/selects, improve Slack interactive setup defaults, and isolate reply controls from plugin interactive handlers. (#53389) Thanks @vincentkoc.
- CLI/containers: add `--container` and `OPENCLAW_CONTAINER` to run `openclaw` commands inside a running Docker or Podman OpenClaw container. (#52651) Thanks @sallyom.
- Discord/auto threads: add optional `autoThreadName: "generated"` naming so new auto-created threads can be renamed asynchronously with concise LLM-generated titles while keeping the existing message-based naming as the default. (#43366) Thanks @davidguttman.
- Plugins/hooks: add `before_dispatch` with canonical inbound metadata and route handled replies through the normal final-delivery path, preserving TTS and routed delivery semantics. (#50444) Thanks @gfzhx.
- Control UI/agents: convert agent workspace file rows to expandable `<details>` with lazy-loaded inline markdown preview, and add comprehensive `.sidebar-markdown` styles for headings, lists, code blocks, tables, blockquotes, and details/summary elements. (#53411) Thanks @BunsDev.
- Control UI/markdown preview: restyle the agent workspace file preview dialog with a frosted backdrop, sized panel, and styled header, and integrate `@create-markdown/preview` v2 system theme for rich markdown rendering (headings, tables, code blocks, callouts, blockquotes) that auto-adapts to the app's light/dark design tokens. (#53411) Thanks @BunsDev.
- macOS app/config: replace horizontal pill-based subsection navigation with a collapsible tree sidebar using disclosure chevrons and indented subsection rows. (#53411) Thanks @BunsDev.
- CLI/skills: soften missing-requirements label from "missing" to "needs setup" and surface API key setup guidance (where to get a key, CLI save command, storage path) in `openclaw skills info` output. (#53411) Thanks @BunsDev.
- macOS app/skills: add "Get your key" homepage link and storage-path hint to the API key editor dialog, and show the config path in save confirmation messages. (#53411) Thanks @BunsDev.
- Control UI/agents: add a "Not set" placeholder to the default agent model selector dropdown. (#53411) Thanks @BunsDev.
- Runtime/install: lower the supported Node 22 floor to `22.14+` while continuing to recommend Node 24, so npm installs and self-updates do not strand Node 22.14 users on older releases.
- CLI/update: preflight the target npm package `engines.node` before `openclaw update` runs a global package install, so outdated Node runtimes fail with a clear upgrade message instead of attempting an unsupported latest release.
### Fixes
- Outbound media/local files: align outbound media access with the configured fs policy so host-local files and inbound-media paths keep sending when `workspaceOnly` is off, while strict workspace-only agents remain sandboxed.
- Security/sandbox media dispatch: close the `mediaUrl`/`fileUrl` alias bypass so outbound tool and message actions cannot escape media-root restrictions. (#54034)
- Gateway/restart sentinel: wake the interrupted agent session via heartbeat after restart instead of only sending a best-effort restart note, retry outbound delivery once on transient failure, and preserve explicit thread/topic routing through the wake path so replies land in the correct Telegram topic or Slack thread. (#53940) Thanks @VACInc.
- Docker/setup: avoid the pre-start `openclaw-cli` shared-network namespace loop by routing setup-time onboard/config writes through `openclaw-gateway`, so fresh Docker installs stop failing before the gateway comes up. (#53385) Thanks @amsminn.
- Gateway/channels: keep channel startup sequential while isolating per-channel boot failures, so one broken channel no longer blocks later channels from starting. (#54215) Thanks @JonathanJing.
- Embedded runs/secrets: stop unresolved `SecretRef` config from crashing embedded agent runs by falling back to the resolved runtime snapshot when needed. Fixes #45838.
- WhatsApp/groups: track recent gateway-sent message IDs and suppress only matching group echoes, preserving owner `/status`, `/new`, and `/activation` commands from linked-account `fromMe` traffic. (#53624) Thanks @w-sss.
- WhatsApp/reply-to-bot detection: restore implicit group reply detection by unwrapping `botInvokeMessage` payloads and reading `selfLid` from `creds.json`, so reply-based mentions reach the bot again in linked-account group chats.
- Telegram/forum topics: recover `#General` topic `1` routing when Telegram omits forum metadata, including native commands, interactive callbacks, inbound message context, and fallback error replies. (#53699) thanks @huntharo
- Discord/gateway supervision: centralize gateway error handling behind a lifetime-owned supervisor so early, active, and late-teardown Carbon gateway errors stay classified consistently and stop surfacing as process-killing teardown crashes.
- Discord/timeouts: send a visible timeout reply when the inbound Discord worker times out before a final reply starts, including created auto-thread targets and queued-run ordering. (#53823) Thanks @Kimbo7870.
- ACP/direct chats: always deliver a terminal ACP result when final TTS does not yield audio, even if block text already streamed earlier, and skip redundant empty-text final synthesis. (#53692) Thanks @w-sss.
- Telegram/outbound errors: preserve actionable 403 membership/block/kick details and treat `bot not a member` as a permanent delivery failure so Telegram sends stop retrying doomed chats. (#53635) Thanks @w-sss.
- Telegram/photos: preflight Telegram photo dimension and aspect-ratio rules, and fall back to document sends when image metadata is invalid or unavailable so photo uploads stop failing with `PHOTO_INVALID_DIMENSIONS`. (#52545) Thanks @hnshah.
- Slack/runtime defaults: trim Slack DM reply overhead, restore Codex auto transport, and tighten Slack/web-search runtime defaults around DM preview threading, cache scoping, warning dedupe, and explicit web-search opt-in. (#53957) Thanks @vincentkoc.
- WhatsApp/allowFrom: show a specific allowFrom policy error for valid blocked targets instead of the misleading `<E.164|group JID>` format hint. Thanks @mcaxtr.
## 2026.3.24-beta.2
### Breaking
### Changes
### Fixes
- Outbound media/local files: align outbound media access with the configured fs policy so host-local files and inbound-media paths keep sending when `workspaceOnly` is off, while strict workspace-only agents remain sandboxed.
- Runtime/install: lower the supported Node 22 floor to `22.14+` while continuing to recommend Node 24, so npm installs and self-updates do not strand Node 22.14 users on older releases.
- CLI/update: preflight the target npm package `engines.node` before `openclaw update` runs a global package install, so outdated Node runtimes fail with a clear upgrade message instead of attempting an unsupported latest release.
- Tests/security audit: isolate audit-test home and personal skill resolution so local `~/.agents/skills` installs no longer make maintainer prep runs fail nondeterministically. (#54473) thanks @huntharo
## 2026.3.24-beta.1
### Breaking
@@ -154,10 +74,6 @@ Docs: https://docs.openclaw.ai
- Marketplace/agents: correct the ClawHub skill URL in agent docs and stream marketplace archive downloads to disk so installs avoid excess memory use and fail cleanly on empty responses. (#54160) Thanks @QuinnH496.
- Discord/config types: add missing `autoArchiveDuration` to `DiscordGuildChannelConfig` so TypeScript config definitions match the existing schema and runtime support. (#43427) Thanks @davidguttman.
- Docs/IRC: fix five `json55` code-fence typos in the IRC channel examples so Mintlify applies JSON5 syntax highlighting correctly. (#50842) Thanks @Hollychou924.
- Discord/commands: trim overlong slash-command descriptions to Discord's 100-character limit and map rejected deploy indexes from Discord validation payloads back to command names/descriptions, so deploys stop failing on long descriptions and startup logs identify the rejected commands. (#54118) thanks @huntharo
- Agents/cooldowns: scope rate-limit cooldowns per model so one 429 no longer blocks every model on the same auth profile, replace the exponential 1 min → 1 h escalation with a stepped 30 s / 1 min / 5 min ladder, and surface a user-facing countdown message when all models are rate-limited. (#49834) Thanks @kiranvk-2011.
- Config/web fetch: allow the documented `tools.web.fetch.maxResponseBytes` setting in runtime schema validation so valid configs no longer fail with unrecognized-key errors. (#53401) Thanks @erhhung.
- Message tool/buttons: keep the shared `buttons` schema optional in merged tool definitions so plain `action=send` calls stop failing validation when no buttons are provided. (#54418) Thanks @adzendo.
## 2026.3.23
@@ -438,8 +354,6 @@ Docs: https://docs.openclaw.ai
- 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. (#46790) 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.
- Gateway/Telegram shutdown: abort stalled Telegram polling fetches on shutdown, clean up per-cycle abort listeners, and keep the in-process watchdog ahead of supervisor stop timeouts so SIGTERM no longer leaves zombie gateways behind. (#51242) Thanks @juliabush.
- 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.
- 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.
- Telegram/setup: warn when setup leaves DMs on pairing without an allowlist, and show valid account-scoped remediation commands. (#50710) Thanks @ernestodeoliveira.
- Doctor/Telegram: replace the fresh-install empty group-allowlist false positive with first-run guidance that explains DM pairing approval and the next group setup steps, so new Telegram installs get actionable setup help instead of a broken-config warning. Thanks @vincentkoc.
- Doctor/extensions: keep Matrix DM `allowFrom` repairs on the canonical `dm.allowFrom` path and stop treating Zalouser group sender gating as if it fell back to `allowFrom`, so doctor warnings and `--fix` stay aligned with runtime access control. Thanks @vincentkoc.
@@ -492,8 +406,6 @@ Docs: https://docs.openclaw.ai
- Exec: harden host env override handling across gateway and node (#51207) Thanks @gladiator9797 and @joshavant.
- Voice Call: enforce spoken-output contract and fix stream TTS silence regression (#51500) Thanks @joshavant.
- xAI/models: rename the bundled Grok 4.20 catalog entries to the GA IDs and normalize saved deprecated beta IDs at runtime so existing configs and sessions keep resolving. (#50772) thanks @Jaaneek
- Plugins/Matrix TTS: send auto-TTS replies as native Matrix voice bubbles instead of generic audio attachments. (#37080) thanks @Matthew19990919.
- Plugins/Matrix TTS: send auto-TTS replies as native Matrix voice bubbles instead of generic audio attachments. (#37080) thanks @Matthew19990919.
- Agents/bootstrap warnings: move bootstrap truncation warnings out of the system prompt and into the per-turn prompt body so prompt-cache reuse stays stable when truncation warnings appear or disappear. (#48753) Thanks @scoootscooob and @obviyus.
- Telegram/DM topic session keys: route named-account DM topics through the same per-account base session key across inbound messages, native commands, and session-state lookups so `/status` and thread recovery stop creating phantom `agent:main:main:thread:...` sessions. (#48204) Thanks @vincentkoc.
- ACP/configured bindings: reinitialize configured ACP sessions that are stuck in `error` state instead of reusing the failed runtime.
@@ -513,9 +425,6 @@ Docs: https://docs.openclaw.ai
- Telegram/routing: fail loud when `message send` targets an unknown non-default Telegram `accountId`, instead of silently falling back to the channel-level bot token and sending through the wrong bot. (#50853) Thanks @hclsys.
- Web search: align onboarding, configure, and finalize with plugin-owned provider contracts, including disabled-provider recovery, config-aware credential hooks, and runtime-visible summaries. (#50935) Thanks @gumadeiras.
- Agents/replay: sanitize malformed assistant tool-call replay blocks before provider replay so follow-up Anthropic requests do not inherit the downstream `replace` crash. (#50005) Thanks @jalehman.
- Plugins/context engines: retry strict legacy `assemble()` calls without the new `prompt` field when older engines reject it, preserving prompt-aware retrieval compatibility for pre-prompt plugins. (#50848) thanks @danhdoan.
- CLI/update status: explicitly say `up to date` when the local version already matches npm latest, while keeping the availability logic unchanged. (#51409) Thanks @dongzhenye.
- Agents/embedded transport errors: distinguish common network failures like connection refused, DNS lookup failure, and interrupted sockets from true timeouts in embedded-run user messaging and lifecycle diagnostics. (#51419) Thanks @scoootscooob.
- Discord/startup logging: report client initialization while the gateway is still connecting instead of claiming Discord is logged in before readiness is reached. (#51425) Thanks @scoootscoob.
- Agents/compaction safeguard: preserve split-turn context and preserved recent turns when capped retry fallback reuses the last successful summary. (#27727) thanks @Pandadadadazxf.
- Agents/memory flush: keep transcript-hash dedup active across memory-flush fallback retries so a write-then-throw flush attempt cannot append duplicate `MEMORY.md` entries before the fallback cycle completes. (#34222) Thanks @lml2468.
@@ -554,8 +463,6 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/edit tool: accept common path/text alias spellings, show current file contents on exact-match failures, and avoid false edit failures after successful writes. (#52516) thanks @mbelinky.
- Agents/compaction: reconcile `sessions.json.compactionCount` after a late embedded auto-compaction success so persisted session counts catch up once the handler reports completion. (#45493) Thanks @jackal092927.
- Mattermost/replies: keep pairing replies, slash-command fallback replies, and model-picker messages on the resolved config path so `exec:` SecretRef bot tokens work across all outbound reply branches. (#48347) thanks @mathiasnagler.
## 2026.3.13

View File

@@ -19,7 +19,7 @@
</p>
**OpenClaw** is a _personal AI assistant_ you run on your own devices.
It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, WebChat). It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WebChat). It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant.
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
@@ -74,7 +74,7 @@ openclaw gateway --port 18789 --verbose
# Send a message
openclaw message send --to +1234567890 --message "Hello from OpenClaw"
# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Google Chat/Signal/iMessage/BlueBubbles/IRC/Microsoft Teams/Matrix/Feishu/LINE/Mattermost/Nextcloud Talk/Nostr/Synology Chat/Tlon/Twitch/Zalo/Zalo Personal/WeChat/WebChat)
# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Google Chat/Signal/iMessage/BlueBubbles/IRC/Microsoft Teams/Matrix/Feishu/LINE/Mattermost/Nextcloud Talk/Nostr/Synology Chat/Tlon/Twitch/Zalo/Zalo Personal/WebChat)
openclaw agent --message "Ship checklist" --thinking high
```
@@ -126,7 +126,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
## Highlights
- **[Local-first Gateway](https://docs.openclaw.ai/gateway)** — single control plane for sessions, channels, tools, and events.
- **[Multi-channel inbox](https://docs.openclaw.ai/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, BlueBubbles (iMessage), iMessage (legacy), IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, WebChat, macOS, iOS/Android.
- **[Multi-channel inbox](https://docs.openclaw.ai/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, BlueBubbles (iMessage), iMessage (legacy), IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WebChat, macOS, iOS/Android.
- **[Multi-agent routing](https://docs.openclaw.ai/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions).
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — wake words on macOS/iOS and continuous voice on Android (ElevenLabs + system TTS fallback).
- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
@@ -150,7 +150,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
### Channels
- [Channels](https://docs.openclaw.ai/channels): [WhatsApp](https://docs.openclaw.ai/channels/whatsapp) (Baileys), [Telegram](https://docs.openclaw.ai/channels/telegram) (grammY), [Slack](https://docs.openclaw.ai/channels/slack) (Bolt), [Discord](https://docs.openclaw.ai/channels/discord) (discord.js), [Google Chat](https://docs.openclaw.ai/channels/googlechat) (Chat API), [Signal](https://docs.openclaw.ai/channels/signal) (signal-cli), [BlueBubbles](https://docs.openclaw.ai/channels/bluebubbles) (iMessage, recommended), [iMessage](https://docs.openclaw.ai/channels/imessage) (legacy imsg), [IRC](https://docs.openclaw.ai/channels/irc), [Microsoft Teams](https://docs.openclaw.ai/channels/msteams), [Matrix](https://docs.openclaw.ai/channels/matrix), [Feishu](https://docs.openclaw.ai/channels/feishu), [LINE](https://docs.openclaw.ai/channels/line), [Mattermost](https://docs.openclaw.ai/channels/mattermost), [Nextcloud Talk](https://docs.openclaw.ai/channels/nextcloud-talk), [Nostr](https://docs.openclaw.ai/channels/nostr), [Synology Chat](https://docs.openclaw.ai/channels/synology-chat), [Tlon](https://docs.openclaw.ai/channels/tlon), [Twitch](https://docs.openclaw.ai/channels/twitch), [Zalo](https://docs.openclaw.ai/channels/zalo), [Zalo Personal](https://docs.openclaw.ai/channels/zalouser), WeChat (`@tencent-weixin/openclaw-weixin`), [WebChat](https://docs.openclaw.ai/web/webchat).
- [Channels](https://docs.openclaw.ai/channels): [WhatsApp](https://docs.openclaw.ai/channels/whatsapp) (Baileys), [Telegram](https://docs.openclaw.ai/channels/telegram) (grammY), [Slack](https://docs.openclaw.ai/channels/slack) (Bolt), [Discord](https://docs.openclaw.ai/channels/discord) (discord.js), [Google Chat](https://docs.openclaw.ai/channels/googlechat) (Chat API), [Signal](https://docs.openclaw.ai/channels/signal) (signal-cli), [BlueBubbles](https://docs.openclaw.ai/channels/bluebubbles) (iMessage, recommended), [iMessage](https://docs.openclaw.ai/channels/imessage) (legacy imsg), [IRC](https://docs.openclaw.ai/channels/irc), [Microsoft Teams](https://docs.openclaw.ai/channels/msteams), [Matrix](https://docs.openclaw.ai/channels/matrix), [Feishu](https://docs.openclaw.ai/channels/feishu), [LINE](https://docs.openclaw.ai/channels/line), [Mattermost](https://docs.openclaw.ai/channels/mattermost), [Nextcloud Talk](https://docs.openclaw.ai/channels/nextcloud-talk), [Nostr](https://docs.openclaw.ai/channels/nostr), [Synology Chat](https://docs.openclaw.ai/channels/synology-chat), [Tlon](https://docs.openclaw.ai/channels/tlon), [Twitch](https://docs.openclaw.ai/channels/twitch), [Zalo](https://docs.openclaw.ai/channels/zalo), [Zalo Personal](https://docs.openclaw.ai/channels/zalouser), [WebChat](https://docs.openclaw.ai/web/webchat).
- [Group routing](https://docs.openclaw.ai/channels/group-messages): mention gating, reply tags, per-channel chunking and routing. Channel rules: [Channels](https://docs.openclaw.ai/channels).
### Apps + nodes
@@ -185,7 +185,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
## How it works (short)
```
WhatsApp / Telegram / Slack / Discord / Google Chat / Signal / iMessage / BlueBubbles / IRC / Microsoft Teams / Matrix / Feishu / LINE / Mattermost / Nextcloud Talk / Nostr / Synology Chat / Tlon / Twitch / Zalo / Zalo Personal / WeChat / WebChat
WhatsApp / Telegram / Slack / Discord / Google Chat / Signal / iMessage / BlueBubbles / IRC / Microsoft Teams / Matrix / Feishu / LINE / Mattermost / Nextcloud Talk / Nostr / Synology Chat / Tlon / Twitch / Zalo / Zalo Personal / WebChat
┌───────────────────────────────┐
@@ -397,12 +397,6 @@ Details: [Security guide](https://docs.openclaw.ai/gateway/security) · [Docker
- Configure a Teams app + Bot Framework, then add a `msteams` config section.
- Allowlist who can talk via `msteams.allowFrom`; group access via `msteams.groupAllowFrom` or `msteams.groupPolicy: "open"`.
### WeChat
- Official Tencent plugin via [`@tencent-weixin/openclaw-weixin`](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin) (iLink Bot API). Private chats only; v2.x requires OpenClaw `>=2026.3.22`.
- Install: `openclaw plugins install "@tencent-weixin/openclaw-weixin"`, then `openclaw channels login --channel openclaw-weixin` to scan the QR code.
- Requires the WeChat ClawBot plugin (WeChat > Me > Settings > Plugins); gradual rollout by Tencent.
### [WebChat](https://docs.openclaw.ai/web/webchat)
- Uses the Gateway WebSocket; no separate WebChat port/config.

View File

@@ -2,58 +2,6 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.3.24</title>
<pubDate>Wed, 25 Mar 2026 17:06:31 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026032490</sparkle:version>
<sparkle:shortVersionString>2026.3.24</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.3.24</h2>
<h3>Breaking</h3>
<h3>Changes</h3>
<ul>
<li>Gateway/OpenAI compatibility: add <code>/v1/models</code> and <code>/v1/embeddings</code>, and forward explicit model overrides through <code>/v1/chat/completions</code> and <code>/v1/responses</code> for broader client and RAG compatibility. Thanks @vincentkoc.</li>
<li>Agents/tools: make <code>/tools</code> show the tools the current agent can actually use right now, add a compact default view with an optional detailed mode, and add a live "Available Right Now" section in the Control UI so it is easier to see what will work before you ask.</li>
<li>Microsoft Teams: migrate to the official Teams SDK and add AI-agent UX best practices including streaming 1:1 replies, welcome cards with prompt starters, feedback/reflection, informative status updates, typing indicators, and native AI labeling. (#51808)</li>
<li>Microsoft Teams: add message edit and delete support for sent messages, including in-thread fallbacks when no explicit target is provided. (#49925)</li>
<li>Skills/install metadata: add one-click install recipes to bundled skills (coding-agent, gh-issues, openai-whisper-api, session-logs, tmux, trello, weather) so the CLI and Control UI can offer dependency installation when requirements are missing. (#53411) Thanks @BunsDev.</li>
<li>Control UI/skills: add status-filter tabs (All / Ready / Needs Setup / Disabled) with counts, replace inline skill cards with a click-to-detail dialog showing requirements, toggle switch, install action, API key entry, source metadata, and homepage link. (#53411) Thanks @BunsDev.</li>
<li>Slack/interactive replies: restore rich reply parity for direct deliveries, auto-render simple trailing <code>Options:</code> lines as buttons/selects, improve Slack interactive setup defaults, and isolate reply controls from plugin interactive handlers. (#53389) Thanks @vincentkoc.</li>
<li>CLI/containers: add <code>--container</code> and <code>OPENCLAW_CONTAINER</code> to run <code>openclaw</code> commands inside a running Docker or Podman OpenClaw container. (#52651) Thanks @sallyom.</li>
<li>Discord/auto threads: add optional <code>autoThreadName: "generated"</code> naming so new auto-created threads can be renamed asynchronously with concise LLM-generated titles while keeping the existing message-based naming as the default. (#43366) Thanks @davidguttman.</li>
<li>Plugins/hooks: add <code>before_dispatch</code> with canonical inbound metadata and route handled replies through the normal final-delivery path, preserving TTS and routed delivery semantics. (#50444) Thanks @gfzhx.</li>
<li>Control UI/agents: convert agent workspace file rows to expandable <code><details></code> with lazy-loaded inline markdown preview, and add comprehensive <code>.sidebar-markdown</code> styles for headings, lists, code blocks, tables, blockquotes, and details/summary elements. (#53411) Thanks @BunsDev.</li>
<li>Control UI/markdown preview: restyle the agent workspace file preview dialog with a frosted backdrop, sized panel, and styled header, and integrate <code>@create-markdown/preview</code> v2 system theme for rich markdown rendering (headings, tables, code blocks, callouts, blockquotes) that auto-adapts to the app's light/dark design tokens. (#53411) Thanks @BunsDev.</li>
<li>macOS app/config: replace horizontal pill-based subsection navigation with a collapsible tree sidebar using disclosure chevrons and indented subsection rows. (#53411) Thanks @BunsDev.</li>
<li>CLI/skills: soften missing-requirements label from "missing" to "needs setup" and surface API key setup guidance (where to get a key, CLI save command, storage path) in <code>openclaw skills info</code> output. (#53411) Thanks @BunsDev.</li>
<li>macOS app/skills: add "Get your key" homepage link and storage-path hint to the API key editor dialog, and show the config path in save confirmation messages. (#53411) Thanks @BunsDev.</li>
<li>Control UI/agents: add a "Not set" placeholder to the default agent model selector dropdown. (#53411) Thanks @BunsDev.</li>
<li>Runtime/install: lower the supported Node 22 floor to <code>22.14+</code> while continuing to recommend Node 24, so npm installs and self-updates do not strand Node 22.14 users on older releases.</li>
<li>CLI/update: preflight the target npm package <code>engines.node</code> before <code>openclaw update</code> runs a global package install, so outdated Node runtimes fail with a clear upgrade message instead of attempting an unsupported latest release.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Outbound media/local files: align outbound media access with the configured fs policy so host-local files and inbound-media paths keep sending when <code>workspaceOnly</code> is off, while strict workspace-only agents remain sandboxed.</li>
<li>Security/sandbox media dispatch: close the <code>mediaUrl</code>/<code>fileUrl</code> alias bypass so outbound tool and message actions cannot escape media-root restrictions. (#54034)</li>
<li>Gateway/restart sentinel: wake the interrupted agent session via heartbeat after restart instead of only sending a best-effort restart note, retry outbound delivery once on transient failure, and preserve explicit thread/topic routing through the wake path so replies land in the correct Telegram topic or Slack thread. (#53940) Thanks @VACInc.</li>
<li>Docker/setup: avoid the pre-start <code>openclaw-cli</code> shared-network namespace loop by routing setup-time onboard/config writes through <code>openclaw-gateway</code>, so fresh Docker installs stop failing before the gateway comes up. (#53385) Thanks @amsminn.</li>
<li>Gateway/channels: keep channel startup sequential while isolating per-channel boot failures, so one broken channel no longer blocks later channels from starting. (#54215) Thanks @JonathanJing.</li>
<li>Embedded runs/secrets: stop unresolved <code>SecretRef</code> config from crashing embedded agent runs by falling back to the resolved runtime snapshot when needed. Fixes #45838.</li>
<li>WhatsApp/groups: track recent gateway-sent message IDs and suppress only matching group echoes, preserving owner <code>/status</code>, <code>/new</code>, and <code>/activation</code> commands from linked-account <code>fromMe</code> traffic. (#53624) Thanks @w-sss.</li>
<li>WhatsApp/reply-to-bot detection: restore implicit group reply detection by unwrapping <code>botInvokeMessage</code> payloads and reading <code>selfLid</code> from <code>creds.json</code>, so reply-based mentions reach the bot again in linked-account group chats.</li>
<li>Telegram/forum topics: recover <code>#General</code> topic <code>1</code> routing when Telegram omits forum metadata, including native commands, interactive callbacks, inbound message context, and fallback error replies. (#53699) thanks @huntharo</li>
<li>Discord/gateway supervision: centralize gateway error handling behind a lifetime-owned supervisor so early, active, and late-teardown Carbon gateway errors stay classified consistently and stop surfacing as process-killing teardown crashes.</li>
<li>Discord/timeouts: send a visible timeout reply when the inbound Discord worker times out before a final reply starts, including created auto-thread targets and queued-run ordering. (#53823) Thanks @Kimbo7870.</li>
<li>ACP/direct chats: always deliver a terminal ACP result when final TTS does not yield audio, even if block text already streamed earlier, and skip redundant empty-text final synthesis. (#53692) Thanks @w-sss.</li>
<li>Telegram/outbound errors: preserve actionable 403 membership/block/kick details and treat <code>bot not a member</code> as a permanent delivery failure so Telegram sends stop retrying doomed chats. (#53635) Thanks @w-sss.</li>
<li>Telegram/photos: preflight Telegram photo dimension and aspect-ratio rules, and fall back to document sends when image metadata is invalid or unavailable so photo uploads stop failing with <code>PHOTO_INVALID_DIMENSIONS</code>. (#52545) Thanks @hnshah.</li>
<li>Slack/runtime defaults: trim Slack DM reply overhead, restore Codex auto transport, and tighten Slack/web-search runtime defaults around DM preview threading, cache scoping, warning dedupe, and explicit web-search opt-in. (#53957) Thanks @vincentkoc.</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.24/OpenClaw-2026.3.24.zip" length="24749233" type="application/octet-stream" sparkle:edSignature="gLm2VvI+PPEnNy4klYSs9WmZLkJTF5BcfFparrtPdnmeE4xgc8kFfICg445I039ev9/A6xGav7pm08reUHDcAg=="/>
</item>
<item>
<title>2026.3.23</title>
<pubDate>Mon, 23 Mar 2026 16:59:51 -0700</pubDate>
@@ -171,5 +119,97 @@
]]></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>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026031290</sparkle:version>
<sparkle:shortVersionString>2026.3.12</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.3.12</h2>
<h3>Changes</h3>
<ul>
<li>Control UI/dashboard-v2: refresh the gateway dashboard with modular overview, chat, config, agent, and session views, plus a command palette, mobile bottom tabs, and richer chat tools like slash commands, search, export, and pinned messages. (#41503) Thanks @BunsDev.</li>
<li>OpenAI/GPT-5.4 fast mode: add configurable session-level fast toggles across <code>/fast</code>, TUI, Control UI, and ACP, with per-model config defaults and OpenAI/Codex request shaping.</li>
<li>Anthropic/Claude fast mode: map the shared <code>/fast</code> toggle and <code>params.fastMode</code> to direct Anthropic API-key <code>service_tier</code> requests, with live verification for both Anthropic and OpenAI fast-mode tiers.</li>
<li>Models/plugins: move Ollama, vLLM, and SGLang onto the provider-plugin architecture, with provider-owned onboarding, discovery, model-picker setup, and post-selection hooks so core provider wiring is more modular.</li>
<li>Docs/Kubernetes: Add a starter K8s install path with raw manifests, Kind setup, and deployment docs. Thanks @sallyom @dzianisv @egkristi</li>
<li>Agents/subagents: add <code>sessions_yield</code> so orchestrators can end the current turn immediately, skip queued tool work, and carry a hidden follow-up payload into the next session turn. (#36537) thanks @jriff</li>
<li>Slack/agent replies: support <code>channelData.slack.blocks</code> in the shared reply delivery path so agents can send Block Kit messages through standard Slack outbound delivery. (#44592) Thanks @vincentkoc.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Security/device pairing: switch <code>/pair</code> and <code>openclaw qr</code> setup codes to short-lived bootstrap tokens so the next release no longer embeds shared gateway credentials in chat or QR pairing payloads. Thanks @lintsinghua.</li>
<li>Security/plugins: disable implicit workspace plugin auto-load so cloned repositories cannot execute workspace plugin code without an explicit trust decision. (<code>GHSA-99qw-6mr3-36qr</code>)(#44174) Thanks @lintsinghua and @vincentkoc.</li>
<li>Models/Kimi Coding: send <code>anthropic-messages</code> tools in native Anthropic format again so <code>kimi-coding</code> stops degrading tool calls into XML/plain-text pseudo invocations instead of real <code>tool_use</code> blocks. (#38669, #39907, #40552) Thanks @opriz.</li>
<li>TUI/chat log: reuse the active assistant message component for the same streaming run so <code>openclaw tui</code> no longer renders duplicate assistant replies. (#35364) Thanks @lisitan.</li>
<li>Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in <code>/models</code> button validation. (#40105) Thanks @avirweb.</li>
<li>Cron/proactive delivery: keep isolated direct cron sends out of the write-ahead resend queue so transient-send retries do not replay duplicate proactive messages after restart. (#40646) Thanks @openperf and @vincentkoc.</li>
<li>Models/Kimi Coding: send the built-in <code>User-Agent: claude-code/0.1.0</code> header by default for <code>kimi-coding</code> while still allowing explicit provider headers to override it, so Kimi Code subscription auth can work without a local header-injection proxy. (#30099) Thanks @Amineelfarssi and @vincentkoc.</li>
<li>Models/OpenAI Codex Spark: keep <code>gpt-5.3-codex-spark</code> working on the <code>openai-codex/*</code> path via resolver fallbacks and clearer Codex-only handling, while continuing to suppress the stale direct <code>openai/*</code> Spark row that OpenAI rejects live.</li>
<li>Ollama/Kimi Cloud: apply the Moonshot Kimi payload compatibility wrapper to Ollama-hosted Kimi models like <code>kimi-k2.5:cloud</code>, so tool routing no longer breaks when thinking is enabled. (#41519) Thanks @vincentkoc.</li>
<li>Moonshot CN API: respect explicit <code>baseUrl</code> (api.moonshot.cn) in implicit provider resolution so platform.moonshot.cn API keys authenticate correctly instead of returning HTTP 401. (#33637) Thanks @chengzhichao-xydt.</li>
<li>Kimi Coding/provider config: respect explicit <code>models.providers["kimi-coding"].baseUrl</code> when resolving the implicit provider so custom Kimi Coding endpoints no longer get overwritten by the built-in default. (#36353) Thanks @2233admin.</li>
<li>Gateway/main-session routing: keep TUI and other <code>mode:UI</code> main-session sends on the internal surface when <code>deliver</code> is enabled, so replies no longer inherit the session's persisted Telegram/WhatsApp route. (#43918) Thanks @obviyus.</li>
<li>BlueBubbles/self-chat echo dedupe: drop reflected duplicate webhook copies only when a matching <code>fromMe</code> event was just seen for the same chat, body, and timestamp, preventing self-chat loops without broad webhook suppression. Related to #32166. (#38442) Thanks @vincentkoc.</li>
<li>iMessage/self-chat echo dedupe: drop reflected duplicate copies only when a matching <code>is_from_me</code> event was just seen for the same chat, text, and <code>created_at</code>, preventing self-chat loops without broad text-only suppression. Related to #32166. (#38440) Thanks @vincentkoc.</li>
<li>Subagents/completion announce retries: raise the default announce timeout to 90 seconds and stop retrying gateway-timeout failures for externally delivered completion announces, preventing duplicate user-facing completion messages after slow gateway responses. Fixes #41235. Thanks @vasujain00 and @vincentkoc.</li>
<li>Mattermost/block streaming: fix duplicate message delivery (one threaded, one top-level) when block streaming is active by excluding <code>replyToId</code> from the block reply dedup key and adding an explicit <code>threading</code> dock to the Mattermost plugin. (#41362) Thanks @mathiasnagler and @vincentkoc.</li>
<li>Mattermost/reply media delivery: pass agent-scoped <code>mediaLocalRoots</code> through shared reply delivery so allowed local files upload correctly from button, slash-command, and model-picker replies. (#44021) Thanks @LyleLiu666.</li>
<li>macOS/Reminders: add the missing <code>NSRemindersUsageDescription</code> to the bundled app so <code>apple-reminders</code> can trigger the system permission prompt from OpenClaw.app. (#8559) Thanks @dinakars777.</li>
<li>Gateway/session discovery: discover disk-only and retired ACP session stores under custom templated <code>session.store</code> roots so ACP reconciliation, session-id/session-label targeting, and run-id fallback keep working after restart. (#44176) thanks @gumadeiras.</li>
<li>Plugins/env-scoped roots: fix plugin discovery/load caches and provenance tracking so same-process <code>HOME</code>/<code>OPENCLAW_HOME</code> changes no longer reuse stale plugin state or misreport <code>~/...</code> plugins as untracked. (#44046) thanks @gumadeiras.</li>
<li>Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and <code>models list --plain</code>, and migrate legacy duplicated <code>openrouter/openrouter/...</code> config entries forward on write.</li>
<li>Windows/native update: make package installs use the npm update path instead of the git path, carry portable Git into native Windows updates, and mirror the installer's Windows npm env so <code>openclaw update</code> no longer dies early on missing <code>git</code> or <code>node-llama-cpp</code> download setup.</li>
<li>Sandbox/write: preserve pinned mutation-helper payload stdin so sandboxed <code>write</code> no longer reports success while creating empty files. (#43876) Thanks @glitch418x.</li>
<li>Security/exec approvals: escape invisible Unicode format characters in approval prompts so zero-width command text renders as visible <code>\u{...}</code> escapes instead of spoofing the reviewed command. (<code>GHSA-pcqg-f7rg-xfvv</code>)(#43687) Thanks @EkiXu and @vincentkoc.</li>
<li>Hooks/loader: fail closed when workspace hook paths cannot be resolved with <code>realpath</code>, so unreadable or broken internal hook paths are skipped instead of falling back to unresolved imports. (#44437) Thanks @vincentkoc.</li>
<li>Hooks/agent deliveries: dedupe repeated hook requests by optional idempotency key so webhook retries can reuse the first run instead of launching duplicate agent executions. (#44438) Thanks @vincentkoc.</li>
<li>Security/exec detection: normalize compatibility Unicode and strip invisible formatting code points before obfuscation checks so zero-width and fullwidth command tricks no longer suppress heuristic detection. (<code>GHSA-9r3v-37xh-2cf6</code>)(#44091) Thanks @wooluo and @vincentkoc.</li>
<li>Security/exec allowlist: preserve POSIX case sensitivity and keep <code>?</code> within a single path segment so exact-looking allowlist patterns no longer overmatch executables across case or directory boundaries. (<code>GHSA-f8r2-vg7x-gh8m</code>)(#43798) Thanks @zpbrent and @vincentkoc.</li>
<li>Security/commands: require sender ownership for <code>/config</code> and <code>/debug</code> so authorized non-owner senders can no longer reach owner-only config and runtime debug surfaces. (<code>GHSA-r7vr-gr74-94p8</code>)(#44305) Thanks @tdjackey and @vincentkoc.</li>
<li>Security/gateway auth: clear unbound client-declared scopes on shared-token WebSocket connects so device-less shared-token operators cannot self-declare elevated scopes. (<code>GHSA-rqpp-rjj8-7wv8</code>)(#44306) Thanks @LUOYEcode and @vincentkoc.</li>
<li>Security/browser.request: block persistent browser profile create/delete routes from write-scoped <code>browser.request</code> so callers can no longer persist admin-only browser profile changes through the browser control surface. (<code>GHSA-vmhq-cqm9-6p7q</code>)(#43800) Thanks @tdjackey and @vincentkoc.</li>
<li>Security/agent: reject public spawned-run lineage fields and keep workspace inheritance on the internal spawned-session path so external <code>agent</code> callers can no longer override the gateway workspace boundary. (<code>GHSA-2rqg-gjgv-84jm</code>)(#43801) Thanks @tdjackey and @vincentkoc.</li>
<li>Security/session_status: enforce sandbox session-tree visibility and shared agent-to-agent access guards before reading or mutating target session state, so sandboxed subagents can no longer inspect parent session metadata or write parent model overrides via <code>session_status</code>. (<code>GHSA-wcxr-59v9-rxr8</code>)(#43754) Thanks @tdjackey and @vincentkoc.</li>
<li>Security/agent tools: mark <code>nodes</code> as explicitly owner-only and document/test that <code>canvas</code> remains a shared trusted-operator surface unless a real boundary bypass exists.</li>
<li>Security/exec approvals: fail closed for Ruby approval flows that use <code>-r</code>, <code>--require</code>, or <code>-I</code> so approval-backed commands no longer bind only the main script while extra local code-loading flags remain outside the reviewed file snapshot.</li>
<li>Security/device pairing: cap issued and verified device-token scopes to each paired device's approved scope baseline so stale or overbroad tokens cannot exceed approved access. (<code>GHSA-2pwv-x786-56f8</code>)(#43686) Thanks @tdjackey and @vincentkoc.</li>
<li>Docs/onboarding: align the legacy wizard reference and <code>openclaw onboard</code> command docs with the Ollama onboarding flow so all onboarding reference paths now document <code>--auth-choice ollama</code>, Cloud + Local mode, and non-interactive usage. (#43473) Thanks @BruceMacD.</li>
<li>Models/secrets: enforce source-managed SecretRef markers in generated <code>models.json</code> so runtime-resolved provider secrets are not persisted when runtime projection is skipped. (#43759) Thanks @joshavant.</li>
<li>Security/WebSocket preauth: shorten unauthenticated handshake retention and reject oversized pre-auth frames before application-layer parsing to reduce pre-pairing exposure on unsupported public deployments. (<code>GHSA-jv4g-m82p-2j93</code>)(#44089) (<code>GHSA-xwx2-ppv2-wx98</code>)(#44089) Thanks @ez-lbz and @vincentkoc.</li>
<li>Security/proxy attachments: restore the shared media-store size cap for persisted browser proxy files so oversized payloads are rejected instead of overriding the intended 5 MB limit. (<code>GHSA-6rph-mmhp-h7h9</code>)(#43684) Thanks @tdjackey and @vincentkoc.</li>
<li>Security/host env: block inherited <code>GIT_EXEC_PATH</code> from sanitized host exec environments so Git helper resolution cannot be steered by host environment state. (<code>GHSA-jf5v-pqgw-gm5m</code>)(#43685) Thanks @zpbrent and @vincentkoc.</li>
<li>Security/Feishu webhook: require <code>encryptKey</code> alongside <code>verificationToken</code> in webhook mode so unsigned forged events are rejected instead of being processed with token-only configuration. (<code>GHSA-g353-mgv3-8pcj</code>)(#44087) Thanks @lintsinghua and @vincentkoc.</li>
<li>Security/Feishu reactions: preserve looked-up group chat typing and fail closed on ambiguous reaction context so group authorization and mention gating cannot be bypassed through synthetic <code>p2p</code> reactions. (<code>GHSA-m69h-jm2f-2pv8</code>)(#44088) Thanks @zpbrent and @vincentkoc.</li>
<li>Security/LINE webhook: require signatures for empty-event POST probes too so unsigned requests no longer confirm webhook reachability with a <code>200</code> response. (<code>GHSA-mhxh-9pjm-w7q5</code>)(#44090) Thanks @TerminalsandCoffee and @vincentkoc.</li>
<li>Security/Zalo webhook: rate limit invalid secret guesses before auth so weak webhook secrets cannot be brute-forced through unauthenticated churned requests without pre-auth <code>429</code> responses. (<code>GHSA-5m9r-p9g7-679c</code>)(#44173) Thanks @zpbrent and @vincentkoc.</li>
<li>Security/Zalouser groups: require stable group IDs for allowlist auth by default and gate mutable group-name matching behind <code>channels.zalouser.dangerouslyAllowNameMatching</code>. Thanks @zpbrent.</li>
<li>Security/Slack and Teams routing: require stable channel and team IDs for allowlist routing by default, with mutable name matching only via each channel's <code>dangerouslyAllowNameMatching</code> break-glass flag.</li>
<li>Security/exec approvals: fail closed for ambiguous inline loader and shell-payload script execution, bind the real script after POSIX shell value-taking flags, and unwrap <code>pnpm</code>/<code>npm exec</code>/<code>npx</code> script runners before approval binding. (<code>GHSA-57jw-9722-6rf2</code>)(<code>GHSA-jvqh-rfmh-jh27</code>)(<code>GHSA-x7pp-23xv-mmr4</code>)(<code>GHSA-jc5j-vg4r-j5jx</code>)(#44247) Thanks @tdjackey and @vincentkoc.</li>
<li>Doctor/gateway service audit: canonicalize service entrypoint paths before comparing them so symlink-vs-realpath installs no longer trigger false "entrypoint does not match the current install" repair prompts. (#43882) Thanks @ngutman.</li>
<li>Doctor/gateway service audit: earlier groundwork for this fix landed in the superseded #28338 branch. Thanks @realriphub.</li>
<li>Gateway/session stores: regenerate the Swift push-test protocol models and align Windows native session-store realpath handling so protocol checks and sync session discovery stop drifting on Windows. (#44266) thanks @jalehman.</li>
<li>Context engine/session routing: forward optional <code>sessionKey</code> through context-engine lifecycle calls so plugins can see structured routing metadata during bootstrap, assembly, post-turn ingestion, and compaction. (#44157) thanks @jalehman.</li>
<li>Agents/failover: classify z.ai <code>network_error</code> stop reasons as retryable timeouts so provider connectivity failures trigger fallback instead of surfacing raw unhandled-stop-reason errors. (#43884) Thanks @hougangdev.</li>
<li>Memory/session sync: add mode-aware post-compaction session reindexing with <code>agents.defaults.compaction.postIndexSync</code> plus <code>agents.defaults.memorySearch.sync.sessions.postCompactionForce</code>, so compacted session memory can refresh immediately without forcing every deployment into synchronous reindexing. (#25561) thanks @rodrigouroz.</li>
<li>Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in <code>/models</code> button validation. (#40105) Thanks @avirweb.</li>
<li>Telegram/native command sync: suppress expected <code>BOT_COMMANDS_TOO_MUCH</code> retry error noise, add a final fallback summary log, and document the difference between command-menu overflow and real Telegram network failures.</li>
<li>Mattermost/reply media delivery: pass agent-scoped <code>mediaLocalRoots</code> through shared reply delivery so allowed local files upload correctly from button, slash-command, and model-picker replies. (#44021) Thanks @LyleLiu666.</li>
<li>Plugins/env-scoped roots: fix plugin discovery/load caches and provenance tracking so same-process <code>HOME</code>/<code>OPENCLAW_HOME</code> changes no longer reuse stale plugin state or misreport <code>~/...</code> plugins as untracked. (#44046) thanks @gumadeiras.</li>
<li>Gateway/session discovery: discover disk-only and retired ACP session stores under custom templated <code>session.store</code> roots so ACP reconciliation, session-id/session-label targeting, and run-id fallback keep working after restart. (#44176) thanks @gumadeiras.</li>
<li>Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and <code>models list --plain</code>, and migrate legacy duplicated <code>openrouter/openrouter/...</code> config entries forward on write.</li>
<li>Gateway/hooks: bucket hook auth failures by forwarded client IP behind trusted proxies and warn when <code>hooks.allowedAgentIds</code> leaves hook routing unrestricted.</li>
<li>Agents/compaction: skip the post-compaction <code>cache-ttl</code> marker write when a compaction completed in the same attempt, preventing the next turn from immediately triggering a second tiny compaction. (#28548) thanks @MoerAI.</li>
<li>Native chat/macOS: add <code>/new</code>, <code>/reset</code>, and <code>/clear</code> 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.</li>
<li>Agents/compaction safeguard: route missing-model and missing-API-key cancellation warnings through the shared subsystem logger so they land in structured and file logs. (#9974) Thanks @dinakars777.</li>
<li>Cron/doctor: stop flagging canonical <code>agentTurn</code> and <code>systemEvent</code> payload kinds as legacy cron storage, while still normalizing whitespace-padded and non-canonical variants. (#44012) Thanks @shuicici.</li>
<li>ACP/client final-message delivery: preserve terminal assistant text snapshots before resolving <code>end_turn</code>, so ACP clients no longer drop the last visible reply when the gateway sends the final message body on the terminal chat event. (#17615) Thanks @pjeby.</li>
<li>Telegram/Discord status reactions: show a temporary compacting reaction during auto-compaction pauses and restore thinking afterward so the bot no longer appears frozen while context is being compacted. (#35474) thanks @Cypherm.</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.12/OpenClaw-2026.3.12.zip" length="23628700" type="application/octet-stream" sparkle:edSignature="o6Zdcw36l3I0jUg14H+RBqNwrhuuSsq1WMDi4tBRa1+5TC3VCVdFKZ2hzmH2Xjru9lDEzVMP8v2A6RexSbOCBQ=="/>
</item>
</channel>
</rss>
</rss>

View File

@@ -1,8 +1,8 @@
// Shared iOS version defaults.
// Generated overrides live in build/Version.xcconfig (git-ignored).
OPENCLAW_GATEWAY_VERSION = 2026.3.24
OPENCLAW_GATEWAY_VERSION = 2026.3.24-beta.1
OPENCLAW_MARKETING_VERSION = 2026.3.24
OPENCLAW_BUILD_VERSION = 2026032490
OPENCLAW_BUILD_VERSION = 202603240
#include? "../build/Version.xcconfig"

View File

@@ -493,12 +493,8 @@ enum OpenClawConfigFile {
return
}
let backup = self.readConfigFingerprint(
at: configURL.deletingLastPathComponent().appendingPathComponent("\(configURL.lastPathComponent).bak"))
let clobberedPath = self.persistClobberedSnapshot(
data: data,
configURL: configURL,
observedAt: observedAt)
let backup = self.readConfigFingerprint(at: configURL.deletingLastPathComponent().appendingPathComponent("\(configURL.lastPathComponent).bak"))
let clobberedPath = self.persistClobberedSnapshot(data: data, configURL: configURL, observedAt: observedAt)
self.logger.warning("config observe anomaly (\(suspicious.joined(separator: ", "))) at \(configURL.path)")
self.appendConfigObserveAudit([
"phase": "read",

View File

@@ -17,7 +17,7 @@
<key>CFBundleShortVersionString</key>
<string>2026.3.24</string>
<key>CFBundleVersion</key>
<string>2026032490</string>
<string>202603240</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -259,12 +259,9 @@ private struct SkillRow: View {
guard let raw = self.skill.homepage?.trimmingCharacters(in: .whitespacesAndNewlines) else {
return nil
}
guard
!raw.isEmpty,
let url = URL(string: raw),
let scheme = url.scheme?.lowercased(),
scheme == "http" || scheme == "https"
else {
guard !raw.isEmpty, let url = URL(string: raw),
let scheme = url.scheme?.lowercased(),
scheme == "http" || scheme == "https" else {
return nil
}
return url
@@ -484,12 +481,9 @@ private struct EnvEditorView: View {
guard let raw = self.editor.homepage?.trimmingCharacters(in: .whitespacesAndNewlines) else {
return nil
}
guard
!raw.isEmpty,
let url = URL(string: raw),
let scheme = url.scheme?.lowercased(),
scheme == "http" || scheme == "https"
else {
guard !raw.isEmpty, let url = URL(string: raw),
let scheme = url.scheme?.lowercased(),
scheme == "http" || scheme == "https" else {
return nil
}
return url

View File

@@ -24618,36 +24618,6 @@
"tags": [],
"hasChildren": false
},
{
"path": "channels.msteams.feedbackEnabled",
"kind": "channel",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.msteams.feedbackReflection",
"kind": "channel",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.msteams.feedbackReflectionCooldownMs",
"kind": "channel",
"type": "integer",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.msteams.groupAllowFrom",
"kind": "channel",
@@ -24684,16 +24654,6 @@
"tags": [],
"hasChildren": false
},
{
"path": "channels.msteams.groupWelcomeCard",
"kind": "channel",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.msteams.healthMonitor",
"kind": "channel",
@@ -24839,26 +24799,6 @@
"tags": [],
"hasChildren": false
},
{
"path": "channels.msteams.promptStarters",
"kind": "channel",
"type": "array",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": true
},
{
"path": "channels.msteams.promptStarters.*",
"kind": "channel",
"type": "string",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.msteams.replyStyle",
"kind": "channel",
@@ -25341,16 +25281,6 @@
"tags": [],
"hasChildren": false
},
{
"path": "channels.msteams.welcomeCard",
"kind": "channel",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [],
"hasChildren": false
},
{
"path": "channels.nextcloud-talk",
"kind": "channel",
@@ -64680,21 +64610,6 @@
"help": "Maximum redirects allowed for web_fetch (default: 3).",
"hasChildren": false
},
{
"path": "tools.web.fetch.maxResponseBytes",
"kind": "core",
"type": "integer",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"performance",
"tools"
],
"label": "Web Fetch Max Download Size (bytes)",
"help": "Max download size before truncation.",
"hasChildren": false
},
{
"path": "tools.web.fetch.readability",
"kind": "core",

View File

@@ -1,4 +1,4 @@
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5639}
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5631}
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -2208,13 +2208,9 @@
{"recordType":"path","path":"channels.msteams.dms.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.msteams.dms.*.historyLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.feedbackEnabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.feedbackReflection","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.feedbackReflectionCooldownMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.msteams.groupAllowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.groupPolicy","kind":"channel","type":"string","required":true,"enumValues":["open","disabled","allowlist"],"defaultValue":"allowlist","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.groupWelcomeCard","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.msteams.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -2229,8 +2225,6 @@
{"recordType":"path","path":"channels.msteams.mediaAuthAllowHosts","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.msteams.mediaAuthAllowHosts.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.mediaMaxMb","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.promptStarters","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.msteams.promptStarters.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.replyStyle","kind":"channel","type":"string","required":false,"enumValues":["thread","top-level"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.responsePrefix","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -2278,7 +2272,6 @@
{"recordType":"path","path":"channels.msteams.webhook","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.msteams.webhook.path","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.webhook.port","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.msteams.welcomeCard","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
{"recordType":"path","path":"channels.nextcloud-talk","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Nextcloud Talk","help":"Self-hosted chat via Nextcloud Talk webhook bots.","hasChildren":true}
{"recordType":"path","path":"channels.nextcloud-talk.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
{"recordType":"path","path":"channels.nextcloud-talk.accounts.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -5552,7 +5545,6 @@
{"recordType":"path","path":"tools.web.fetch.maxChars","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"Web Fetch Max Chars","help":"Max characters returned by web_fetch (truncated).","hasChildren":false}
{"recordType":"path","path":"tools.web.fetch.maxCharsCap","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"Web Fetch Hard Max Chars","help":"Hard cap for web_fetch maxChars (applies to config and tool calls).","hasChildren":false}
{"recordType":"path","path":"tools.web.fetch.maxRedirects","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","storage","tools"],"label":"Web Fetch Max Redirects","help":"Maximum redirects allowed for web_fetch (default: 3).","hasChildren":false}
{"recordType":"path","path":"tools.web.fetch.maxResponseBytes","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"Web Fetch Max Download Size (bytes)","help":"Max download size before truncation.","hasChildren":false}
{"recordType":"path","path":"tools.web.fetch.readability","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Web Fetch Readability Extraction","help":"Use Readability to extract main content from HTML (fallbacks to basic HTML cleanup).","hasChildren":false}
{"recordType":"path","path":"tools.web.fetch.timeoutSeconds","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"Web Fetch Timeout (sec)","help":"Timeout in seconds for web_fetch requests.","hasChildren":false}
{"recordType":"path","path":"tools.web.fetch.userAgent","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Web Fetch User-Agent","help":"Override User-Agent header for web_fetch requests.","hasChildren":false}

View File

@@ -127,7 +127,7 @@
"exportName": "ChannelConfiguredBindingConversationRef",
"kind": "type",
"source": {
"line": 554,
"line": 553,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -136,7 +136,7 @@
"exportName": "ChannelConfiguredBindingMatch",
"kind": "type",
"source": {
"line": 559,
"line": 558,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -145,7 +145,7 @@
"exportName": "ChannelConfiguredBindingProvider",
"kind": "type",
"source": {
"line": 563,
"line": 562,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -154,7 +154,7 @@
"exportName": "ChannelGatewayContext",
"kind": "type",
"source": {
"line": 239,
"line": 238,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -352,7 +352,7 @@
"exportName": "ImageGenerationProvider",
"kind": "type",
"source": {
"line": 72,
"line": 66,
"path": "src/image-generation/types.ts"
}
},
@@ -379,7 +379,7 @@
"exportName": "ImageGenerationResult",
"kind": "type",
"source": {
"line": 42,
"line": 36,
"path": "src/image-generation/types.ts"
}
},
@@ -397,7 +397,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 952,
"line": 951,
"path": "src/plugins/types.ts"
}
},
@@ -415,7 +415,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1316,
"line": 1314,
"path": "src/plugins/types.ts"
}
},
@@ -424,7 +424,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 89,
"line": 88,
"path": "src/plugins/types.ts"
}
},
@@ -433,7 +433,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 60,
"line": 59,
"path": "src/plugins/types.ts"
}
},
@@ -451,7 +451,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 156,
"line": 155,
"path": "src/plugins/types.ts"
}
},
@@ -460,7 +460,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 141,
"line": 140,
"path": "src/plugins/types.ts"
}
},
@@ -469,7 +469,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 296,
"line": 295,
"path": "src/plugins/types.ts"
}
},
@@ -496,7 +496,7 @@
"exportName": "RuntimeLogger",
"kind": "type",
"source": {
"line": 7,
"line": 4,
"path": "src/plugins/runtime/types-core.ts"
}
},
@@ -523,7 +523,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 934,
"line": 933,
"path": "src/plugins/types.ts"
}
},
@@ -914,7 +914,7 @@
"exportName": "createMessageToolCardSchema",
"kind": "function",
"source": {
"line": 29,
"line": 27,
"path": "src/plugin-sdk/channel-actions.ts"
}
},
@@ -1044,7 +1044,7 @@
"exportName": "BaseProbeResult",
"kind": "type",
"source": {
"line": 559,
"line": 558,
"path": "src/channels/plugins/types.core.ts"
}
},
@@ -1053,7 +1053,7 @@
"exportName": "BaseTokenResolution",
"kind": "type",
"source": {
"line": 565,
"line": 564,
"path": "src/channels/plugins/types.core.ts"
}
},
@@ -1518,7 +1518,7 @@
"exportName": "BaseProbeResult",
"kind": "type",
"source": {
"line": 559,
"line": 558,
"path": "src/channels/plugins/types.core.ts"
}
},
@@ -1527,7 +1527,7 @@
"exportName": "BaseTokenResolution",
"kind": "type",
"source": {
"line": 565,
"line": 564,
"path": "src/channels/plugins/types.core.ts"
}
},
@@ -1581,7 +1581,7 @@
"exportName": "ChannelAllowlistAdapter",
"kind": "type",
"source": {
"line": 498,
"line": 497,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1590,7 +1590,7 @@
"exportName": "ChannelAuthAdapter",
"kind": "type",
"source": {
"line": 363,
"line": 362,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1635,7 +1635,7 @@
"exportName": "ChannelCommandAdapter",
"kind": "type",
"source": {
"line": 445,
"line": 444,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1653,7 +1653,7 @@
"exportName": "ChannelConfiguredBindingConversationRef",
"kind": "type",
"source": {
"line": 554,
"line": 553,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1662,7 +1662,7 @@
"exportName": "ChannelConfiguredBindingMatch",
"kind": "type",
"source": {
"line": 559,
"line": 558,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1671,7 +1671,7 @@
"exportName": "ChannelConfiguredBindingProvider",
"kind": "type",
"source": {
"line": 563,
"line": 562,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1680,7 +1680,7 @@
"exportName": "ChannelDirectoryAdapter",
"kind": "type",
"source": {
"line": 407,
"line": 406,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1707,7 +1707,7 @@
"exportName": "ChannelElevatedAdapter",
"kind": "type",
"source": {
"line": 438,
"line": 437,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1716,7 +1716,7 @@
"exportName": "ChannelExecApprovalAdapter",
"kind": "type",
"source": {
"line": 464,
"line": 463,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1743,7 +1743,7 @@
"exportName": "ChannelGatewayAdapter",
"kind": "type",
"source": {
"line": 347,
"line": 346,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1752,7 +1752,7 @@
"exportName": "ChannelGatewayContext",
"kind": "type",
"source": {
"line": 239,
"line": 238,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1779,7 +1779,7 @@
"exportName": "ChannelHeartbeatAdapter",
"kind": "type",
"source": {
"line": 373,
"line": 372,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1806,7 +1806,7 @@
"exportName": "ChannelLifecycleAdapter",
"kind": "type",
"source": {
"line": 450,
"line": 449,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1815,7 +1815,7 @@
"exportName": "ChannelLoginWithQrStartResult",
"kind": "type",
"source": {
"line": 318,
"line": 317,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1824,7 +1824,7 @@
"exportName": "ChannelLoginWithQrWaitResult",
"kind": "type",
"source": {
"line": 323,
"line": 322,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1833,7 +1833,7 @@
"exportName": "ChannelLogoutContext",
"kind": "type",
"source": {
"line": 328,
"line": 327,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1842,7 +1842,7 @@
"exportName": "ChannelLogoutResult",
"kind": "type",
"source": {
"line": 312,
"line": 311,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1950,7 +1950,7 @@
"exportName": "ChannelOutboundAdapter",
"kind": "type",
"source": {
"line": 155,
"line": 154,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -1977,7 +1977,7 @@
"exportName": "ChannelPairingAdapter",
"kind": "type",
"source": {
"line": 336,
"line": 335,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -2013,7 +2013,7 @@
"exportName": "ChannelResolveKind",
"kind": "type",
"source": {
"line": 418,
"line": 417,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -2022,7 +2022,7 @@
"exportName": "ChannelResolverAdapter",
"kind": "type",
"source": {
"line": 428,
"line": 427,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -2031,7 +2031,7 @@
"exportName": "ChannelResolveResult",
"kind": "type",
"source": {
"line": 420,
"line": 419,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -2040,7 +2040,7 @@
"exportName": "ChannelSecurityAdapter",
"kind": "type",
"source": {
"line": 576,
"line": 575,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -2085,7 +2085,7 @@
"exportName": "ChannelStatusAdapter",
"kind": "type",
"source": {
"line": 185,
"line": 184,
"path": "src/channels/plugins/types.adapters.ts"
}
},
@@ -3396,7 +3396,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 952,
"line": 951,
"path": "src/plugins/types.ts"
}
},
@@ -3414,7 +3414,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1316,
"line": 1314,
"path": "src/plugins/types.ts"
}
},
@@ -3423,7 +3423,7 @@
"exportName": "OpenClawPluginCommandDefinition",
"kind": "type",
"source": {
"line": 1070,
"line": 1068,
"path": "src/plugins/types.ts"
}
},
@@ -3432,7 +3432,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 89,
"line": 88,
"path": "src/plugins/types.ts"
}
},
@@ -3441,7 +3441,7 @@
"exportName": "OpenClawPluginDefinition",
"kind": "type",
"source": {
"line": 1298,
"line": 1296,
"path": "src/plugins/types.ts"
}
},
@@ -3450,7 +3450,7 @@
"exportName": "OpenClawPluginService",
"kind": "type",
"source": {
"line": 1287,
"line": 1285,
"path": "src/plugins/types.ts"
}
},
@@ -3459,7 +3459,7 @@
"exportName": "OpenClawPluginServiceContext",
"kind": "type",
"source": {
"line": 1279,
"line": 1277,
"path": "src/plugins/types.ts"
}
},
@@ -3468,7 +3468,7 @@
"exportName": "OpenClawPluginToolContext",
"kind": "type",
"source": {
"line": 104,
"line": 103,
"path": "src/plugins/types.ts"
}
},
@@ -3477,7 +3477,7 @@
"exportName": "OpenClawPluginToolFactory",
"kind": "type",
"source": {
"line": 121,
"line": 120,
"path": "src/plugins/types.ts"
}
},
@@ -3486,7 +3486,7 @@
"exportName": "PluginCommandContext",
"kind": "type",
"source": {
"line": 968,
"line": 966,
"path": "src/plugins/types.ts"
}
},
@@ -3495,7 +3495,7 @@
"exportName": "PluginInteractiveTelegramHandlerContext",
"kind": "type",
"source": {
"line": 1099,
"line": 1097,
"path": "src/plugins/types.ts"
}
},
@@ -3504,7 +3504,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 60,
"line": 59,
"path": "src/plugins/types.ts"
}
},
@@ -3522,7 +3522,7 @@
"exportName": "ProviderAugmentModelCatalogContext",
"kind": "type",
"source": {
"line": 572,
"line": 571,
"path": "src/plugins/types.ts"
}
},
@@ -3531,7 +3531,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 156,
"line": 155,
"path": "src/plugins/types.ts"
}
},
@@ -3540,7 +3540,7 @@
"exportName": "ProviderAuthDoctorHintContext",
"kind": "type",
"source": {
"line": 447,
"line": 446,
"path": "src/plugins/types.ts"
}
},
@@ -3549,7 +3549,7 @@
"exportName": "ProviderAuthMethod",
"kind": "type",
"source": {
"line": 234,
"line": 233,
"path": "src/plugins/types.ts"
}
},
@@ -3558,7 +3558,7 @@
"exportName": "ProviderAuthMethodNonInteractiveContext",
"kind": "type",
"source": {
"line": 218,
"line": 217,
"path": "src/plugins/types.ts"
}
},
@@ -3567,7 +3567,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 141,
"line": 140,
"path": "src/plugins/types.ts"
}
},
@@ -3576,7 +3576,7 @@
"exportName": "ProviderBuildMissingAuthMessageContext",
"kind": "type",
"source": {
"line": 500,
"line": 499,
"path": "src/plugins/types.ts"
}
},
@@ -3585,7 +3585,7 @@
"exportName": "ProviderBuiltInModelSuppressionContext",
"kind": "type",
"source": {
"line": 516,
"line": 515,
"path": "src/plugins/types.ts"
}
},
@@ -3594,7 +3594,7 @@
"exportName": "ProviderBuiltInModelSuppressionResult",
"kind": "type",
"source": {
"line": 525,
"line": 524,
"path": "src/plugins/types.ts"
}
},
@@ -3603,7 +3603,7 @@
"exportName": "ProviderCacheTtlEligibilityContext",
"kind": "type",
"source": {
"line": 488,
"line": 487,
"path": "src/plugins/types.ts"
}
},
@@ -3612,7 +3612,7 @@
"exportName": "ProviderCatalogContext",
"kind": "type",
"source": {
"line": 255,
"line": 254,
"path": "src/plugins/types.ts"
}
},
@@ -3621,7 +3621,7 @@
"exportName": "ProviderCatalogResult",
"kind": "type",
"source": {
"line": 278,
"line": 277,
"path": "src/plugins/types.ts"
}
},
@@ -3630,7 +3630,7 @@
"exportName": "ProviderDefaultThinkingPolicyContext",
"kind": "type",
"source": {
"line": 549,
"line": 548,
"path": "src/plugins/types.ts"
}
},
@@ -3639,7 +3639,7 @@
"exportName": "ProviderDiscoveryContext",
"kind": "type",
"source": {
"line": 588,
"line": 587,
"path": "src/plugins/types.ts"
}
},
@@ -3648,7 +3648,7 @@
"exportName": "ProviderFetchUsageSnapshotContext",
"kind": "type",
"source": {
"line": 428,
"line": 427,
"path": "src/plugins/types.ts"
}
},
@@ -3657,7 +3657,7 @@
"exportName": "ProviderModernModelPolicyContext",
"kind": "type",
"source": {
"line": 559,
"line": 558,
"path": "src/plugins/types.ts"
}
},
@@ -3666,7 +3666,7 @@
"exportName": "ProviderNormalizeResolvedModelContext",
"kind": "type",
"source": {
"line": 339,
"line": 338,
"path": "src/plugins/types.ts"
}
},
@@ -3675,7 +3675,7 @@
"exportName": "ProviderPreparedRuntimeAuth",
"kind": "type",
"source": {
"line": 375,
"line": 374,
"path": "src/plugins/types.ts"
}
},
@@ -3684,7 +3684,7 @@
"exportName": "ProviderPrepareDynamicModelContext",
"kind": "type",
"source": {
"line": 330,
"line": 329,
"path": "src/plugins/types.ts"
}
},
@@ -3693,7 +3693,7 @@
"exportName": "ProviderPrepareExtraParamsContext",
"kind": "type",
"source": {
"line": 461,
"line": 460,
"path": "src/plugins/types.ts"
}
},
@@ -3702,7 +3702,7 @@
"exportName": "ProviderPrepareRuntimeAuthContext",
"kind": "type",
"source": {
"line": 354,
"line": 353,
"path": "src/plugins/types.ts"
}
},
@@ -3711,7 +3711,7 @@
"exportName": "ProviderResolvedUsageAuth",
"kind": "type",
"source": {
"line": 415,
"line": 414,
"path": "src/plugins/types.ts"
}
},
@@ -3720,7 +3720,7 @@
"exportName": "ProviderResolveDynamicModelContext",
"kind": "type",
"source": {
"line": 313,
"line": 312,
"path": "src/plugins/types.ts"
}
},
@@ -3729,7 +3729,7 @@
"exportName": "ProviderResolveUsageAuthContext",
"kind": "type",
"source": {
"line": 396,
"line": 395,
"path": "src/plugins/types.ts"
}
},
@@ -3738,7 +3738,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 296,
"line": 295,
"path": "src/plugins/types.ts"
}
},
@@ -3747,7 +3747,7 @@
"exportName": "ProviderThinkingPolicyContext",
"kind": "type",
"source": {
"line": 537,
"line": 536,
"path": "src/plugins/types.ts"
}
},
@@ -3765,7 +3765,7 @@
"exportName": "ProviderWrapStreamFnContext",
"kind": "type",
"source": {
"line": 478,
"line": 477,
"path": "src/plugins/types.ts"
}
},
@@ -3810,7 +3810,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 934,
"line": 933,
"path": "src/plugins/types.ts"
}
},
@@ -3902,7 +3902,7 @@
"exportName": "MediaUnderstandingProviderPlugin",
"kind": "type",
"source": {
"line": 952,
"line": 951,
"path": "src/plugins/types.ts"
}
},
@@ -3920,7 +3920,7 @@
"exportName": "OpenClawPluginApi",
"kind": "type",
"source": {
"line": 1316,
"line": 1314,
"path": "src/plugins/types.ts"
}
},
@@ -3929,7 +3929,7 @@
"exportName": "OpenClawPluginCommandDefinition",
"kind": "type",
"source": {
"line": 1070,
"line": 1068,
"path": "src/plugins/types.ts"
}
},
@@ -3938,7 +3938,7 @@
"exportName": "OpenClawPluginConfigSchema",
"kind": "type",
"source": {
"line": 89,
"line": 88,
"path": "src/plugins/types.ts"
}
},
@@ -3947,7 +3947,7 @@
"exportName": "OpenClawPluginDefinition",
"kind": "type",
"source": {
"line": 1298,
"line": 1296,
"path": "src/plugins/types.ts"
}
},
@@ -3956,7 +3956,7 @@
"exportName": "OpenClawPluginService",
"kind": "type",
"source": {
"line": 1287,
"line": 1285,
"path": "src/plugins/types.ts"
}
},
@@ -3965,7 +3965,7 @@
"exportName": "OpenClawPluginServiceContext",
"kind": "type",
"source": {
"line": 1279,
"line": 1277,
"path": "src/plugins/types.ts"
}
},
@@ -3974,7 +3974,7 @@
"exportName": "PluginCommandContext",
"kind": "type",
"source": {
"line": 968,
"line": 966,
"path": "src/plugins/types.ts"
}
},
@@ -3983,7 +3983,7 @@
"exportName": "PluginInteractiveTelegramHandlerContext",
"kind": "type",
"source": {
"line": 1099,
"line": 1097,
"path": "src/plugins/types.ts"
}
},
@@ -3992,7 +3992,7 @@
"exportName": "PluginLogger",
"kind": "type",
"source": {
"line": 60,
"line": 59,
"path": "src/plugins/types.ts"
}
},
@@ -4001,7 +4001,7 @@
"exportName": "ProviderAugmentModelCatalogContext",
"kind": "type",
"source": {
"line": 572,
"line": 571,
"path": "src/plugins/types.ts"
}
},
@@ -4010,7 +4010,7 @@
"exportName": "ProviderAuthContext",
"kind": "type",
"source": {
"line": 156,
"line": 155,
"path": "src/plugins/types.ts"
}
},
@@ -4019,7 +4019,7 @@
"exportName": "ProviderAuthDoctorHintContext",
"kind": "type",
"source": {
"line": 447,
"line": 446,
"path": "src/plugins/types.ts"
}
},
@@ -4028,7 +4028,7 @@
"exportName": "ProviderAuthMethod",
"kind": "type",
"source": {
"line": 234,
"line": 233,
"path": "src/plugins/types.ts"
}
},
@@ -4037,7 +4037,7 @@
"exportName": "ProviderAuthMethodNonInteractiveContext",
"kind": "type",
"source": {
"line": 218,
"line": 217,
"path": "src/plugins/types.ts"
}
},
@@ -4046,7 +4046,7 @@
"exportName": "ProviderAuthResult",
"kind": "type",
"source": {
"line": 141,
"line": 140,
"path": "src/plugins/types.ts"
}
},
@@ -4055,7 +4055,7 @@
"exportName": "ProviderBuildMissingAuthMessageContext",
"kind": "type",
"source": {
"line": 500,
"line": 499,
"path": "src/plugins/types.ts"
}
},
@@ -4064,7 +4064,7 @@
"exportName": "ProviderBuiltInModelSuppressionContext",
"kind": "type",
"source": {
"line": 516,
"line": 515,
"path": "src/plugins/types.ts"
}
},
@@ -4073,7 +4073,7 @@
"exportName": "ProviderBuiltInModelSuppressionResult",
"kind": "type",
"source": {
"line": 525,
"line": 524,
"path": "src/plugins/types.ts"
}
},
@@ -4082,7 +4082,7 @@
"exportName": "ProviderCacheTtlEligibilityContext",
"kind": "type",
"source": {
"line": 488,
"line": 487,
"path": "src/plugins/types.ts"
}
},
@@ -4091,7 +4091,7 @@
"exportName": "ProviderCatalogContext",
"kind": "type",
"source": {
"line": 255,
"line": 254,
"path": "src/plugins/types.ts"
}
},
@@ -4100,7 +4100,7 @@
"exportName": "ProviderCatalogResult",
"kind": "type",
"source": {
"line": 278,
"line": 277,
"path": "src/plugins/types.ts"
}
},
@@ -4109,7 +4109,7 @@
"exportName": "ProviderDefaultThinkingPolicyContext",
"kind": "type",
"source": {
"line": 549,
"line": 548,
"path": "src/plugins/types.ts"
}
},
@@ -4118,7 +4118,7 @@
"exportName": "ProviderDiscoveryContext",
"kind": "type",
"source": {
"line": 588,
"line": 587,
"path": "src/plugins/types.ts"
}
},
@@ -4127,7 +4127,7 @@
"exportName": "ProviderFetchUsageSnapshotContext",
"kind": "type",
"source": {
"line": 428,
"line": 427,
"path": "src/plugins/types.ts"
}
},
@@ -4136,7 +4136,7 @@
"exportName": "ProviderModernModelPolicyContext",
"kind": "type",
"source": {
"line": 559,
"line": 558,
"path": "src/plugins/types.ts"
}
},
@@ -4145,7 +4145,7 @@
"exportName": "ProviderNormalizeResolvedModelContext",
"kind": "type",
"source": {
"line": 339,
"line": 338,
"path": "src/plugins/types.ts"
}
},
@@ -4154,7 +4154,7 @@
"exportName": "ProviderPreparedRuntimeAuth",
"kind": "type",
"source": {
"line": 375,
"line": 374,
"path": "src/plugins/types.ts"
}
},
@@ -4163,7 +4163,7 @@
"exportName": "ProviderPrepareDynamicModelContext",
"kind": "type",
"source": {
"line": 330,
"line": 329,
"path": "src/plugins/types.ts"
}
},
@@ -4172,7 +4172,7 @@
"exportName": "ProviderPrepareExtraParamsContext",
"kind": "type",
"source": {
"line": 461,
"line": 460,
"path": "src/plugins/types.ts"
}
},
@@ -4181,7 +4181,7 @@
"exportName": "ProviderPrepareRuntimeAuthContext",
"kind": "type",
"source": {
"line": 354,
"line": 353,
"path": "src/plugins/types.ts"
}
},
@@ -4190,7 +4190,7 @@
"exportName": "ProviderResolvedUsageAuth",
"kind": "type",
"source": {
"line": 415,
"line": 414,
"path": "src/plugins/types.ts"
}
},
@@ -4199,7 +4199,7 @@
"exportName": "ProviderResolveDynamicModelContext",
"kind": "type",
"source": {
"line": 313,
"line": 312,
"path": "src/plugins/types.ts"
}
},
@@ -4208,7 +4208,7 @@
"exportName": "ProviderResolveUsageAuthContext",
"kind": "type",
"source": {
"line": 396,
"line": 395,
"path": "src/plugins/types.ts"
}
},
@@ -4217,7 +4217,7 @@
"exportName": "ProviderRuntimeModel",
"kind": "type",
"source": {
"line": 296,
"line": 295,
"path": "src/plugins/types.ts"
}
},
@@ -4226,7 +4226,7 @@
"exportName": "ProviderThinkingPolicyContext",
"kind": "type",
"source": {
"line": 537,
"line": 536,
"path": "src/plugins/types.ts"
}
},
@@ -4235,7 +4235,7 @@
"exportName": "ProviderWrapStreamFnContext",
"kind": "type",
"source": {
"line": 478,
"line": 477,
"path": "src/plugins/types.ts"
}
},
@@ -4244,7 +4244,7 @@
"exportName": "SpeechProviderPlugin",
"kind": "type",
"source": {
"line": 934,
"line": 933,
"path": "src/plugins/types.ts"
}
}
@@ -4884,7 +4884,7 @@
"exportName": "ChannelGatewayContext",
"kind": "type",
"source": {
"line": 239,
"line": 238,
"path": "src/channels/plugins/types.adapters.ts"
}
},

View File

@@ -12,10 +12,10 @@
{"declaration":"export type ChannelAgentToolFactory = ChannelAgentToolFactory;","entrypoint":"index","exportName":"ChannelAgentToolFactory","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":23,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelCapabilities = ChannelCapabilities;","entrypoint":"index","exportName":"ChannelCapabilities","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":230,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelConfigSchema = ChannelConfigSchema;","entrypoint":"index","exportName":"ChannelConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":48,"sourcePath":"src/channels/plugins/types.plugin.ts"}
{"declaration":"export type ChannelConfiguredBindingConversationRef = ChannelConfiguredBindingConversationRef;","entrypoint":"index","exportName":"ChannelConfiguredBindingConversationRef","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":554,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfiguredBindingMatch = ChannelConfiguredBindingMatch;","entrypoint":"index","exportName":"ChannelConfiguredBindingMatch","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"index","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":563,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"index","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":239,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfiguredBindingConversationRef = ChannelConfiguredBindingConversationRef;","entrypoint":"index","exportName":"ChannelConfiguredBindingConversationRef","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":553,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfiguredBindingMatch = ChannelConfiguredBindingMatch;","entrypoint":"index","exportName":"ChannelConfiguredBindingMatch","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"index","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":562,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"index","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":238,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelId = ChannelId;","entrypoint":"index","exportName":"ChannelId","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionAdapter = ChannelMessageActionAdapter;","entrypoint":"index","exportName":"ChannelMessageActionAdapter","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionContext = ChannelMessageActionContext;","entrypoint":"index","exportName":"ChannelMessageActionContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":482,"sourcePath":"src/channels/plugins/types.core.ts"}
@@ -37,26 +37,26 @@
{"declaration":"export type DiagnosticEventPayload = DiagnosticEventPayload;","entrypoint":"index","exportName":"DiagnosticEventPayload","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":150,"sourcePath":"src/infra/diagnostic-events.ts"}
{"declaration":"export type GeneratedImageAsset = GeneratedImageAsset;","entrypoint":"index","exportName":"GeneratedImageAsset","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":4,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type HookEntry = HookEntry;","entrypoint":"index","exportName":"HookEntry","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":47,"sourcePath":"src/hooks/types.ts"}
{"declaration":"export type ImageGenerationProvider = ImageGenerationProvider;","entrypoint":"index","exportName":"ImageGenerationProvider","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":72,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationProvider = ImageGenerationProvider;","entrypoint":"index","exportName":"ImageGenerationProvider","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":66,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationRequest = ImageGenerationRequest;","entrypoint":"index","exportName":"ImageGenerationRequest","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":21,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationResolution = ImageGenerationResolution;","entrypoint":"index","exportName":"ImageGenerationResolution","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":12,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationResult = ImageGenerationResult;","entrypoint":"index","exportName":"ImageGenerationResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":42,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationResult = ImageGenerationResult;","entrypoint":"index","exportName":"ImageGenerationResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":36,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type ImageGenerationSourceImage = ImageGenerationSourceImage;","entrypoint":"index","exportName":"ImageGenerationSourceImage","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":14,"sourcePath":"src/image-generation/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"index","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":952,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"index","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"index","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1316,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":89,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"index","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":60,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"index","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":1314,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"index","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"index","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":59,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"index","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"index","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":156,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"index","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":141,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"index","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":296,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"index","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":155,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"index","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":140,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"index","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":295,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ReplyPayload = ReplyPayload;","entrypoint":"index","exportName":"ReplyPayload","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":76,"sourcePath":"src/auto-reply/types.ts"}
{"declaration":"export type RuntimeEnv = RuntimeEnv;","entrypoint":"index","exportName":"RuntimeEnv","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":4,"sourcePath":"src/runtime.ts"}
{"declaration":"export type RuntimeLogger = RuntimeLogger;","entrypoint":"index","exportName":"RuntimeLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/plugins/runtime/types-core.ts"}
{"declaration":"export type RuntimeLogger = RuntimeLogger;","entrypoint":"index","exportName":"RuntimeLogger","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":4,"sourcePath":"src/plugins/runtime/types-core.ts"}
{"declaration":"export type SecretInput = SecretInput;","entrypoint":"index","exportName":"SecretInput","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":16,"sourcePath":"src/config/types.secrets.ts"}
{"declaration":"export type SecretRef = SecretRef;","entrypoint":"index","exportName":"SecretRef","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":10,"sourcePath":"src/config/types.secrets.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"index","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":934,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"index","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":933,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type StatefulBindingTargetDescriptor = StatefulBindingTargetDescriptor;","entrypoint":"index","exportName":"StatefulBindingTargetDescriptor","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":17,"sourcePath":"src/channels/plugins/binding-types.ts"}
{"declaration":"export type StatefulBindingTargetDriver = StatefulBindingTargetDriver;","entrypoint":"index","exportName":"StatefulBindingTargetDriver","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":15,"sourcePath":"src/channels/plugins/stateful-target-drivers.ts"}
{"declaration":"export type StatefulBindingTargetReadyResult = StatefulBindingTargetReadyResult;","entrypoint":"index","exportName":"StatefulBindingTargetReadyResult","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/channels/plugins/stateful-target-drivers.ts"}
@@ -99,7 +99,7 @@
{"declaration":"export type CompiledAllowlist = CompiledAllowlist;","entrypoint":"allow-from","exportName":"CompiledAllowlist","importSpecifier":"openclaw/plugin-sdk/allow-from","kind":"type","recordType":"export","sourceLine":19,"sourcePath":"src/channels/allowlist-match.ts"}
{"category":"channel","entrypoint":"channel-actions","importSpecifier":"openclaw/plugin-sdk/channel-actions","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/channel-actions.ts"}
{"declaration":"export function createMessageToolButtonsSchema(): TSchema;","entrypoint":"channel-actions","exportName":"createMessageToolButtonsSchema","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":11,"sourcePath":"src/plugin-sdk/channel-actions.ts"}
{"declaration":"export function createMessageToolCardSchema(): TSchema;","entrypoint":"channel-actions","exportName":"createMessageToolCardSchema","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":29,"sourcePath":"src/plugin-sdk/channel-actions.ts"}
{"declaration":"export function createMessageToolCardSchema(): TSchema;","entrypoint":"channel-actions","exportName":"createMessageToolCardSchema","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":27,"sourcePath":"src/plugin-sdk/channel-actions.ts"}
{"declaration":"export function createUnionActionGate<TAccount, TKey extends string>(accounts: readonly TAccount[], createGate: (account: TAccount) => OptionalDefaultGate<TKey>): OptionalDefaultGate<TKey>;","entrypoint":"channel-actions","exportName":"createUnionActionGate","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/actions/shared.ts"}
{"declaration":"export function listTokenSourcedAccounts<TAccount extends TokenSourcedAccount>(accounts: readonly TAccount[]): TAccount[];","entrypoint":"channel-actions","exportName":"listTokenSourcedAccounts","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":7,"sourcePath":"src/channels/plugins/actions/shared.ts"}
{"declaration":"export function resolveReactionMessageId(params: { args: Record<string, unknown>; toolContext?: ReactionToolContext | undefined; }): string | number | undefined;","entrypoint":"channel-actions","exportName":"resolveReactionMessageId","importSpecifier":"openclaw/plugin-sdk/channel-actions","kind":"function","recordType":"export","sourceLine":7,"sourcePath":"src/channels/plugins/actions/reaction-message-id.ts"}
@@ -113,8 +113,8 @@
{"declaration":"export const MarkdownConfigSchema: z.ZodOptional<z.ZodObject<{ tables: z.ZodOptional<z.ZodEnum<{ off: \"off\"; bullets: \"bullets\"; code: \"code\"; }>>; }, z.core.$strict>>;","entrypoint":"channel-config-schema","exportName":"MarkdownConfigSchema","importSpecifier":"openclaw/plugin-sdk/channel-config-schema","kind":"const","recordType":"export","sourceLine":371,"sourcePath":"src/config/zod-schema.core.ts"}
{"declaration":"export const ToolPolicySchema: z.ZodOptional<z.ZodObject<{ allow: z.ZodOptional<z.ZodArray<z.ZodString>>; alsoAllow: z.ZodOptional<z.ZodArray<z.ZodString>>; deny: z.ZodOptional<z.ZodArray<z.ZodString>>; }, z.core.$strict>>;","entrypoint":"channel-config-schema","exportName":"ToolPolicySchema","importSpecifier":"openclaw/plugin-sdk/channel-config-schema","kind":"const","recordType":"export","sourceLine":253,"sourcePath":"src/config/zod-schema.agent-runtime.ts"}
{"category":"channel","entrypoint":"channel-contract","importSpecifier":"openclaw/plugin-sdk/channel-contract","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/channel-contract.ts"}
{"declaration":"export type BaseProbeResult = BaseProbeResult<TError>;","entrypoint":"channel-contract","exportName":"BaseProbeResult","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type BaseTokenResolution = BaseTokenResolution;","entrypoint":"channel-contract","exportName":"BaseTokenResolution","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":565,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type BaseProbeResult = BaseProbeResult<TError>;","entrypoint":"channel-contract","exportName":"BaseProbeResult","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type BaseTokenResolution = BaseTokenResolution;","entrypoint":"channel-contract","exportName":"BaseTokenResolution","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":564,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelAccountSnapshot = ChannelAccountSnapshot;","entrypoint":"channel-contract","exportName":"ChannelAccountSnapshot","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":144,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelAgentTool = ChannelAgentTool;","entrypoint":"channel-contract","exportName":"ChannelAgentTool","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":18,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelGroupContext = ChannelGroupContext;","entrypoint":"channel-contract","exportName":"ChannelGroupContext","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":216,"sourcePath":"src/channels/plugins/types.core.ts"}
@@ -165,43 +165,43 @@
{"declaration":"export function waitUntilAbort(signal?: AbortSignal | undefined, onAbort?: (() => void | Promise<void>) | undefined): Promise<void>;","entrypoint":"channel-runtime","exportName":"waitUntilAbort","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"function","recordType":"export","sourceLine":38,"sourcePath":"src/plugin-sdk/channel-lifecycle.ts"}
{"declaration":"export const CHANNEL_MESSAGE_ACTION_NAMES: readonly [\"send\", \"broadcast\", \"poll\", \"poll-vote\", \"react\", \"reactions\", \"read\", \"edit\", \"unsend\", \"reply\", \"sendWithEffect\", \"renameGroup\", \"setGroupIcon\", \"addParticipant\", \"removeParticipant\", \"leaveGroup\", \"sendAttachment\", \"delete\", \"pin\", \"unpin\", \"list-pins\", \"permissions\", \"thread-create\", \"thread-list\", \"thread-reply\", \"search\", \"sticker\", \"sticker-search\", \"member-info\", \"role-info\", \"emoji-list\", \"emoji-upload\", \"sticker-upload\", \"role-add\", \"role-remove\", \"channel-info\", \"channel-list\", \"channel-create\", \"channel-edit\", \"channel-delete\", \"channel-move\", \"category-create\", \"category-edit\", \"category-delete\", \"topic-create\", \"topic-edit\", \"voice-status\", \"event-list\", \"event-create\", \"timeout\", \"kick\", \"ban\", \"set-profile\", \"set-presence\", \"set-profile\", \"download-file\"];","entrypoint":"channel-runtime","exportName":"CHANNEL_MESSAGE_ACTION_NAMES","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"const","recordType":"export","sourceLine":1,"sourcePath":"src/channels/plugins/message-action-names.ts"}
{"declaration":"export const CHANNEL_MESSAGE_CAPABILITIES: readonly [\"interactive\", \"buttons\", \"cards\", \"components\", \"blocks\"];","entrypoint":"channel-runtime","exportName":"CHANNEL_MESSAGE_CAPABILITIES","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"const","recordType":"export","sourceLine":1,"sourcePath":"src/channels/plugins/message-capabilities.ts"}
{"declaration":"export type BaseProbeResult = BaseProbeResult<TError>;","entrypoint":"channel-runtime","exportName":"BaseProbeResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type BaseTokenResolution = BaseTokenResolution;","entrypoint":"channel-runtime","exportName":"BaseTokenResolution","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":565,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type BaseProbeResult = BaseProbeResult<TError>;","entrypoint":"channel-runtime","exportName":"BaseProbeResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type BaseTokenResolution = BaseTokenResolution;","entrypoint":"channel-runtime","exportName":"BaseTokenResolution","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":564,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelAccountSnapshot = ChannelAccountSnapshot;","entrypoint":"channel-runtime","exportName":"ChannelAccountSnapshot","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":144,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelAccountState = ChannelAccountState;","entrypoint":"channel-runtime","exportName":"ChannelAccountState","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":108,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelAgentPromptAdapter = ChannelAgentPromptAdapter;","entrypoint":"channel-runtime","exportName":"ChannelAgentPromptAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":463,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelAgentTool = ChannelAgentTool;","entrypoint":"channel-runtime","exportName":"ChannelAgentTool","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":18,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelAgentToolFactory = ChannelAgentToolFactory;","entrypoint":"channel-runtime","exportName":"ChannelAgentToolFactory","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":23,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelAllowlistAdapter = ChannelAllowlistAdapter;","entrypoint":"channel-runtime","exportName":"ChannelAllowlistAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":498,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelAuthAdapter = ChannelAuthAdapter;","entrypoint":"channel-runtime","exportName":"ChannelAuthAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":363,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelAllowlistAdapter = ChannelAllowlistAdapter;","entrypoint":"channel-runtime","exportName":"ChannelAllowlistAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":497,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelAuthAdapter = ChannelAuthAdapter;","entrypoint":"channel-runtime","exportName":"ChannelAuthAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":362,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelCapabilities = ChannelCapabilities;","entrypoint":"channel-runtime","exportName":"ChannelCapabilities","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":230,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelCapabilitiesDiagnostics = ChannelCapabilitiesDiagnostics;","entrypoint":"channel-runtime","exportName":"ChannelCapabilitiesDiagnostics","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":47,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelCapabilitiesDisplayLine = ChannelCapabilitiesDisplayLine;","entrypoint":"channel-runtime","exportName":"ChannelCapabilitiesDisplayLine","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":42,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelCapabilitiesDisplayTone = ChannelCapabilitiesDisplayTone;","entrypoint":"channel-runtime","exportName":"ChannelCapabilitiesDisplayTone","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":40,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelCommandAdapter = ChannelCommandAdapter;","entrypoint":"channel-runtime","exportName":"ChannelCommandAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":445,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelCommandAdapter = ChannelCommandAdapter;","entrypoint":"channel-runtime","exportName":"ChannelCommandAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":444,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfigAdapter = ChannelConfigAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelConfigAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":91,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfiguredBindingConversationRef = ChannelConfiguredBindingConversationRef;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingConversationRef","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":554,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfiguredBindingMatch = ChannelConfiguredBindingMatch;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingMatch","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":563,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelDirectoryAdapter = ChannelDirectoryAdapter;","entrypoint":"channel-runtime","exportName":"ChannelDirectoryAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":407,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfiguredBindingConversationRef = ChannelConfiguredBindingConversationRef;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingConversationRef","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":553,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfiguredBindingMatch = ChannelConfiguredBindingMatch;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingMatch","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":562,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelDirectoryAdapter = ChannelDirectoryAdapter;","entrypoint":"channel-runtime","exportName":"ChannelDirectoryAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":406,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelDirectoryEntry = ChannelDirectoryEntry;","entrypoint":"channel-runtime","exportName":"ChannelDirectoryEntry","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":469,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelDirectoryEntryKind = ChannelDirectoryEntryKind;","entrypoint":"channel-runtime","exportName":"ChannelDirectoryEntryKind","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":467,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelElevatedAdapter = ChannelElevatedAdapter;","entrypoint":"channel-runtime","exportName":"ChannelElevatedAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":438,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelExecApprovalAdapter = ChannelExecApprovalAdapter;","entrypoint":"channel-runtime","exportName":"ChannelExecApprovalAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":464,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelElevatedAdapter = ChannelElevatedAdapter;","entrypoint":"channel-runtime","exportName":"ChannelElevatedAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":437,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelExecApprovalAdapter = ChannelExecApprovalAdapter;","entrypoint":"channel-runtime","exportName":"ChannelExecApprovalAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":463,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelExecApprovalForwardTarget = ChannelExecApprovalForwardTarget;","entrypoint":"channel-runtime","exportName":"ChannelExecApprovalForwardTarget","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelExecApprovalInitiatingSurfaceState = ChannelExecApprovalInitiatingSurfaceState;","entrypoint":"channel-runtime","exportName":"ChannelExecApprovalInitiatingSurfaceState","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":27,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelGatewayAdapter = ChannelGatewayAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelGatewayAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":347,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":239,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelGatewayAdapter = ChannelGatewayAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelGatewayAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":346,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":238,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelGroupAdapter = ChannelGroupAdapter;","entrypoint":"channel-runtime","exportName":"ChannelGroupAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":122,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelGroupContext = ChannelGroupContext;","entrypoint":"channel-runtime","exportName":"ChannelGroupContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":216,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelHeartbeatAdapter = ChannelHeartbeatAdapter;","entrypoint":"channel-runtime","exportName":"ChannelHeartbeatAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":373,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelHeartbeatAdapter = ChannelHeartbeatAdapter;","entrypoint":"channel-runtime","exportName":"ChannelHeartbeatAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":372,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelHeartbeatDeps = ChannelHeartbeatDeps;","entrypoint":"channel-runtime","exportName":"ChannelHeartbeatDeps","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":116,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelId = ChannelId;","entrypoint":"channel-runtime","exportName":"ChannelId","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelLifecycleAdapter = ChannelLifecycleAdapter;","entrypoint":"channel-runtime","exportName":"ChannelLifecycleAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":450,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelLoginWithQrStartResult = ChannelLoginWithQrStartResult;","entrypoint":"channel-runtime","exportName":"ChannelLoginWithQrStartResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":318,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelLoginWithQrWaitResult = ChannelLoginWithQrWaitResult;","entrypoint":"channel-runtime","exportName":"ChannelLoginWithQrWaitResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":323,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelLogoutContext = ChannelLogoutContext<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelLogoutContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":328,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelLogoutResult = ChannelLogoutResult;","entrypoint":"channel-runtime","exportName":"ChannelLogoutResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":312,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelLifecycleAdapter = ChannelLifecycleAdapter;","entrypoint":"channel-runtime","exportName":"ChannelLifecycleAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":449,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelLoginWithQrStartResult = ChannelLoginWithQrStartResult;","entrypoint":"channel-runtime","exportName":"ChannelLoginWithQrStartResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":317,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelLoginWithQrWaitResult = ChannelLoginWithQrWaitResult;","entrypoint":"channel-runtime","exportName":"ChannelLoginWithQrWaitResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":322,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelLogoutContext = ChannelLogoutContext<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelLogoutContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":327,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelLogoutResult = ChannelLogoutResult;","entrypoint":"channel-runtime","exportName":"ChannelLogoutResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":311,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelLogSink = ChannelLogSink;","entrypoint":"channel-runtime","exportName":"ChannelLogSink","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":209,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMentionAdapter = ChannelMentionAdapter;","entrypoint":"channel-runtime","exportName":"ChannelMentionAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":260,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessageActionAdapter = ChannelMessageActionAdapter;","entrypoint":"channel-runtime","exportName":"ChannelMessageActionAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/channels/plugins/types.core.ts"}
@@ -213,22 +213,22 @@
{"declaration":"export type ChannelMessageToolSchemaContribution = ChannelMessageToolSchemaContribution;","entrypoint":"channel-runtime","exportName":"ChannelMessageToolSchemaContribution","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":51,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMessagingAdapter = ChannelMessagingAdapter;","entrypoint":"channel-runtime","exportName":"ChannelMessagingAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":395,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelMeta = ChannelMeta;","entrypoint":"channel-runtime","exportName":"ChannelMeta","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":122,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelOutboundAdapter = ChannelOutboundAdapter;","entrypoint":"channel-runtime","exportName":"ChannelOutboundAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":155,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelOutboundAdapter = ChannelOutboundAdapter;","entrypoint":"channel-runtime","exportName":"ChannelOutboundAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":154,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelOutboundContext = ChannelOutboundContext;","entrypoint":"channel-runtime","exportName":"ChannelOutboundContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":128,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelOutboundTargetMode = ChannelOutboundTargetMode;","entrypoint":"channel-runtime","exportName":"ChannelOutboundTargetMode","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":15,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelPairingAdapter = ChannelPairingAdapter;","entrypoint":"channel-runtime","exportName":"ChannelPairingAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":336,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelPairingAdapter = ChannelPairingAdapter;","entrypoint":"channel-runtime","exportName":"ChannelPairingAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":335,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelPlugin = ChannelPlugin<ResolvedAccount, Probe, Audit>;","entrypoint":"channel-runtime","exportName":"ChannelPlugin","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":55,"sourcePath":"src/channels/plugins/types.plugin.ts"}
{"declaration":"export type ChannelPollContext = ChannelPollContext;","entrypoint":"channel-runtime","exportName":"ChannelPollContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":547,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelPollResult = ChannelPollResult;","entrypoint":"channel-runtime","exportName":"ChannelPollResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":538,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelResolveKind = ChannelResolveKind;","entrypoint":"channel-runtime","exportName":"ChannelResolveKind","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":418,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelResolverAdapter = ChannelResolverAdapter;","entrypoint":"channel-runtime","exportName":"ChannelResolverAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":428,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelResolveResult = ChannelResolveResult;","entrypoint":"channel-runtime","exportName":"ChannelResolveResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":420,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelSecurityAdapter = ChannelSecurityAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelSecurityAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":576,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelResolveKind = ChannelResolveKind;","entrypoint":"channel-runtime","exportName":"ChannelResolveKind","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":417,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelResolverAdapter = ChannelResolverAdapter;","entrypoint":"channel-runtime","exportName":"ChannelResolverAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":427,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelResolveResult = ChannelResolveResult;","entrypoint":"channel-runtime","exportName":"ChannelResolveResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":419,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelSecurityAdapter = ChannelSecurityAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelSecurityAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":575,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelSecurityContext = ChannelSecurityContext<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelSecurityContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":254,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelSecurityDmPolicy = ChannelSecurityDmPolicy;","entrypoint":"channel-runtime","exportName":"ChannelSecurityDmPolicy","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":245,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelSetupAdapter = ChannelSetupAdapter;","entrypoint":"channel-runtime","exportName":"ChannelSetupAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":56,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelSetupInput = ChannelSetupInput;","entrypoint":"channel-runtime","exportName":"ChannelSetupInput","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":63,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelStatusAdapter = ChannelStatusAdapter<ResolvedAccount, Probe, Audit>;","entrypoint":"channel-runtime","exportName":"ChannelStatusAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":185,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelStatusAdapter = ChannelStatusAdapter<ResolvedAccount, Probe, Audit>;","entrypoint":"channel-runtime","exportName":"ChannelStatusAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":184,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelStatusIssue = ChannelStatusIssue;","entrypoint":"channel-runtime","exportName":"ChannelStatusIssue","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":100,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelStreamingAdapter = ChannelStreamingAdapter;","entrypoint":"channel-runtime","exportName":"ChannelStreamingAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":279,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelStructuredComponents = ChannelStructuredComponents;","entrypoint":"channel-runtime","exportName":"ChannelStructuredComponents","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":288,"sourcePath":"src/channels/plugins/types.core.ts"}
@@ -373,53 +373,53 @@
{"declaration":"export type ChannelPlugin = ChannelPlugin<ResolvedAccount, Probe, Audit>;","entrypoint":"core","exportName":"ChannelPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":55,"sourcePath":"src/channels/plugins/types.plugin.ts"}
{"declaration":"export type GatewayBindUrlResult = GatewayBindUrlResult;","entrypoint":"core","exportName":"GatewayBindUrlResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1,"sourcePath":"src/shared/gateway-bind-url.ts"}
{"declaration":"export type GatewayRequestHandlerOptions = GatewayRequestHandlerOptions;","entrypoint":"core","exportName":"GatewayRequestHandlerOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":112,"sourcePath":"src/gateway/server-methods/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"core","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":952,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"core","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"core","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1316,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1070,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":89,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1298,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1287,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1279,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"core","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":104,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"core","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":121,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":968,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"core","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1099,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"core","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":60,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"core","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1314,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"core","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1068,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"core","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"core","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1296,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"core","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1285,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"core","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1277,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolContext = OpenClawPluginToolContext;","entrypoint":"core","exportName":"OpenClawPluginToolContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":103,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginToolFactory = OpenClawPluginToolFactory;","entrypoint":"core","exportName":"OpenClawPluginToolFactory","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":120,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"core","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":966,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"core","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1097,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"core","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":59,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"core","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"core","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":572,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"core","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":156,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"core","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":447,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"core","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":234,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"core","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":218,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"core","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":141,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"core","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":500,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":525,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"core","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":488,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":255,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"core","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":278,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":549,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":588,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"core","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":428,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"core","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"core","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":339,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"core","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":375,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":330,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"core","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":461,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"core","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":354,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"core","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":415,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":313,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"core","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":396,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"core","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":296,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":537,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"core","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":571,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"core","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":155,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"core","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":446,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"core","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":233,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"core","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":217,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"core","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":140,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"core","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":499,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":515,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"core","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":524,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"core","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":487,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":254,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"core","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":277,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":548,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"core","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":587,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"core","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":427,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"core","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"core","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":338,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"core","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":374,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":329,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"core","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":460,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"core","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":353,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"core","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":414,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"core","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":312,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"core","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":395,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"core","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":295,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"core","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":536,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderUsageSnapshot = ProviderUsageSnapshot;","entrypoint":"core","exportName":"ProviderUsageSnapshot","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/infra/provider-usage.types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"core","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":478,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"core","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":477,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type RoutePeer = RoutePeer;","entrypoint":"core","exportName":"RoutePeer","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":21,"sourcePath":"src/routing/resolve-route.ts"}
{"declaration":"export type RoutePeerKind = ChatType;","entrypoint":"core","exportName":"RoutePeerKind","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":19,"sourcePath":"src/routing/resolve-route.ts"}
{"declaration":"export type SecretFileReadOptions = SecretFileReadOptions;","entrypoint":"core","exportName":"SecretFileReadOptions","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":7,"sourcePath":"src/infra/secret-file.ts"}
{"declaration":"export type SecretFileReadResult = SecretFileReadResult;","entrypoint":"core","exportName":"SecretFileReadResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":12,"sourcePath":"src/infra/secret-file.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"core","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":934,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"core","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":933,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type TailscaleStatusCommandResult = TailscaleStatusCommandResult;","entrypoint":"core","exportName":"TailscaleStatusCommandResult","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":1,"sourcePath":"src/shared/tailscale-status.ts"}
{"declaration":"export type TailscaleStatusCommandRunner = TailscaleStatusCommandRunner;","entrypoint":"core","exportName":"TailscaleStatusCommandRunner","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":6,"sourcePath":"src/shared/tailscale-status.ts"}
{"declaration":"export type UsageProviderId = UsageProviderId;","entrypoint":"core","exportName":"UsageProviderId","importSpecifier":"openclaw/plugin-sdk/core","kind":"type","recordType":"export","sourceLine":20,"sourcePath":"src/infra/provider-usage.types.ts"}
@@ -429,45 +429,45 @@
{"declaration":"export function definePluginEntry({ id, name, description, kind, configSchema, register, }: DefinePluginEntryOptions): DefinedPluginEntry;","entrypoint":"plugin-entry","exportName":"definePluginEntry","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export","sourceLine":88,"sourcePath":"src/plugin-sdk/plugin-entry.ts"}
{"declaration":"export function emptyPluginConfigSchema(): OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"emptyPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"function","recordType":"export","sourceLine":13,"sourcePath":"src/plugins/config-schema.ts"}
{"declaration":"export type AnyAgentTool = AnyAgentTool;","entrypoint":"plugin-entry","exportName":"AnyAgentTool","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":9,"sourcePath":"src/agents/tools/common.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"plugin-entry","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":952,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type MediaUnderstandingProviderPlugin = MediaUnderstandingProvider;","entrypoint":"plugin-entry","exportName":"MediaUnderstandingProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":951,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"plugin-entry","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1316,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1070,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":89,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1298,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"plugin-entry","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1287,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1279,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"plugin-entry","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":968,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"plugin-entry","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1099,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"plugin-entry","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":60,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":572,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":156,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":447,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":234,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":218,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"plugin-entry","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":141,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"plugin-entry","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":500,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":525,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"plugin-entry","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":488,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":255,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"plugin-entry","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":278,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":549,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":588,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"plugin-entry","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":428,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":559,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":339,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"plugin-entry","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":375,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":330,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":461,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":354,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"plugin-entry","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":415,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":313,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":396,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"plugin-entry","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":296,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":537,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"plugin-entry","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":478,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"plugin-entry","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":934,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginApi = OpenClawPluginApi;","entrypoint":"plugin-entry","exportName":"OpenClawPluginApi","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1314,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginCommandDefinition = OpenClawPluginCommandDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginCommandDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1068,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginConfigSchema = OpenClawPluginConfigSchema;","entrypoint":"plugin-entry","exportName":"OpenClawPluginConfigSchema","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":88,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginDefinition = OpenClawPluginDefinition;","entrypoint":"plugin-entry","exportName":"OpenClawPluginDefinition","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1296,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginService = OpenClawPluginService;","entrypoint":"plugin-entry","exportName":"OpenClawPluginService","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1285,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type OpenClawPluginServiceContext = OpenClawPluginServiceContext;","entrypoint":"plugin-entry","exportName":"OpenClawPluginServiceContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1277,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginCommandContext = PluginCommandContext;","entrypoint":"plugin-entry","exportName":"PluginCommandContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":966,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginInteractiveTelegramHandlerContext = PluginInteractiveTelegramHandlerContext;","entrypoint":"plugin-entry","exportName":"PluginInteractiveTelegramHandlerContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":1097,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type PluginLogger = PluginLogger;","entrypoint":"plugin-entry","exportName":"PluginLogger","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":59,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAugmentModelCatalogContext = ProviderAugmentModelCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderAugmentModelCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":571,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthContext = ProviderAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":155,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthDoctorHintContext = ProviderAuthDoctorHintContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthDoctorHintContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":446,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethod = ProviderAuthMethod;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethod","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":233,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthMethodNonInteractiveContext = ProviderAuthMethodNonInteractiveContext;","entrypoint":"plugin-entry","exportName":"ProviderAuthMethodNonInteractiveContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":217,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderAuthResult = ProviderAuthResult;","entrypoint":"plugin-entry","exportName":"ProviderAuthResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":140,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuildMissingAuthMessageContext = ProviderBuildMissingAuthMessageContext;","entrypoint":"plugin-entry","exportName":"ProviderBuildMissingAuthMessageContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":499,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionContext = ProviderBuiltInModelSuppressionContext;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":515,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderBuiltInModelSuppressionResult = ProviderBuiltInModelSuppressionResult;","entrypoint":"plugin-entry","exportName":"ProviderBuiltInModelSuppressionResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":524,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCacheTtlEligibilityContext = ProviderCacheTtlEligibilityContext;","entrypoint":"plugin-entry","exportName":"ProviderCacheTtlEligibilityContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":487,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderCatalogContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":254,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderCatalogResult = ProviderCatalogResult;","entrypoint":"plugin-entry","exportName":"ProviderCatalogResult","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":277,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDefaultThinkingPolicyContext = ProviderDefaultThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderDefaultThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":548,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderDiscoveryContext = ProviderCatalogContext;","entrypoint":"plugin-entry","exportName":"ProviderDiscoveryContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":587,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderFetchUsageSnapshotContext = ProviderFetchUsageSnapshotContext;","entrypoint":"plugin-entry","exportName":"ProviderFetchUsageSnapshotContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":427,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderModernModelPolicyContext = ProviderModernModelPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderModernModelPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":558,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderNormalizeResolvedModelContext = ProviderNormalizeResolvedModelContext;","entrypoint":"plugin-entry","exportName":"ProviderNormalizeResolvedModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":338,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPreparedRuntimeAuth = ProviderPreparedRuntimeAuth;","entrypoint":"plugin-entry","exportName":"ProviderPreparedRuntimeAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":374,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":329,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareExtraParamsContext = ProviderPrepareExtraParamsContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareExtraParamsContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":460,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderPrepareRuntimeAuthContext = ProviderPrepareRuntimeAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderPrepareRuntimeAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":353,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolvedUsageAuth = ProviderResolvedUsageAuth;","entrypoint":"plugin-entry","exportName":"ProviderResolvedUsageAuth","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":414,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveDynamicModelContext = ProviderResolveDynamicModelContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveDynamicModelContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":312,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderResolveUsageAuthContext = ProviderResolveUsageAuthContext;","entrypoint":"plugin-entry","exportName":"ProviderResolveUsageAuthContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":395,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderRuntimeModel = ProviderRuntimeModel;","entrypoint":"plugin-entry","exportName":"ProviderRuntimeModel","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":295,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderThinkingPolicyContext = ProviderThinkingPolicyContext;","entrypoint":"plugin-entry","exportName":"ProviderThinkingPolicyContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":536,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type ProviderWrapStreamFnContext = ProviderWrapStreamFnContext;","entrypoint":"plugin-entry","exportName":"ProviderWrapStreamFnContext","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":477,"sourcePath":"src/plugins/types.ts"}
{"declaration":"export type SpeechProviderPlugin = SpeechProviderPlugin;","entrypoint":"plugin-entry","exportName":"SpeechProviderPlugin","importSpecifier":"openclaw/plugin-sdk/plugin-entry","kind":"type","recordType":"export","sourceLine":933,"sourcePath":"src/plugins/types.ts"}
{"category":"provider","entrypoint":"provider-onboard","importSpecifier":"openclaw/plugin-sdk/provider-onboard","recordType":"module","sourceLine":1,"sourcePath":"src/plugin-sdk/provider-onboard.ts"}
{"declaration":"export function applyAgentDefaultModelPrimary(cfg: OpenClawConfig, primary: string): OpenClawConfig;","entrypoint":"provider-onboard","exportName":"applyAgentDefaultModelPrimary","importSpecifier":"openclaw/plugin-sdk/provider-onboard","kind":"function","recordType":"export","sourceLine":76,"sourcePath":"src/plugins/provider-onboarding-config.ts"}
{"declaration":"export function applyCloudflareAiGatewayConfig(cfg: OpenClawConfig, params?: { accountId?: string | undefined; gatewayId?: string | undefined; } | undefined): OpenClawConfig;","entrypoint":"provider-onboard","exportName":"applyCloudflareAiGatewayConfig","importSpecifier":"openclaw/plugin-sdk/provider-onboard","kind":"function","recordType":"export","sourceLine":85,"sourcePath":"extensions/cloudflare-ai-gateway/onboard.ts"}
@@ -537,7 +537,7 @@
{"declaration":"export function removeAckReactionAfterReply(params: { removeAfterReply: boolean; ackReactionPromise: Promise<boolean> | null; ackReactionValue: string | null; remove: () => Promise<void>; onError?: ((err: unknown) => void) | undefined; }): void;","entrypoint":"testing","exportName":"removeAckReactionAfterReply","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":81,"sourcePath":"src/channels/ack-reactions.ts"}
{"declaration":"export function shouldAckReaction(params: AckReactionGateParams): boolean;","entrypoint":"testing","exportName":"shouldAckReaction","importSpecifier":"openclaw/plugin-sdk/testing","kind":"function","recordType":"export","sourceLine":16,"sourcePath":"src/channels/ack-reactions.ts"}
{"declaration":"export type ChannelAccountSnapshot = ChannelAccountSnapshot;","entrypoint":"testing","exportName":"ChannelAccountSnapshot","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":144,"sourcePath":"src/channels/plugins/types.core.ts"}
{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"testing","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":239,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"testing","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":238,"sourcePath":"src/channels/plugins/types.adapters.ts"}
{"declaration":"export type MockFn = MockFn<T>;","entrypoint":"testing","exportName":"MockFn","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":5,"sourcePath":"src/test-utils/vitest-mock-fn.ts"}
{"declaration":"export type OpenClawConfig = OpenClawConfig;","entrypoint":"testing","exportName":"OpenClawConfig","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":32,"sourcePath":"src/config/types.openclaw.ts"}
{"declaration":"export type PluginRuntime = PluginRuntime;","entrypoint":"testing","exportName":"PluginRuntime","importSpecifier":"openclaw/plugin-sdk/testing","kind":"type","recordType":"export","sourceLine":54,"sourcePath":"src/plugins/runtime/types.ts"}

View File

@@ -33,7 +33,6 @@ Text is supported everywhere; media and reactions vary by channel.
- [Twitch](/channels/twitch) — Twitch chat via IRC connection (plugin, installed separately).
- [Voice Call](/plugins/voice-call) — Telephony via Plivo or Twilio (plugin, installed separately).
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
- [WeChat](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin) — Tencent iLink Bot plugin via QR login; private chats only.
- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).

View File

@@ -36,7 +36,7 @@ openclaw pairing list telegram
openclaw pairing approve telegram <CODE>
```
Supported channels: `bluebubbles`, `discord`, `feishu`, `googlechat`, `imessage`, `irc`, `line`, `matrix`, `mattermost`, `msteams`, `nextcloud-talk`, `nostr`, `openclaw-weixin`, `signal`, `slack`, `synology-chat`, `telegram`, `twitch`, `whatsapp`, `zalo`, `zalouser`.
Supported channels: `bluebubbles`, `discord`, `feishu`, `googlechat`, `imessage`, `irc`, `line`, `matrix`, `mattermost`, `msteams`, `nextcloud-talk`, `nostr`, `signal`, `slack`, `synology-chat`, `telegram`, `twitch`, `whatsapp`, `zalo`, `zalouser`.
### Where the state lives

View File

@@ -1,5 +1,5 @@
---
summary: "CLI reference for `openclaw config` (get/set/unset/file/schema/validate)"
summary: "CLI reference for `openclaw config` (get/set/unset/file/validate)"
read_when:
- You want to read or edit config non-interactively
title: "config"
@@ -7,7 +7,7 @@ title: "config"
# `openclaw config`
Config helpers for non-interactive edits in `openclaw.json`: get/set/unset/file/schema/validate
Config helpers for non-interactive edits in `openclaw.json`: get/set/unset/validate
values by path and print the active config file. Run without a subcommand to
open the configure wizard (same as `openclaw configure`).
@@ -15,7 +15,6 @@ open the configure wizard (same as `openclaw configure`).
```bash
openclaw config file
openclaw config schema
openclaw config get browser.executablePath
openclaw config set browser.executablePath "/usr/bin/google-chrome"
openclaw config set agents.defaults.heartbeat.every "2h"
@@ -28,21 +27,7 @@ openclaw config validate
openclaw config validate --json
```
### `config schema`
Print the generated JSON schema for `openclaw.json` to stdout as plain text.
```bash
openclaw config schema
```
Pipe it into a file when you want to inspect or validate it with other tools:
```bash
openclaw config schema > openclaw.schema.json
```
### Paths
## Paths
Paths use dot or bracket notation:

View File

@@ -396,7 +396,7 @@ Interactive configuration wizard (models, channels, skills, gateway).
### `config`
Non-interactive config helpers (get/set/unset/file/schema/validate). Running `openclaw config` with no
Non-interactive config helpers (get/set/unset/file/validate). Running `openclaw config` with no
subcommand launches the wizard.
Subcommands:
@@ -413,7 +413,6 @@ Subcommands:
- `config set --strict-json`: require JSON5 parsing for path/value input. `--json` remains a legacy alias for strict parsing outside dry-run output mode.
- `config unset <path>`: remove a value.
- `config file`: print the active config file path.
- `config schema`: print the generated JSON schema for `openclaw.json`.
- `config validate`: validate the current config against the schema without starting the gateway.
- `config validate --json`: emit machine-readable JSON output.

View File

@@ -165,30 +165,6 @@ Set `stream: true` to receive Server-Sent Events (SSE):
- Each event line is `data: <json>`
- Stream ends with `data: [DONE]`
## Open WebUI quick setup
For a basic Open WebUI connection:
- Base URL: `http://127.0.0.1:18789/v1`
- Docker on macOS base URL: `http://host.docker.internal:18789/v1`
- API key: your Gateway bearer token
- Model: `openclaw/default`
Expected behavior:
- `GET /v1/models` should list `openclaw/default`
- Open WebUI should use `openclaw/default` as the chat model id
- If you want a specific backend provider/model for that agent, set the agent's normal default model or send `x-openclaw-model`
Quick smoke:
```bash
curl -sS http://127.0.0.1:18789/v1/models \
-H 'Authorization: Bearer YOUR_TOKEN'
```
If that returns `openclaw/default`, most Open WebUI setups can connect with the same base URL and token.
## Examples
Non-streaming:

View File

@@ -2153,8 +2153,9 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
1. Upgrade to a current OpenClaw release (or run from source `main`), then restart the gateway.
2. Make sure MiniMax is configured (wizard or JSON), or that a MiniMax API key
exists in env/auth profiles so the provider can be injected.
3. Use the exact model id (case-sensitive): `minimax/MiniMax-M2.7` or
`minimax/MiniMax-M2.7-highspeed`.
3. Use the exact model id (case-sensitive): `minimax/MiniMax-M2.7`,
`minimax/MiniMax-M2.7-highspeed`, `minimax/MiniMax-M2.5`, or
`minimax/MiniMax-M2.5-highspeed`.
4. Run:
```bash

View File

@@ -424,16 +424,10 @@ If you want to rely on env keys (e.g. exported in your `~/.profile`), run local
## Docker runners (optional "works in Linux" checks)
These Docker runners split into two buckets:
- Live-model runners: `test:docker:live-models` and `test:docker:live-gateway` run `pnpm test:live` inside the repo Docker image, mounting your local config dir and workspace (and sourcing `~/.profile` if mounted).
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:gateway-network`, and `test:docker:plugins` boot one or more real containers and verify higher-level integration paths.
The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:
These run `pnpm test:live` inside the repo Docker image, mounting your local config dir and workspace (and sourcing `~/.profile` if mounted). They also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:
- Direct models: `pnpm test:docker:live-models` (script: `scripts/test-live-models-docker.sh`)
- Gateway + dev agent: `pnpm test:docker:live-gateway` (script: `scripts/test-live-gateway-models-docker.sh`)
- Open WebUI live smoke: `pnpm test:docker:openwebui` (script: `scripts/e2e/openwebui-docker.sh`)
- Onboarding wizard (TTY, full scaffolding): `pnpm test:docker:onboard` (script: `scripts/e2e/onboard-docker.sh`)
- Gateway networking (two containers, WS auth + health): `pnpm test:docker:gateway-network` (script: `scripts/e2e/gateway-network-docker.sh`)
- Plugins (install smoke + `/plugin` alias + Claude-bundle restart semantics): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`)
@@ -446,17 +440,6 @@ real Telegram/Discord/etc. channel workers inside the container.
`test:docker:live-models` still runs `pnpm test:live`, so pass through
`OPENCLAW_LIVE_GATEWAY_*` as well when you need to narrow or exclude gateway
live coverage from that Docker lane.
`test:docker:openwebui` is a higher-level compatibility smoke: it starts an
OpenClaw gateway container with the OpenAI-compatible HTTP endpoints enabled,
starts a pinned Open WebUI container against that gateway, signs in through
Open WebUI, verifies `/api/models` exposes `openclaw/default`, then sends a
real chat request through Open WebUI's `/api/chat/completions` proxy.
The first run can be noticeably slower because Docker may need to pull the
Open WebUI image and Open WebUI may need to finish its own cold-start setup.
This lane expects a usable live model key, and `OPENCLAW_PROFILE_FILE`
(`~/.profile` by default) is the primary way to provide it in Dockerized runs.
Successful runs print a small JSON payload like `{ "ok": true, "model":
"openclaw/default", ... }`.
Manual ACP plain-language thread smoke (not CI):
@@ -475,9 +458,6 @@ Useful env vars:
- `OPENCLAW_LIVE_GATEWAY_MODELS=...` / `OPENCLAW_LIVE_MODELS=...` to narrow the run
- `OPENCLAW_LIVE_GATEWAY_PROVIDERS=...` / `OPENCLAW_LIVE_PROVIDERS=...` to filter providers in-container
- `OPENCLAW_LIVE_REQUIRE_PROFILE_KEYS=1` to ensure creds come from the profile store (not env)
- `OPENCLAW_OPENWEBUI_MODEL=...` to choose the model exposed by the gateway for the Open WebUI smoke
- `OPENCLAW_OPENWEBUI_PROMPT=...` to override the nonce-check prompt used by the Open WebUI smoke
- `OPENWEBUI_IMAGE=...` to override the pinned Open WebUI image tag
## Docs sanity

View File

@@ -54,7 +54,7 @@ OpenClaw is a **self-hosted gateway** that connects your favorite chat apps —
- **Agent-native**: built for coding agents with tool use, sessions, memory, and multi-agent routing
- **Open source**: MIT licensed, community-driven
**What do you need?** Node 24 (recommended), or Node 22 LTS (`22.14+`) for compatibility, an API key from your chosen provider, and 5 minutes. For best quality and security, use the strongest latest-generation model available.
**What do you need?** Node 24 (recommended), or Node 22 LTS (`22.16+`) for compatibility, an API key from your chosen provider, and 5 minutes. For best quality and security, use the strongest latest-generation model available.
## How it works

View File

@@ -48,7 +48,7 @@ The Ansible playbook installs and configures:
1. **Tailscale** -- mesh VPN for secure remote access
2. **UFW firewall** -- SSH + Tailscale ports only
3. **Docker CE + Compose V2** -- for agent sandboxes
4. **Node.js 24 + pnpm** -- runtime dependencies (Node 22 LTS, currently `22.14+`, remains supported)
4. **Node.js 24 + pnpm** -- runtime dependencies (Node 22 LTS, currently `22.16+`, remains supported)
5. **OpenClaw** -- host-based, not containerized
6. **Systemd service** -- auto-start with security hardening

View File

@@ -41,7 +41,7 @@ Bun is an optional local runtime for running TypeScript directly (`bun run ...`,
Bun blocks dependency lifecycle scripts unless explicitly trusted. For this repo, the commonly blocked scripts are not required:
- `@whiskeysockets/baileys` `preinstall` -- checks Node major >= 20 (OpenClaw defaults to Node 24 and still supports Node 22 LTS, currently `22.14+`)
- `@whiskeysockets/baileys` `preinstall` -- checks Node major >= 20 (OpenClaw defaults to Node 24 and still supports Node 22 LTS, currently `22.16+`)
- `protobufjs` `postinstall` -- emits warnings about incompatible version schemes (no build artifacts)
If you hit a runtime issue that requires these scripts, trust them explicitly:

View File

@@ -45,7 +45,7 @@ For all flags and CI/automation options, see [Installer internals](/install/inst
## System requirements
- **Node 24** (recommended) or Node 22.14+ — the installer script handles this automatically
- **Node 24** (recommended) or Node 22.16+ — the installer script handles this automatically
- **macOS, Linux, or Windows** — both native Windows and WSL2 are supported; WSL2 is more stable. See [Windows](/platforms/windows).
- `pnpm` is only needed if you build from source

View File

@@ -71,7 +71,7 @@ Recommended for most interactive installs on macOS/Linux/WSL.
Supports macOS and Linux (including WSL). If macOS is detected, installs Homebrew if missing.
</Step>
<Step title="Ensure Node.js 24 by default">
Checks Node version and installs Node 24 if needed (Homebrew on macOS, NodeSource setup scripts on Linux apt/dnf/yum). OpenClaw still supports Node 22 LTS, currently `22.14+`, for compatibility.
Checks Node version and installs Node 24 if needed (Homebrew on macOS, NodeSource setup scripts on Linux apt/dnf/yum). OpenClaw still supports Node 22 LTS, currently `22.16+`, for compatibility.
</Step>
<Step title="Ensure Git">
Installs Git if missing.
@@ -257,7 +257,7 @@ Designed for environments where you want everything under a local prefix (defaul
Requires PowerShell 5+.
</Step>
<Step title="Ensure Node.js 24 by default">
If missing, attempts install via winget, then Chocolatey, then Scoop. Node 22 LTS, currently `22.14+`, remains supported for compatibility.
If missing, attempts install via winget, then Chocolatey, then Scoop. Node 22 LTS, currently `22.16+`, remains supported for compatibility.
</Step>
<Step title="Install OpenClaw">
- `npm` method (default): global npm install using selected `-Tag`

View File

@@ -9,7 +9,7 @@ read_when:
# Node.js
OpenClaw requires **Node 22.14 or newer**. **Node 24 is the default and recommended runtime** for installs, CI, and release workflows. Node 22 remains supported via the active LTS line. The [installer script](/install#alternative-install-methods) will detect and install Node automatically — this page is for when you want to set up Node yourself and make sure everything is wired up correctly (versions, PATH, global installs).
OpenClaw requires **Node 22.16 or newer**. **Node 24 is the default and recommended runtime** for installs, CI, and release workflows. Node 22 remains supported via the active LTS line. The [installer script](/install#alternative-install-methods) will detect and install Node automatically — this page is for when you want to set up Node yourself and make sure everything is wired up correctly (versions, PATH, global installs).
## Check your version
@@ -17,7 +17,7 @@ OpenClaw requires **Node 22.14 or newer**. **Node 24 is the default and recommen
node -v
```
If this prints `v24.x.x` or higher, you're on the recommended default. If it prints `v22.14.x` or higher, you're on the supported Node 22 LTS path, but we still recommend upgrading to Node 24 when convenient. If Node isn't installed or the version is too old, pick an install method below.
If this prints `v24.x.x` or higher, you're on the recommended default. If it prints `v22.16.x` or higher, you're on the supported Node 22 LTS path, but we still recommend upgrading to Node 24 when convenient. If Node isn't installed or the version is too old, pick an install method below.
## Install Node

View File

@@ -15,7 +15,7 @@ Native Linux companion apps are planned. Contributions are welcome if you want t
## Beginner quick path (VPS)
1. Install Node 24 (recommended; Node 22 LTS, currently `22.14+`, still works for compatibility)
1. Install Node 24 (recommended; Node 22 LTS, currently `22.16+`, still works for compatibility)
2. `npm i -g openclaw@latest`
3. `openclaw onboard --install-daemon`
4. From your laptop: `ssh -N -L 18789:127.0.0.1:18789 <user>@<host>`

View File

@@ -16,7 +16,7 @@ running (or attaches to an existing local Gateway if one is already running).
## Install the CLI (required for local mode)
Node 24 is the default runtime on the Mac. Node 22 LTS, currently `22.14+`, still works for compatibility. Then install `openclaw` globally:
Node 24 is the default runtime on the Mac. Node 22 LTS, currently `22.16+`, still works for compatibility. Then install `openclaw` globally:
```bash
npm install -g openclaw@<version>

View File

@@ -14,7 +14,7 @@ This guide covers the necessary steps to build and run the OpenClaw macOS applic
Before building the app, ensure you have the following installed:
1. **Xcode 26.2+**: Required for Swift development.
2. **Node.js 24 & pnpm**: Recommended for the gateway, CLI, and packaging scripts. Node 22 LTS, currently `22.14+`, remains supported for compatibility.
2. **Node.js 24 & pnpm**: Recommended for the gateway, CLI, and packaging scripts. Node 22 LTS, currently `22.16+`, remains supported for compatibility.
## 1. Install Dependencies

View File

@@ -14,7 +14,7 @@ This app is usually built from [`scripts/package-mac-app.sh`](https://github.com
- calls [`scripts/codesign-mac-app.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/codesign-mac-app.sh) to sign the main binary and app bundle so macOS treats each rebuild as the same signed bundle and keeps TCC permissions (notifications, accessibility, screen recording, mic, speech). For stable permissions, use a real signing identity; ad-hoc is opt-in and fragile (see [macOS permissions](/platforms/mac/permissions)).
- uses `CODESIGN_TIMESTAMP=auto` by default; it enables trusted timestamps for Developer ID signatures. Set `CODESIGN_TIMESTAMP=off` to skip timestamping (offline debug builds).
- inject build metadata into Info.plist: `OpenClawBuildTimestamp` (UTC) and `OpenClawGitCommit` (short hash) so the About pane can show build, git, and debug/release channel.
- **Packaging defaults to Node 24**: the script runs TS builds and the Control UI build. Node 22 LTS, currently `22.14+`, remains supported for compatibility.
- **Packaging defaults to Node 24**: the script runs TS builds and the Control UI build. Node 22 LTS, currently `22.16+`, remains supported for compatibility.
- reads `SIGN_IDENTITY` from the environment. Add `export SIGN_IDENTITY="Apple Development: Your Name (TEAMID)"` (or your Developer ID Application cert) to your shell rc to always sign with your cert. Ad-hoc signing requires explicit opt-in via `ALLOW_ADHOC_SIGNING=1` or `SIGN_IDENTITY="-"` (not recommended for permission testing).
- runs a Team ID audit after signing and fails if any Mach-O inside the app bundle is signed by a different Team ID. Set `SKIP_TEAM_ID_CHECK=1` to bypass.

View File

@@ -8,12 +8,16 @@ title: "MiniMax"
# MiniMax
OpenClaw's MiniMax provider defaults to **MiniMax M2.7**.
OpenClaw's MiniMax provider defaults to **MiniMax M2.7** and keeps
**MiniMax M2.5** in the catalog for compatibility.
## Model lineup
- `MiniMax-M2.7`: default hosted text model.
- `MiniMax-M2.7-highspeed`: faster M2.7 text tier.
- `MiniMax-M2.5`: previous text model, still available in the MiniMax catalog.
- `MiniMax-M2.5-highspeed`: faster M2.5 text tier.
- `MiniMax-VL-01`: vision model for text + image inputs.
## Choose a setup
@@ -76,6 +80,24 @@ Configure via CLI:
contextWindow: 200000,
maxTokens: 8192,
},
{
id: "MiniMax-M2.5",
name: "MiniMax M2.5",
reasoning: true,
input: ["text"],
cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.12 },
contextWindow: 200000,
maxTokens: 8192,
},
{
id: "MiniMax-M2.5-highspeed",
name: "MiniMax M2.5 Highspeed",
reasoning: true,
input: ["text"],
cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.12 },
contextWindow: 200000,
maxTokens: 8192,
},
],
},
},
@@ -106,6 +128,46 @@ Example below uses Opus as a concrete primary; swap to your preferred latest-gen
}
```
### Optional: Local via LM Studio (manual)
**Best for:** local inference with LM Studio.
We have seen strong results with MiniMax M2.5 on powerful hardware (e.g. a
desktop/server) using LM Studio's local server.
Configure manually via `openclaw.json`:
```json5
{
agents: {
defaults: {
model: { primary: "lmstudio/minimax-m2.5-gs32" },
models: { "lmstudio/minimax-m2.5-gs32": { alias: "Minimax" } },
},
},
models: {
mode: "merge",
providers: {
lmstudio: {
baseUrl: "http://127.0.0.1:1234/v1",
apiKey: "lmstudio",
api: "openai-responses",
models: [
{
id: "minimax-m2.5-gs32",
name: "MiniMax M2.5 GS32",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 196608,
maxTokens: 8192,
},
],
},
},
},
}
```
## Configure via `openclaw configure`
Use the interactive config wizard to set MiniMax without editing JSON:
@@ -128,7 +190,7 @@ Use the interactive config wizard to set MiniMax without editing JSON:
- Model refs are `minimax/<model>`.
- Default text model: `MiniMax-M2.7`.
- Alternate text model: `MiniMax-M2.7-highspeed`.
- Alternate text models: `MiniMax-M2.7-highspeed`, `MiniMax-M2.5`, `MiniMax-M2.5-highspeed`.
- Coding Plan usage API: `https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains` (requires a coding plan key).
- Update pricing values in `models.json` if you need exact cost tracking.
- Referral link for MiniMax Coding Plan (10% off): [https://platform.minimax.io/subscribe/coding-plan?code=DbXJTRClnb&source=link](https://platform.minimax.io/subscribe/coding-plan?code=DbXJTRClnb&source=link)
@@ -152,6 +214,8 @@ Make sure the model id is **casesensitive**:
- `minimax/MiniMax-M2.7`
- `minimax/MiniMax-M2.7-highspeed`
- `minimax/MiniMax-M2.5`
- `minimax/MiniMax-M2.5-highspeed`
Then recheck with:

View File

@@ -26,7 +26,6 @@ title: "Tests"
- Gateway integration: opt-in via `OPENCLAW_TEST_INCLUDE_GATEWAY=1 pnpm test` or `pnpm test:gateway`.
- `pnpm test:e2e`: Runs gateway end-to-end smoke tests (multi-instance WS/HTTP/node pairing). Defaults to `forks` + adaptive workers in `vitest.e2e.config.ts`; tune with `OPENCLAW_E2E_WORKERS=<n>` and set `OPENCLAW_E2E_VERBOSE=1` for verbose logs.
- `pnpm test:live`: Runs provider live tests (minimax/zai). Requires API keys and `LIVE=1` (or provider-specific `*_LIVE_TEST=1`) to unskip.
- `pnpm test:docker:openwebui`: Starts Dockerized OpenClaw + Open WebUI, signs in through Open WebUI, checks `/api/models`, then runs a real proxied chat through `/api/chat/completions`. Requires a usable live model key (for example OpenAI in `~/.profile`), pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites.
## Local PR gate

View File

@@ -14,7 +14,7 @@ and a working chat session.
## What you need
- **Node.js** — Node 24 recommended (Node 22.14+ also supported)
- **Node.js** — Node 24 recommended (Node 22.16+ also supported)
- **An API key** from a model provider (Anthropic, OpenAI, Google, etc.) — onboarding will prompt you
<Tip>

View File

@@ -21,7 +21,7 @@ For onboarding details, see [Onboarding (CLI)](/start/wizard).
## Prereqs (from source)
- Node 24 recommended (Node 22 LTS, currently `22.14+`, still supported)
- Node 24 recommended (Node 22 LTS, currently `22.16+`, still supported)
- `pnpm`
- Docker (optional; only for containerized setup/e2e — see [Docker](/install/docker))

View File

@@ -0,0 +1,557 @@
# Plugin SDK Namespaces Plan
## TL;DR
OpenClaw should introduce a few clear SDK namespaces like `plugin`, `channel`,
and `provider`, instead of keeping so much of the public surface flat.
The safe way to do that is:
- add thin ESM facade entrypoints, not TypeScript `namespace`
- keep the root `openclaw/plugin-sdk` surface small
- replace flat registration methods on `OpenClawPluginApi` with namespace groups
- ship the cutover in one coordinated release instead of dragging old flat APIs
along
- forbid leaf modules from importing back through namespace facades
That gives plugin authors a cleaner SDK that feels closer to VS Code, without
turning the SDK into a giant barrel or creating circular import problems.
## Goal
Introduce public namespaces to the OpenClaw Plugin SDK so the surface feels
closer to the VS Code extension API, while keeping the implementation tight,
isolated, and resistant to circular imports.
This plan is about the public SDK shape. It is not a proposal to merge
everything into one giant barrel.
## Why This Is Worth Doing
Today the Plugin SDK has three visible problems:
- The public package export surface is large and mostly flat.
- `src/plugin-sdk/core.ts` and `src/plugin-sdk/index.ts` carry too many
unrelated meanings.
- `OpenClawPluginApi` is still a flat registration API even though
`api.runtime` already proves grouped namespaces work well.
The result is harder docs, harder discovery, and too many helper names that
look equally important even when they are not.
## Current Facts In The Repo
- Package exports are generated from a flat entrypoint list in
`src/plugin-sdk/entrypoints.ts` and `scripts/lib/plugin-sdk-entrypoints.json`.
- The root `openclaw/plugin-sdk` entry is intentionally tiny in
`src/plugin-sdk/index.ts`.
- `api.runtime` is already a successful namespace model. It groups behavior as
`agent`, `subagent`, `media`, `imageGeneration`, `webSearch`, `tools`,
`channel`, `events`, `logging`, `state`, `tts`, `mediaUnderstanding`, and
`modelAuth` in `src/plugins/runtime/index.ts`.
- The main plugin registration API is still flat in `OpenClawPluginApi` in
`src/plugins/types.ts`.
- The concrete API object is assembled in `src/plugins/registry.ts`, and a
second partial copy exists in `src/plugins/captured-registration.ts`.
Those facts suggest a path that is low-risk:
- keep leaf modules as the source of truth
- add namespace facades on top
- cut docs, examples, and templates over in the same release as the namespace
model
## Design Principles
### 1. Do Not Use TypeScript `namespace`
Use normal ESM modules and package exports.
The SDK already ships as package export subpaths. The namespace model should be
implemented as public facade modules, not TypeScript `namespace` syntax.
### 2. Keep The Root Tiny
Do not turn `openclaw/plugin-sdk` into a giant VS Code-style monolith.
The closest safe equivalent is:
- a tiny root for shared types and a few universal values
- a small number of explicit namespace entrypoints
- optional ergonomic aggregation only after the namespace surfaces settle
### 3. Namespace Facades Must Be Thin
Namespace entrypoints should contain no real business logic.
They should only:
- re-export stable leaves
- assemble small namespace objects
That keeps cycles and accidental coupling down.
### 4. Types Stay Direct And Easy To Import
Like VS Code, namespaces should mostly group behavior. Common types should stay
directly importable from the root or the owning domain surface.
Examples:
- `ChannelPlugin`
- `ProviderPlugin`
- `OpenClawPluginApi`
- `PluginRuntime`
### 5. Do Not Namespace Everything At Once
Only namespace areas that already have a clear public identity.
Phase 1 should focus on:
- `plugin`
- `channel`
- `provider`
`runtime` already has a good public namespace shape on `api.runtime` and should
not be reopened as a giant package-export family in the first pass.
## Proposed Public Model
### Namespace Entry Points
Canonical public entrypoints:
- `openclaw/plugin-sdk/plugin`
- `openclaw/plugin-sdk/channel`
- `openclaw/plugin-sdk/provider`
- `openclaw/plugin-sdk/runtime`
- `openclaw/plugin-sdk/testing`
What each should mean:
- `plugin`
- plugin entry helpers
- shared plugin definition helpers
- plugin-facing config schema helpers that are truly universal
- `channel`
- channel entry helpers
- chat-channel builders
- stable channel-facing contracts and helpers
- `provider`
- provider entry helpers
- auth, catalog, models, onboard, stream, usage, and provider registration helpers
- `runtime`
- the existing `api.runtime` story and runtime-related public helpers that are
truly stable
- `testing`
- plugin author testing helpers
### Nested Leaves
Under those namespaces, the long-term canonical leaves should become nested:
- `openclaw/plugin-sdk/channel/setup`
- `openclaw/plugin-sdk/channel/pairing`
- `openclaw/plugin-sdk/channel/reply-pipeline`
- `openclaw/plugin-sdk/channel/contract`
- `openclaw/plugin-sdk/channel/targets`
- `openclaw/plugin-sdk/channel/actions`
- `openclaw/plugin-sdk/channel/inbound`
- `openclaw/plugin-sdk/channel/lifecycle`
- `openclaw/plugin-sdk/channel/policy`
- `openclaw/plugin-sdk/channel/feedback`
- `openclaw/plugin-sdk/channel/config-schema`
- `openclaw/plugin-sdk/channel/config-helpers`
- `openclaw/plugin-sdk/provider/auth`
- `openclaw/plugin-sdk/provider/catalog`
- `openclaw/plugin-sdk/provider/models`
- `openclaw/plugin-sdk/provider/onboard`
- `openclaw/plugin-sdk/provider/stream`
- `openclaw/plugin-sdk/provider/usage`
- `openclaw/plugin-sdk/provider/web-search`
Not every current flat subpath needs a namespaced replacement. The goal is to
promote the stable public domains, not to preserve every current export forever.
## What Happens To `core`
`core` is overloaded today. In a namespace model it should shrink, not grow.
Target split:
- plugin-wide entry helpers move toward `plugin`
- channel builders and channel-oriented shared helpers move toward `channel`
- `core` stops being a first-class public destination and shrinks to the
smallest possible remaining shared surface
Rule: no new public API should be added to `core` once namespace entrypoints
exist.
## Proposed `OpenClawPluginApi` Shape
Keep context fields flat:
- `id`
- `name`
- `version`
- `description`
- `source`
- `rootDir`
- `registrationMode`
- `config`
- `pluginConfig`
- `runtime`
- `logger`
- `resolvePath`
Move registration behavior behind namespaces:
| Current flat method | Proposed namespace location |
| ------------------------------------ | ----------------------------------------- |
| `registerTool` | `api.tool.register` |
| `registerHook` | `api.hook.register` |
| `on` | `api.hook.on` |
| `registerHttpRoute` | `api.http.registerRoute` |
| `registerChannel` | `api.channel.register` |
| `registerProvider` | `api.provider.register` |
| `registerSpeechProvider` | `api.provider.registerSpeech` |
| `registerMediaUnderstandingProvider` | `api.provider.registerMediaUnderstanding` |
| `registerImageGenerationProvider` | `api.provider.registerImageGeneration` |
| `registerWebSearchProvider` | `api.provider.registerWebSearch` |
| `registerGatewayMethod` | `api.gateway.registerMethod` |
| `registerCli` | `api.cli.register` |
| `registerService` | `api.service.register` |
| `registerInteractiveHandler` | `api.interactive.register` |
| `registerCommand` | `api.command.register` |
| `registerContextEngine` | `api.contextEngine.register` |
| `registerMemoryPromptSection` | `api.memory.registerPromptSection` |
The cutover should replace the flat methods in one coordinated change.
That gives plugin authors a clearer public shape and avoids carrying two public
registration models at the same time.
## Example Public Usage
Proposed style:
```ts
import { definePluginEntry } from "openclaw/plugin-sdk/plugin";
import { channel } from "openclaw/plugin-sdk/channel";
import { provider } from "openclaw/plugin-sdk/provider";
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";
const chatPlugin: ChannelPlugin = channel.createChatPlugin({
id: "demo",
/* ... */
});
export default definePluginEntry({
id: "demo",
register(api: OpenClawPluginApi) {
api.channel.register(chatPlugin);
api.command.register({
name: "status",
description: "Show plugin status",
run: async () => ({ text: "ok" }),
});
},
});
```
This is close to the VS Code mental model:
- grouped behavior
- direct types
- obvious public areas
without requiring a single monolithic root import.
## Optional Ergonomic Surface
If the project later wants the closest possible VS Code feel, add a dedicated
opt-in facade such as `openclaw/plugin-sdk/sdk`.
That facade can assemble:
- `plugin`
- `channel`
- `provider`
- `runtime`
- `testing`
It should not be phase 1.
Why:
- it is the highest-risk barrel from a cycle and weight perspective
- it is easier to add once the namespace surfaces already exist
- it preserves the root `openclaw/plugin-sdk` entry as a small type-oriented
surface
## Internal Implementation Rules
These rules are the important part. Without them, namespaces will rot into
barrels and cycles.
### Rule 1: Namespace Facades Are One-Way
Namespace entrypoints may import leaf modules.
Leaf modules may not import their namespace entrypoint.
Examples:
- allowed: `src/plugin-sdk/channel.ts` importing `./channel-setup.ts`
- forbidden: `src/plugin-sdk/channel-setup.ts` importing `./channel.ts`
### Rule 1A: Allowed Dependency Directions Must Be Explicit
The allowed directions should be:
- namespace facade -> leaves in the same namespace
- leaf -> local implementation helpers
- leaf -> dedicated shared internal leaf
- leaf -> another leaf in the same namespace only by direct relative import,
never through the namespace facade
The forbidden directions should be:
- leaf -> its own namespace facade
- leaf -> another namespace facade
- namespace facade -> another namespace facade
- channel leaf -> provider leaf, or provider leaf -> channel leaf, unless the
dependency is first extracted into a shared internal leaf
Short version:
- facades point downward
- leaves never point back upward
- cross-namespace sharing must go sideways through a shared internal leaf, not
directly through another public namespace
### Rule 1B: If Two Namespaces Need Each Other, Extract A Shared Leaf
If `channel` and `provider` start needing each other directly, that is the sign
that the seam is wrong.
Do not allow:
- `src/plugin-sdk/channel/*` importing from `src/plugin-sdk/provider/*`
- `src/plugin-sdk/provider/*` importing from `src/plugin-sdk/channel/*`
Instead:
- extract the shared logic into a dedicated internal leaf
- let both sides depend on that leaf
- keep the public namespaces separate
This is the main cycle-prevention rule. Shared logic moves to a lower layer
before it creates a back-edge.
### Rule 2: No Public-Specifier Self-Imports Inside The SDK
Files inside `src/plugin-sdk/**` should never import from
`openclaw/plugin-sdk/...`.
They should import local source files directly.
### Rule 3: Shared Code Lives In Shared Leaves
If `channel` and `provider` need the same implementation detail, move that code
to a shared leaf instead of importing one namespace from the other.
Good shared homes:
- a dedicated internal shared leaf
- a very small shared core leaf only if it has a precise, stable reason to
exist
- existing domain-neutral helpers
Bad pattern:
- `provider/*` importing from `channel/index`
- `channel/*` importing from `provider/index`
### Rule 4: Assemble The API Surface Once
`OpenClawPluginApi` should be built by one canonical factory.
`src/plugins/registry.ts` and `src/plugins/captured-registration.ts` should stop
hand-building separate versions of the API object.
That factory can expose:
- the namespaced shape only
from the same underlying implementation.
### Rule 5: Namespace Entry Files Stay Small
Namespace facades should stay close to pure exports. If a namespace file grows
real orchestration logic, split that logic back into leaf modules.
### Dependency Shape
The intended import graph is:
```text
public facade
-> same-namespace leaves
-> local helpers
-> shared internal leaves
```
Not this:
```text
channel facade -> provider facade
channel leaf -> channel facade
provider leaf -> channel leaf
```
Concrete examples:
- allowed: `src/plugin-sdk/channel.ts` -> `./channel/setup.ts`
- allowed: `src/plugin-sdk/channel/setup.ts` -> `./_internal/channel-shared.ts`
- allowed: `src/plugin-sdk/provider/auth.ts` -> `../_internal/provider-shared.ts`
- forbidden: `src/plugin-sdk/channel/setup.ts` -> `./channel.ts`
- forbidden: `src/plugin-sdk/channel/setup.ts` -> `../provider/index.ts`
- forbidden: `src/plugin-sdk/channel.ts` -> `./provider.ts`
## Migration Strategy
This should be a cutover, not a long overlap period.
That means:
- one coordinated release
- one migration guide
- one docs/templates/test update
- one public SDK shape after the release
## Phase 1: Extract The Canonical API Builder
Do this first, before changing the public surface.
Why:
- it removes duplicated API assembly
- it gives one place to switch the public shape
- it reduces cutover risk
Implementation:
- extract one canonical API builder from `src/plugins/registry.ts` and
`src/plugins/captured-registration.ts`
- make that builder assemble the new namespaced registration API
## Phase 2: Add Canonical Namespace Entrypoints
Add:
- `plugin`
- `channel`
- `provider`
as thin public facades over existing flat leaves.
Implementation detail:
- the first pass can re-export current flat files
- do not move source layout and package exports in the same commit if it can be
avoided
Examples:
- `src/plugin-sdk/channel/setup.ts` can initially re-export from
`../channel-setup.js`
- `src/plugin-sdk/provider/auth.ts` can initially re-export from
`../provider-auth.js`
This lets the public namespace story land before the internal source move,
without forcing all implementation files to move in the same commit.
## Phase 3: Cut Public API, Docs, And Templates Together
In the same release:
- docs prefer namespaced entrypoints
- templates prefer namespaced imports
- tests and examples switch to the namespaced shape
- `OpenClawPluginApi` changes to the namespaced registration model
- flat registration methods are removed instead of carried as aliases
## Phase 4: Remove The Old Public Story
After the cutover release lands:
- stop documenting superseded flat leaves as public API
- keep only the namespace model in author-facing docs
- remove any leftover flat registration surface that survived only as
transitional scaffolding during implementation
## What Should Not Be Namespaced In Phase 1
To keep the refactor tight, do not force these into the first milestone:
- every `*-runtime` helper subpath
- extension-branded public subpaths
- one-off utilities that do not yet have a stable domain home
- the root `openclaw/plugin-sdk` barrel
If a subpath is only public because history leaked it, namespace work should not
promote it.
## Guardrails And Validation
The namespace rollout should ship with explicit checks.
### Existing Checks To Reuse
- `src/plugin-sdk/subpaths.test.ts`
- `src/plugin-sdk/runtime-api-guardrails.test.ts`
- `pnpm build` for `[CIRCULAR_REEXPORT]` warnings
- `pnpm plugin-sdk:api:check`
### New Checks To Add
- namespace facade files may only re-export or compose approved leaves
- leaf files under a namespace may not import their parent `index` facade
- leaf files under one namespace may not import another namespace facade
- cross-namespace leaf imports should fail unless the target is an approved
shared internal leaf
- namespace facades may not import other namespace facades
- no new API should be added to `core` once namespace facades exist
- `OpenClawPluginApi` must not expose both flat and namespaced registration
methods after cutover
## Recommended End State
The elegant end state is:
- a tiny root
- a few first-class namespaces
- direct types
- a grouped `api` registration surface
- stable leaves under each namespace
- no reverse imports from leaves back into namespace facades
That gives OpenClaw a VS Code-like feel where the public SDK has clear domains,
but still respects the repo's existing build, lazy-loading, and package-boundary
constraints.
## Short Recommendation
If this work starts soon, the first implementation step should be:
1. extract one canonical `OpenClawPluginApi` builder
2. switch that builder to the namespaced registration shape
3. add `plugin`, `channel`, and `provider` facade entrypoints
4. cut docs, templates, and examples over in the same release
5. remove the old flat registration story instead of maintaining dual public APIs
That sequence keeps the refactor elegant and minimizes the chance that
namespaces become another layer of accidental coupling.

View File

@@ -1,31 +1,6 @@
import { ChannelType } from "discord-api-types/v10";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig, loadConfig } from "../../../../src/config/config.js";
const { logVerboseMock } = vi.hoisted(() => ({
logVerboseMock: vi.fn(),
}));
const { loggerWarnMock } = vi.hoisted(() => ({
loggerWarnMock: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/runtime-env", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/runtime-env")>(
"openclaw/plugin-sdk/runtime-env",
);
return {
...actual,
createSubsystemLogger: () => ({
child: vi.fn(),
info: vi.fn(),
error: vi.fn(),
warn: loggerWarnMock,
debug: vi.fn(),
}),
logVerbose: logVerboseMock,
};
});
let listNativeCommandSpecs: typeof import("../../../../src/auto-reply/commands-registry.js").listNativeCommandSpecs;
let createDiscordNativeCommand: typeof import("./native-command.js").createDiscordNativeCommand;
let createNoopThreadBindingManager: typeof import("./thread-bindings.js").createNoopThreadBindingManager;
@@ -102,11 +77,6 @@ describe("createDiscordNativeCommand option wiring", () => {
({ createNoopThreadBindingManager } = await import("./thread-bindings.js"));
});
beforeEach(() => {
logVerboseMock.mockReset();
loggerWarnMock.mockReset();
});
it("uses autocomplete for /acp action so inline action values are accepted", async () => {
const command = createNativeCommand("acp");
const action = requireOption(command, "action");
@@ -198,42 +168,4 @@ describe("createDiscordNativeCommand option wiring", () => {
expect(respond).toHaveBeenCalledWith([]);
});
it("truncates Discord command and option descriptions to Discord's limit", () => {
const longDescription = "x".repeat(140);
const cfg = {} as ReturnType<typeof loadConfig>;
const discordConfig = {} as NonNullable<OpenClawConfig["channels"]>["discord"];
const command = createDiscordNativeCommand({
command: {
name: "longdesc",
description: longDescription,
acceptsArgs: true,
args: [
{
name: "input",
description: longDescription,
type: "string",
required: false,
},
],
},
cfg,
discordConfig,
accountId: "default",
sessionPrefix: "discord:slash",
ephemeralDefault: true,
threadBindings: createNoopThreadBindingManager("default"),
});
expect(command.description).toHaveLength(100);
expect(command.description).toBe("x".repeat(100));
expect(requireOption(command, "input").description).toHaveLength(100);
expect(requireOption(command, "input").description).toBe("x".repeat(100));
expect(loggerWarnMock).toHaveBeenCalledWith(
expect.stringContaining("truncating native command description (command:longdesc)"),
);
expect(loggerWarnMock).toHaveBeenCalledWith(
expect.stringContaining("truncating native command description (command:longdesc arg:input)"),
);
});
});

View File

@@ -81,27 +81,6 @@ import { resolveDiscordThreadParentInfo } from "./threading.js";
type DiscordConfig = NonNullable<OpenClawConfig["channels"]>["discord"];
const log = createSubsystemLogger("discord/native-command");
// Discord application command and option descriptions are limited to 1-100 chars.
// https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure
const DISCORD_COMMAND_DESCRIPTION_MAX = 100;
function truncateDiscordCommandDescription(params: { value: string; label: string }): string {
const { value, label } = params;
if (value.length <= DISCORD_COMMAND_DESCRIPTION_MAX) {
return value;
}
log.warn(
`discord: truncating native command description (${label}) from ${value.length} to ${DISCORD_COMMAND_DESCRIPTION_MAX}: ${JSON.stringify(value)}`,
);
return value.slice(0, DISCORD_COMMAND_DESCRIPTION_MAX);
}
function resolveDiscordCommandLogLabel(command: ChatCommandDefinition): string {
if (typeof command.nativeName === "string" && command.nativeName.trim().length > 0) {
return command.nativeName;
}
return command.key;
}
function resolveDiscordNativeCommandAllowlistAccess(params: {
cfg: OpenClawConfig;
@@ -145,7 +124,6 @@ function buildDiscordCommandOptions(params: {
) => Promise<{ provider?: string; model?: string } | null>;
}): CommandOptions | undefined {
const { command, cfg, authorizeChoiceContext, resolveChoiceContext } = params;
const commandLabel = resolveDiscordCommandLogLabel(command);
const args = command.args;
if (!args || args.length === 0) {
return undefined;
@@ -155,10 +133,7 @@ function buildDiscordCommandOptions(params: {
if (arg.type === "number") {
return {
name: arg.name,
description: truncateDiscordCommandDescription({
value: arg.description,
label: `command:${commandLabel} arg:${arg.name}`,
}),
description: arg.description,
type: ApplicationCommandOptionType.Number,
required,
};
@@ -166,10 +141,7 @@ function buildDiscordCommandOptions(params: {
if (arg.type === "boolean") {
return {
name: arg.name,
description: truncateDiscordCommandDescription({
value: arg.description,
label: `command:${commandLabel} arg:${arg.name}`,
}),
description: arg.description,
type: ApplicationCommandOptionType.Boolean,
required,
};
@@ -220,10 +192,7 @@ function buildDiscordCommandOptions(params: {
: undefined;
return {
name: arg.name,
description: truncateDiscordCommandDescription({
value: arg.description,
label: `command:${commandLabel} arg:${arg.name}`,
}),
description: arg.description,
type: ApplicationCommandOptionType.String,
required,
choices,
@@ -548,10 +517,7 @@ export function createDiscordNativeCommand(params: {
return new (class extends Command {
name = command.name;
description = truncateDiscordCommandDescription({
value: command.description,
label: `command:${command.name}`,
});
description = command.description;
defer = true;
ephemeral = ephemeralDefault;
options = options;

View File

@@ -37,7 +37,6 @@ const {
} = getProviderMonitorTestMocks();
let monitorDiscordProvider: typeof import("./provider.js").monitorDiscordProvider;
let providerTesting: typeof import("./provider.js").__testing;
function createConfigWithDiscordAccount(overrides: Record<string, unknown> = {}): OpenClawConfig {
return {
@@ -130,7 +129,7 @@ describe("monitorDiscordProvider", () => {
vi.doMock("../token.js", () => ({
normalizeDiscordToken: (value?: string) => value,
}));
({ monitorDiscordProvider, __testing: providerTesting } = await import("./provider.js"));
({ monitorDiscordProvider } = await import("./provider.js"));
});
beforeEach(() => {
@@ -553,57 +552,6 @@ describe("monitorDiscordProvider", () => {
);
});
it("formats rejected Discord deploy entries with command details", () => {
const details = providerTesting.formatDiscordDeployErrorDetails({
status: 400,
discordCode: 50035,
rawBody: {
code: 50035,
message: "Invalid Form Body",
errors: {
63: {
description: {
_errors: [{ code: "BASE_TYPE_MAX_LENGTH", message: "Must be 100 or fewer." }],
},
},
65: {
description: {
_errors: [{ code: "BASE_TYPE_MAX_LENGTH", message: "Must be 100 or fewer." }],
},
},
66: {
description: {
_errors: [{ code: "BASE_TYPE_MAX_LENGTH", message: "Must be 100 or fewer." }],
},
},
67: {
description: {
_errors: [{ code: "BASE_TYPE_MAX_LENGTH", message: "Must be 100 or fewer." }],
},
},
},
},
deployRequestBody: Array.from({ length: 68 }, (_entry, index) => ({
name: `command-${index}`,
description: `description-${index}`,
})),
});
expect(details).toContain("status=400");
expect(details).toContain("code=50035");
expect(details).toContain("rejected=");
expect(details).toContain(
'#63 fields=description name=command-63 description="description-63"',
);
expect(details).toContain(
'#65 fields=description name=command-65 description="description-65"',
);
expect(details).toContain(
'#66 fields=description name=command-66 description="description-66"',
);
expect(details).not.toContain("command-67");
});
it("configures Carbon native deploy by default", async () => {
await monitorDiscordProvider({
config: baseConfig(),

View File

@@ -312,10 +312,8 @@ async function deployDiscordCommands(params: {
}
return result;
} catch (err) {
attachDiscordDeployRequestBody(err, body);
const details = formatDiscordDeployErrorDetails(err);
params.runtime.error?.(
`discord startup [${accountId}] deploy-rest:put:error ${Math.max(0, Date.now() - startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt} error=${formatErrorMessage(err)}${details}`,
`discord startup [${accountId}] deploy-rest:put:error ${Math.max(0, Date.now() - startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt} error=${formatErrorMessage(err)}`,
);
throw err;
}
@@ -402,108 +400,13 @@ function logDiscordStartupPhase(params: {
`discord startup [${params.accountId}] ${params.phase} ${elapsedMs}ms${suffix ? ` ${suffix}` : ""}`,
);
}
const DISCORD_DEPLOY_REJECTED_ENTRY_LIMIT = 3;
type DiscordDeployErrorLike = {
status?: unknown;
discordCode?: unknown;
rawBody?: unknown;
deployRequestBody?: unknown;
};
function attachDiscordDeployRequestBody(err: unknown, body: unknown) {
if (!err || typeof err !== "object" || body === undefined) {
return;
}
const deployErr = err as DiscordDeployErrorLike;
if (deployErr.deployRequestBody === undefined) {
deployErr.deployRequestBody = body;
}
}
function stringifyDiscordDeployField(value: unknown): string {
if (typeof value === "string") {
return JSON.stringify(value);
}
try {
return JSON.stringify(value);
} catch {
return inspect(value, { depth: 2, breakLength: 120 });
}
}
function readDiscordDeployRejectedFields(value: unknown): string[] {
if (Array.isArray(value)) {
return value.filter((entry): entry is string => typeof entry === "string").slice(0, 6);
}
if (!value || typeof value !== "object") {
return [];
}
return Object.keys(value).slice(0, 6);
}
function resolveDiscordRejectedDeployEntriesSource(
rawBody: unknown,
): Record<string, unknown> | null {
if (!rawBody || typeof rawBody !== "object") {
return null;
}
const payload = rawBody as { errors?: unknown };
const errors = payload.errors && typeof payload.errors === "object" ? payload.errors : undefined;
const source = errors ?? rawBody;
return source && typeof source === "object" ? (source as Record<string, unknown>) : null;
}
function formatDiscordRejectedDeployEntries(params: {
rawBody: unknown;
requestBody: unknown;
}): string[] {
const requestBody = Array.isArray(params.requestBody) ? params.requestBody : null;
const rejectedEntriesSource = resolveDiscordRejectedDeployEntriesSource(params.rawBody);
if (!rejectedEntriesSource || !requestBody || requestBody.length === 0) {
return [];
}
const rawEntries = Object.entries(rejectedEntriesSource).filter(([key]) => /^\d+$/.test(key));
return rawEntries.slice(0, DISCORD_DEPLOY_REJECTED_ENTRY_LIMIT).flatMap(([key, value]) => {
const index = Number.parseInt(key, 10);
if (!Number.isFinite(index) || index < 0 || index >= requestBody.length) {
return [];
}
const command = requestBody[index];
if (!command || typeof command !== "object") {
return [`#${index} fields=${readDiscordDeployRejectedFields(value).join("|") || "unknown"}`];
}
const payload = command as {
name?: unknown;
description?: unknown;
options?: unknown;
};
const parts = [
`#${index}`,
`fields=${readDiscordDeployRejectedFields(value).join("|") || "unknown"}`,
];
if (typeof payload.name === "string" && payload.name.trim().length > 0) {
parts.push(`name=${payload.name}`);
}
if (payload.description !== undefined) {
parts.push(`description=${stringifyDiscordDeployField(payload.description)}`);
}
if (Array.isArray(payload.options) && payload.options.length > 0) {
parts.push(`options=${payload.options.length}`);
}
return [parts.join(" ")];
});
}
function formatDiscordDeployErrorDetails(err: unknown): string {
if (!err || typeof err !== "object") {
return "";
}
const status = (err as DiscordDeployErrorLike).status;
const discordCode = (err as DiscordDeployErrorLike).discordCode;
const rawBody = (err as DiscordDeployErrorLike).rawBody;
const requestBody = (err as DiscordDeployErrorLike).deployRequestBody;
const status = (err as { status?: unknown }).status;
const discordCode = (err as { discordCode?: unknown }).discordCode;
const rawBody = (err as { rawBody?: unknown }).rawBody;
const details: string[] = [];
if (typeof status === "number") {
details.push(`status=${status}`);
@@ -525,10 +428,6 @@ function formatDiscordDeployErrorDetails(err: unknown): string {
details.push(`body=${trimmed}`);
}
}
const rejectedEntries = formatDiscordRejectedDeployEntries({ rawBody, requestBody });
if (rejectedEntries.length > 0) {
details.push(`rejected=${rejectedEntries.join("; ")}`);
}
return details.length > 0 ? ` (${details.join(", ")})` : "";
}
@@ -1159,5 +1058,4 @@ export const __testing = {
resolveDefaultGroupPolicy,
resolveDiscordRestFetch,
resolveThreadBindingsEnabled: resolveThreadBindingsEnabledForTesting,
formatDiscordDeployErrorDetails,
};

View File

@@ -733,77 +733,6 @@ describe("handleFeishuMessage command authorization", () => {
);
});
it("uses message create_time as Timestamp instead of Date.now()", async () => {
mockShouldComputeCommandAuthorized.mockReturnValue(false);
const cfg: ClawdbotConfig = {
channels: {
feishu: {
dmPolicy: "open",
},
},
} as ClawdbotConfig;
const event: FeishuMessageEvent = {
sender: {
sender_id: {
open_id: "ou-attacker",
},
},
message: {
message_id: "msg-create-time",
chat_id: "oc-dm",
chat_type: "p2p",
message_type: "text",
content: JSON.stringify({ text: "delete this" }),
create_time: "1700000000000",
},
};
await dispatchMessage({ cfg, event });
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
expect.objectContaining({
Timestamp: 1700000000000,
}),
);
});
it("falls back to Date.now() when create_time is absent", async () => {
mockShouldComputeCommandAuthorized.mockReturnValue(false);
const cfg: ClawdbotConfig = {
channels: {
feishu: {
dmPolicy: "open",
},
},
} as ClawdbotConfig;
const event: FeishuMessageEvent = {
sender: {
sender_id: {
open_id: "ou-attacker",
},
},
message: {
message_id: "msg-no-create-time",
chat_id: "oc-dm",
chat_type: "p2p",
message_type: "text",
content: JSON.stringify({ text: "hello" }),
},
};
const before = Date.now();
await dispatchMessage({ cfg, event });
const after = Date.now();
const call = mockFinalizeInboundContext.mock.calls[0]?.[0] as { Timestamp: number };
expect(call.Timestamp).toBeGreaterThanOrEqual(before);
expect(call.Timestamp).toBeLessThanOrEqual(after);
});
it("replies pairing challenge to DM chat_id instead of user:sender id", async () => {
const cfg: ClawdbotConfig = {
channels: {

View File

@@ -357,14 +357,6 @@ export async function handleFeishuMessage(params: {
? [...new Set(rawBroadcastAgents.map((id) => normalizeAgentId(id)))]
: null;
// Parse message create_time early so every downstream consumer (pending
// history, inbound payload, etc.) uses the original authoring timestamp
// instead of the delivery/processing time. Feishu uses a millisecond
// epoch string; fall back to Date.now() only when the field is absent.
const messageCreateTimeMs = event.message.create_time
? parseInt(event.message.create_time, 10)
: Date.now();
let requireMention = false; // DMs never require mention; groups may override below
if (isGroup) {
if (groupConfig?.enabled === false) {
@@ -442,7 +434,7 @@ export async function handleFeishuMessage(params: {
entry: {
sender: ctx.senderOpenId,
body: `${ctx.senderName ?? ctx.senderOpenId}: ${ctx.content}`,
timestamp: messageCreateTimeMs,
timestamp: Date.now(),
messageId: ctx.messageId,
},
});
@@ -927,7 +919,7 @@ export async function handleFeishuMessage(params: {
// Only use rootId (om_* message anchor) — threadId (omt_*) is a container
// ID and would produce invalid reply targets downstream.
MessageThreadId: ctx.rootId && isTopicSessionForThread ? ctx.rootId : undefined,
Timestamp: messageCreateTimeMs,
Timestamp: Date.now(),
WasMentioned: wasMentioned,
CommandAuthorized: commandAuthorized,
OriginatingChannel: "feishu" as const,
@@ -937,6 +929,10 @@ export async function handleFeishuMessage(params: {
});
};
// Parse message create_time (Feishu uses millisecond epoch string).
const messageCreateTimeMs = event.message.create_time
? parseInt(event.message.create_time, 10)
: undefined;
// Determine reply target based on group session mode:
// - Topic-mode groups (group_topic / group_topic_sender): reply to the topic
// root so the bot stays in the same thread.

View File

@@ -1,8 +1,10 @@
import { promises as fs } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const createFeishuClientMock = vi.hoisted(() => vi.fn());
const fetchRemoteMediaMock = vi.hoisted(() => vi.fn());
const loadWebMediaMock = vi.hoisted(() => vi.fn());
const convertMock = vi.hoisted(() => vi.fn());
const documentCreateMock = vi.hoisted(() => vi.fn());
const blockListMock = vi.hoisted(() => vi.fn());
@@ -26,9 +28,6 @@ vi.mock("./runtime.js", () => ({
fetchRemoteMedia: fetchRemoteMediaMock,
},
},
media: {
loadWebMedia: loadWebMediaMock,
},
}),
}));
@@ -425,17 +424,15 @@ describe("feishu_doc image fetch hardening", () => {
},
});
loadWebMediaMock.mockResolvedValueOnce({
buffer: Buffer.from("hello from local file", "utf8"),
fileName: "test-local.txt",
});
const localPath = join(tmpdir(), `feishu-docx-upload-${Date.now()}.txt`);
await fs.writeFile(localPath, "hello from local file", "utf8");
const feishuDocTool = resolveFeishuDocTool();
const result = await feishuDocTool.execute("tool-call", {
action: "upload_file",
doc_token: "doc_1",
file_path: "/tmp/allowed/test-local.txt",
file_path: localPath,
filename: "test-local.txt",
});
@@ -443,13 +440,6 @@ describe("feishu_doc image fetch hardening", () => {
expect(result.details.file_token).toBe("token_1");
expect(result.details.file_name).toBe("test-local.txt");
// localRoots is not passed — loadWebMedia uses default roots (tmp, media,
// workspace, sandboxes) plus workspace-profile auto-discovery.
expect(loadWebMediaMock).toHaveBeenCalledWith(
expect.stringContaining("test-local.txt"),
expect.objectContaining({ optimizeImages: false }),
);
expect(driveUploadAllMock).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
@@ -459,6 +449,8 @@ describe("feishu_doc image fetch hardening", () => {
}),
}),
);
await fs.unlink(localPath);
});
it("returns an error when upload_file cannot list placeholder siblings", async () => {
@@ -474,64 +466,23 @@ describe("feishu_doc image fetch hardening", () => {
data: { items: [] },
});
loadWebMediaMock.mockResolvedValueOnce({
buffer: Buffer.from("hello from local file", "utf8"),
fileName: "test-local.txt",
});
const localPath = join(tmpdir(), `feishu-docx-upload-fail-${Date.now()}.txt`);
await fs.writeFile(localPath, "hello from local file", "utf8");
const feishuDocTool = resolveFeishuDocTool();
try {
const feishuDocTool = resolveFeishuDocTool();
const result = await feishuDocTool.execute("tool-call", {
action: "upload_file",
doc_token: "doc_1",
file_path: "/tmp/allowed/test-local.txt",
filename: "test-local.txt",
});
const result = await feishuDocTool.execute("tool-call", {
action: "upload_file",
doc_token: "doc_1",
file_path: localPath,
filename: "test-local.txt",
});
expect(result.details.error).toBe("list failed");
expect(driveUploadAllMock).not.toHaveBeenCalled();
});
it("rejects traversal paths in upload_file via loadWebMedia sandbox", async () => {
loadWebMediaMock.mockRejectedValueOnce(
new Error("Local media path is not under an allowed directory: /etc/passwd"),
);
const feishuDocTool = resolveFeishuDocTool();
const result = await feishuDocTool.execute("tool-call", {
action: "upload_file",
doc_token: "doc_1",
file_path: "/etc/passwd",
});
expect(result.details.error).toContain("not under an allowed directory");
expect(driveUploadAllMock).not.toHaveBeenCalled();
});
it("rejects traversal paths in upload_image via loadWebMedia sandbox", async () => {
blockChildrenCreateMock.mockResolvedValueOnce({
code: 0,
data: {
children: [{ block_type: 27, block_id: "img_block_1" }],
},
});
loadWebMediaMock.mockRejectedValueOnce(
new Error(
"Local media path is not under an allowed directory: /home/admin/.openclaw/openclaw.json",
),
);
const feishuDocTool = resolveFeishuDocTool();
const result = await feishuDocTool.execute("tool-call", {
action: "upload_image",
doc_token: "doc_1",
file_path: "/home/admin/.openclaw/openclaw.json",
});
expect(result.details.error).toContain("not under an allowed directory");
expect(driveUploadAllMock).not.toHaveBeenCalled();
expect(result.details.error).toBe("list failed");
expect(driveUploadAllMock).not.toHaveBeenCalled();
} finally {
await fs.unlink(localPath);
}
});
});

View File

@@ -1,6 +1,6 @@
import { existsSync } from "node:fs";
import { existsSync, promises as fs } from "node:fs";
import { homedir } from "node:os";
import { isAbsolute, resolve } from "node:path";
import { isAbsolute } from "node:path";
import { basename } from "node:path";
import type * as Lark from "@larksuiteoapi/node-sdk";
import { Type } from "@sinclair/typebox";
@@ -536,15 +536,11 @@ async function resolveUploadInput(
const absolutePath = isAbsolute(imageInput);
if (unambiguousPath || (absolutePath && existsSync(candidate))) {
// Use loadWebMedia to enforce localRoots sandbox (same as sendMediaFeishu).
// localRoots left undefined so loadWebMedia uses default roots (tmp, media,
// workspace, sandboxes) plus workspace-profile auto-discovery.
const resolvedPath = resolve(candidate);
const loaded = await getFeishuRuntime().media.loadWebMedia(resolvedPath, {
maxBytes,
optimizeImages: false,
});
return { buffer: loaded.buffer, fileName: explicitFileName ?? basename(candidate) };
const buffer = await fs.readFile(candidate);
if (buffer.length > maxBytes) {
throw new Error(`Local file exceeds limit: ${buffer.length} bytes > ${maxBytes} bytes`);
}
return { buffer, fileName: explicitFileName ?? basename(candidate) };
}
if (absolutePath && !existsSync(candidate)) {
@@ -598,15 +594,12 @@ async function resolveUploadInput(
};
}
// Use loadWebMedia to enforce localRoots sandbox (same as sendMediaFeishu).
// localRoots left undefined — see comment above.
const resolvedFilePath = resolve(filePath!);
const loaded = await getFeishuRuntime().media.loadWebMedia(resolvedFilePath, {
maxBytes,
optimizeImages: false,
});
const buffer = await fs.readFile(filePath!);
if (buffer.length > maxBytes) {
throw new Error(`Local file exceeds limit: ${buffer.length} bytes > ${maxBytes} bytes`);
}
return {
buffer: loaded.buffer,
buffer,
fileName: explicitFileName || basename(filePath!),
};
}

View File

@@ -1,122 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { botNames, botOpenIds, stopFeishuMonitorState, wsClients } from "./monitor.state.js";
import type { ResolvedFeishuAccount } from "./types.js";
const createFeishuWSClientMock = vi.hoisted(() => vi.fn());
vi.mock("./client.js", () => ({
createFeishuWSClient: createFeishuWSClientMock,
}));
import { monitorWebSocket } from "./monitor.transport.js";
type MockWsClient = {
start: ReturnType<typeof vi.fn>;
close: ReturnType<typeof vi.fn>;
};
function createAccount(accountId: string): ResolvedFeishuAccount {
return {
accountId,
enabled: true,
configured: true,
appId: `cli_${accountId}`,
appSecret: `secret_${accountId}`, // pragma: allowlist secret
domain: "feishu",
config: {
enabled: true,
connectionMode: "websocket",
},
} as ResolvedFeishuAccount;
}
function createWsClient(): MockWsClient {
return {
start: vi.fn(),
close: vi.fn(),
};
}
afterEach(() => {
stopFeishuMonitorState();
vi.clearAllMocks();
});
describe("feishu websocket cleanup", () => {
it("closes the websocket client when the monitor aborts", async () => {
const wsClient = createWsClient();
createFeishuWSClientMock.mockReturnValue(wsClient);
const abortController = new AbortController();
const accountId = "alpha";
botOpenIds.set(accountId, "ou_alpha");
botNames.set(accountId, "Alpha");
const monitorPromise = monitorWebSocket({
account: createAccount(accountId),
accountId,
runtime: {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
},
abortSignal: abortController.signal,
eventDispatcher: {} as never,
});
expect(wsClient.start).toHaveBeenCalledTimes(1);
expect(wsClients.get(accountId)).toBe(wsClient);
abortController.abort();
await monitorPromise;
expect(wsClient.close).toHaveBeenCalledTimes(1);
expect(wsClients.has(accountId)).toBe(false);
expect(botOpenIds.has(accountId)).toBe(false);
expect(botNames.has(accountId)).toBe(false);
});
it("closes targeted websocket clients during stop cleanup", () => {
const alphaClient = createWsClient();
const betaClient = createWsClient();
wsClients.set("alpha", alphaClient as never);
wsClients.set("beta", betaClient as never);
botOpenIds.set("alpha", "ou_alpha");
botOpenIds.set("beta", "ou_beta");
botNames.set("alpha", "Alpha");
botNames.set("beta", "Beta");
stopFeishuMonitorState("alpha");
expect(alphaClient.close).toHaveBeenCalledTimes(1);
expect(betaClient.close).not.toHaveBeenCalled();
expect(wsClients.has("alpha")).toBe(false);
expect(wsClients.has("beta")).toBe(true);
expect(botOpenIds.has("alpha")).toBe(false);
expect(botOpenIds.has("beta")).toBe(true);
expect(botNames.has("alpha")).toBe(false);
expect(botNames.has("beta")).toBe(true);
});
it("closes all websocket clients during global stop cleanup", () => {
const alphaClient = createWsClient();
const betaClient = createWsClient();
wsClients.set("alpha", alphaClient as never);
wsClients.set("beta", betaClient as never);
botOpenIds.set("alpha", "ou_alpha");
botOpenIds.set("beta", "ou_beta");
botNames.set("alpha", "Alpha");
botNames.set("beta", "Beta");
stopFeishuMonitorState();
expect(alphaClient.close).toHaveBeenCalledTimes(1);
expect(betaClient.close).toHaveBeenCalledTimes(1);
expect(wsClients.size).toBe(0);
expect(botOpenIds.size).toBe(0);
expect(botNames.size).toBe(0);
});
});

View File

@@ -104,15 +104,6 @@ const feishuWebhookAnomalyTracker = createWebhookAnomalyTracker({
logEvery: feishuWebhookAnomalyDefaults.logEvery,
});
function closeWsClient(client: Lark.WSClient | undefined): void {
if (!client) return;
try {
client.close();
} catch {
/* Best-effort cleanup */
}
}
export function clearFeishuWebhookRateLimitStateForTest(): void {
feishuWebhookRateLimiter.clear();
feishuWebhookAnomalyTracker.clear();
@@ -143,7 +134,6 @@ export function recordWebhookStatus(
export function stopFeishuMonitorState(accountId?: string): void {
if (accountId) {
closeWsClient(wsClients.get(accountId));
wsClients.delete(accountId);
const server = httpServers.get(accountId);
if (server) {
@@ -155,9 +145,6 @@ export function stopFeishuMonitorState(accountId?: string): void {
return;
}
for (const client of wsClients.values()) {
closeWsClient(client);
}
wsClients.clear();
for (const server of httpServers.values()) {
server.close();

View File

@@ -1,11 +1,11 @@
import { vi } from "vitest";
export function createFeishuClientMockModule(): {
createFeishuWSClient: () => { start: () => void; close: () => void };
createFeishuWSClient: () => { start: () => void };
createEventDispatcher: () => { register: () => void };
} {
return {
createFeishuWSClient: vi.fn(() => ({ start: vi.fn(), close: vi.fn() })),
createFeishuWSClient: vi.fn(() => ({ start: vi.fn() })),
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
};
}

View File

@@ -89,35 +89,23 @@ export async function monitorWebSocket({
eventDispatcher,
}: MonitorTransportParams): Promise<void> {
const log = runtime?.log ?? console.log;
const error = runtime?.error ?? console.error;
log(`feishu[${accountId}]: starting WebSocket connection...`);
const wsClient = createFeishuWSClient(account);
wsClients.set(accountId, wsClient);
return new Promise((resolve, reject) => {
let cleanedUp = false;
const cleanup = () => {
if (cleanedUp) return;
cleanedUp = true;
abortSignal?.removeEventListener("abort", handleAbort);
try {
wsClient.close();
} catch (err) {
error(`feishu[${accountId}]: error closing WebSocket client: ${String(err)}`);
} finally {
wsClients.delete(accountId);
botOpenIds.delete(accountId);
botNames.delete(accountId);
}
wsClients.delete(accountId);
botOpenIds.delete(accountId);
botNames.delete(accountId);
};
function handleAbort() {
const handleAbort = () => {
log(`feishu[${accountId}]: abort signal received, stopping`);
cleanup();
resolve();
}
};
if (abortSignal?.aborted) {
cleanup();
@@ -132,6 +120,7 @@ export async function monitorWebSocket({
log(`feishu[${accountId}]: WebSocket client started`);
} catch (err) {
cleanup();
abortSignal?.removeEventListener("abort", handleAbort);
reject(err);
}
});

View File

@@ -269,19 +269,18 @@ describe("sendMessageIMessage", () => {
expect(result.messageId).toBe("123");
});
it("passes replyToId as separate reply_to param instead of embedding in text", async () => {
it("prepends reply tag as the first token when replyToId is provided", async () => {
requestMock.mockClear().mockResolvedValue({ ok: true });
stopMock.mockClear().mockResolvedValue(undefined);
await sendWithDefaults("chat_id:123", "hello world", {
await sendWithDefaults("chat_id:123", " hello\nworld", {
replyToId: "abc-123",
});
const params = getSentParams();
expect(params.text).toBe("hello world");
expect(params.reply_to).toBe("abc-123");
expect(params.text).toBe("[[reply_to:abc-123]] hello\nworld");
});
it("strips inline reply tags from text and passes replyToId as reply_to param", async () => {
it("rewrites an existing leading reply tag to keep the requested id first", async () => {
requestMock.mockClear().mockResolvedValue({ ok: true });
stopMock.mockClear().mockResolvedValue(undefined);
@@ -289,11 +288,10 @@ describe("sendMessageIMessage", () => {
replyToId: "new-id",
});
const params = getSentParams();
expect(params.text).toBe("hello");
expect(params.reply_to).toBe("new-id");
expect(params.text).toBe("[[reply_to:new-id]] hello");
});
it("sanitizes replyToId before passing as reply_to param", async () => {
it("sanitizes replyToId before writing the leading reply tag", async () => {
requestMock.mockClear().mockResolvedValue({ ok: true });
stopMock.mockClear().mockResolvedValue(undefined);
@@ -301,11 +299,10 @@ describe("sendMessageIMessage", () => {
replyToId: " [ab]\n\u0000c\td ] ",
});
const params = getSentParams();
expect(params.text).toBe("hello");
expect(params.reply_to).toBe("abcd");
expect(params.text).toBe("[[reply_to:abcd]] hello");
});
it("omits reply_to param when sanitized replyToId is empty", async () => {
it("skips reply tagging when sanitized replyToId is empty", async () => {
requestMock.mockClear().mockResolvedValue({ ok: true });
stopMock.mockClear().mockResolvedValue(undefined);
@@ -314,35 +311,6 @@ describe("sendMessageIMessage", () => {
});
const params = getSentParams();
expect(params.text).toBe("hello");
expect(params.reply_to).toBeUndefined();
});
it("strips stray [[reply_to:...]] tags from text even without replyToId option", async () => {
requestMock.mockClear().mockResolvedValue({ ok: true });
stopMock.mockClear().mockResolvedValue(undefined);
await sendWithDefaults("chat_id:123", "[[reply_to:65]] Great question");
const params = getSentParams();
expect(params.text).toBe("Great question");
expect(params.reply_to).toBeUndefined();
});
it("strips [[audio_as_voice]] tags from outbound text", async () => {
requestMock.mockClear().mockResolvedValue({ ok: true });
stopMock.mockClear().mockResolvedValue(undefined);
await sendWithDefaults("chat_id:123", "hello [[audio_as_voice]] world");
const params = getSentParams();
expect(params.text).toBe("hello world");
});
it("throws when text is only directive tags and no media", async () => {
requestMock.mockClear().mockResolvedValue({ ok: true });
stopMock.mockClear().mockResolvedValue(undefined);
await expect(sendWithDefaults("chat_id:123", "[[reply_to:65]]")).rejects.toThrow(
"iMessage send requires text or media",
);
});
it("normalizes string message_id values from rpc result", async () => {

View File

@@ -3,7 +3,6 @@ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
import { kindFromMime } from "openclaw/plugin-sdk/media-runtime";
import { resolveOutboundAttachmentFromUrl } from "openclaw/plugin-sdk/media-runtime";
import { convertMarkdownTables } from "openclaw/plugin-sdk/text-runtime";
import { stripInlineDirectiveTagsForDelivery } from "openclaw/plugin-sdk/text-runtime";
import { resolveIMessageAccount, type ResolvedIMessageAccount } from "./accounts.js";
import { createIMessageRpcClient, type IMessageRpcClient } from "./client.js";
import { formatIMessageChatTarget, type IMessageService, parseIMessageTarget } from "./targets.js";
@@ -35,6 +34,7 @@ export type IMessageSendResult = {
messageId: string;
};
const LEADING_REPLY_TAG_RE = /^\s*\[\[\s*reply_to\s*:\s*([^\]\n]+)\s*\]\]\s*/i;
const MAX_REPLY_TO_ID_LENGTH = 256;
function stripUnsafeReplyTagChars(value: string): string {
@@ -64,6 +64,21 @@ function sanitizeReplyToId(rawReplyToId?: string): string | undefined {
return sanitized;
}
function prependReplyTagIfNeeded(message: string, replyToId?: string): string {
const resolvedReplyToId = sanitizeReplyToId(replyToId);
if (!resolvedReplyToId) {
return message;
}
const replyTag = `[[reply_to:${resolvedReplyToId}]]`;
const existingLeadingTag = message.match(LEADING_REPLY_TAG_RE);
if (existingLeadingTag) {
const remainder = message.slice(existingLeadingTag[0].length).trimStart();
return remainder ? `${replyTag} ${remainder}` : replyTag;
}
const trimmedMessage = message.trimStart();
return trimmedMessage ? `${replyTag} ${trimmedMessage}` : replyTag;
}
function resolveMessageId(result: Record<string, unknown> | null | undefined): string | null {
if (!result) {
return null;
@@ -132,19 +147,13 @@ export async function sendMessageIMessage(
});
message = convertMarkdownTables(message, tableMode);
}
message = stripInlineDirectiveTagsForDelivery(message).text;
if (!message.trim() && !filePath) {
throw new Error("iMessage send requires text or media");
}
const resolvedReplyToId = sanitizeReplyToId(opts.replyToId);
message = prependReplyTagIfNeeded(message, opts.replyToId);
const params: Record<string, unknown> = {
text: message,
service: service || "auto",
region,
};
if (resolvedReplyToId) {
params.reply_to = resolvedReplyToId;
}
if (filePath) {
params.file = filePath;
}

View File

@@ -22,7 +22,7 @@ export default definePluginEntry({
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Kimi Code API key (subscription)",
label: "Kimi API key (subscription)",
hint: "Kimi K2.5 + Kimi",
optionKey: "kimiCodeApiKey",
flagName: "--kimi-code-api-key",
@@ -38,10 +38,10 @@ export default definePluginEntry({
noteTitle: "Kimi",
wizard: {
choiceId: "kimi-code-api-key",
choiceLabel: "Kimi Code API key (subscription)",
choiceLabel: "Kimi API key (subscription)",
groupId: "moonshot",
groupLabel: "Moonshot AI (Kimi K2.5)",
groupHint: "Kimi K2.5",
groupHint: "Kimi K2.5 + Kimi",
},
}),
],

View File

@@ -10,14 +10,14 @@
"provider": "kimi",
"method": "api-key",
"choiceId": "kimi-code-api-key",
"choiceLabel": "Kimi Code API key (subscription)",
"groupId": "moonshot",
"groupLabel": "Moonshot AI (Kimi K2.5)",
"groupHint": "Kimi K2.5",
"choiceLabel": "Kimi Code API key",
"groupId": "kimi-code",
"groupLabel": "Kimi Code",
"groupHint": "Dedicated coding endpoint",
"optionKey": "kimiCodeApiKey",
"cliFlag": "--kimi-code-api-key",
"cliOption": "--kimi-code-api-key <key>",
"cliDescription": "Kimi Code API key (subscription)"
"cliDescription": "Kimi Code API key"
}
],
"configSchema": {

View File

@@ -48,7 +48,6 @@ function fakeApi(overrides: Partial<OpenClawPluginApi> = {}): OpenClawPluginApi
registerSpeechProvider() {},
registerMediaUnderstandingProvider() {},
registerImageGenerationProvider() {},
registerVideoGenerationProvider() {},
registerWebSearchProvider() {},
registerInteractiveHandler() {},
onConversationBindingResolved() {},

View File

@@ -1086,7 +1086,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
idLine: `Your Mattermost user id: ${senderId}`,
code,
}),
{ cfg, accountId: account.accountId },
{ accountId: account.accountId },
);
opts.statusSink?.({ lastOutboundAt: Date.now() });
} catch (err) {

View File

@@ -45,7 +45,6 @@ describe("deliverMattermostReplyPayload", () => {
"channel:town-square",
"caption",
expect.objectContaining({
cfg,
accountId: "default",
mediaUrl,
replyToId: "root-post",
@@ -64,7 +63,6 @@ describe("deliverMattermostReplyPayload", () => {
it("forwards replyToId for text-only chunked replies", async () => {
const sendMessage = vi.fn(async () => undefined);
const cfg = {} satisfies OpenClawConfig;
const core = {
channel: {
text: {
@@ -77,7 +75,7 @@ describe("deliverMattermostReplyPayload", () => {
await deliverMattermostReplyPayload({
core,
cfg,
cfg: {} satisfies OpenClawConfig,
payload: { text: "hello" },
to: "channel:town-square",
accountId: "default",
@@ -89,14 +87,9 @@ describe("deliverMattermostReplyPayload", () => {
});
expect(sendMessage).toHaveBeenCalledTimes(1);
expect(sendMessage).toHaveBeenCalledWith(
"channel:town-square",
"hello",
expect.objectContaining({
cfg,
accountId: "default",
replyToId: "root-post",
}),
);
expect(sendMessage).toHaveBeenCalledWith("channel:town-square", "hello", {
accountId: "default",
replyToId: "root-post",
});
});
});

View File

@@ -11,7 +11,6 @@ type SendMattermostMessage = (
to: string,
text: string,
opts: {
cfg?: OpenClawConfig;
accountId?: string;
mediaUrl?: string;
mediaLocalRoots?: readonly string[];
@@ -50,14 +49,12 @@ export async function deliverMattermostReplyPayload(params: {
params.core.channel.text.chunkMarkdownTextWithMode(value, params.textLimit, chunkMode),
sendText: async (chunk) => {
await params.sendMessage(params.to, chunk, {
cfg: params.cfg,
accountId: params.accountId,
replyToId: params.replyToId,
});
},
sendMedia: async ({ mediaUrl, caption }) => {
await params.sendMessage(params.to, caption ?? "", {
cfg: params.cfg,
accountId: params.accountId,
mediaUrl,
mediaLocalRoots,

View File

@@ -1,193 +0,0 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { PassThrough } from "node:stream";
import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/mattermost";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ResolvedMattermostAccount } from "./accounts.js";
const mockState = vi.hoisted(() => ({
readRequestBodyWithLimit: vi.fn(async () => "token=valid-token"),
parseSlashCommandPayload: vi.fn(() => ({
token: "valid-token",
command: "/oc_models",
text: "models",
channel_id: "chan-1",
user_id: "user-1",
user_name: "alice",
team_id: "team-1",
})),
resolveCommandText: vi.fn((_trigger: string, text: string) => text),
buildModelsProviderData: vi.fn(async () => ({ providers: [] })),
resolveMattermostModelPickerEntry: vi.fn(() => ({ kind: "summary" })),
authorizeMattermostCommandInvocation: vi.fn(() => ({
ok: true,
commandAuthorized: true,
channelInfo: { id: "chan-1", type: "O", name: "town-square", display_name: "Town Square" },
kind: "channel",
chatType: "channel",
channelName: "town-square",
channelDisplay: "Town Square",
roomLabel: "#town-square",
})),
createMattermostClient: vi.fn(() => ({})),
fetchMattermostChannel: vi.fn(async () => ({
id: "chan-1",
type: "O",
name: "town-square",
display_name: "Town Square",
})),
sendMessageMattermost: vi.fn(async () => ({ messageId: "post-1", channelId: "chan-1" })),
normalizeMattermostAllowList: vi.fn((value: unknown) => value),
}));
vi.mock("openclaw/plugin-sdk/mattermost", () => ({
buildModelsProviderData: mockState.buildModelsProviderData,
createReplyPrefixOptions: vi.fn(() => ({})),
createTypingCallbacks: vi.fn(() => ({ onReplyStart: vi.fn() })),
isRequestBodyLimitError: vi.fn(() => false),
logTypingFailure: vi.fn(),
readRequestBodyWithLimit: mockState.readRequestBodyWithLimit,
}));
vi.mock("../runtime.js", () => ({
getMattermostRuntime: () => ({
channel: {
commands: {
shouldHandleTextCommands: () => true,
},
text: {
hasControlCommand: () => false,
},
pairing: {
readAllowFromStore: vi.fn(async () => []),
},
routing: {
resolveAgentRoute: vi.fn(() => ({
agentId: "agent-1",
sessionKey: "mattermost:session:1",
accountId: "default",
})),
},
},
}),
}));
vi.mock("./client.js", () => ({
createMattermostClient: mockState.createMattermostClient,
fetchMattermostChannel: mockState.fetchMattermostChannel,
normalizeMattermostBaseUrl: vi.fn((value: string | undefined) => value?.trim() ?? ""),
sendMattermostTyping: vi.fn(),
}));
vi.mock("./model-picker.js", () => ({
renderMattermostModelSummaryView: vi.fn(),
renderMattermostModelsPickerView: vi.fn(),
renderMattermostProviderPickerView: vi.fn(),
resolveMattermostModelPickerCurrentModel: vi.fn(),
resolveMattermostModelPickerEntry: mockState.resolveMattermostModelPickerEntry,
}));
vi.mock("./monitor-auth.js", () => ({
authorizeMattermostCommandInvocation: mockState.authorizeMattermostCommandInvocation,
normalizeMattermostAllowList: mockState.normalizeMattermostAllowList,
}));
vi.mock("./reply-delivery.js", () => ({
deliverMattermostReplyPayload: vi.fn(),
}));
vi.mock("./send.js", () => ({
sendMessageMattermost: mockState.sendMessageMattermost,
}));
vi.mock("./slash-commands.js", () => ({
parseSlashCommandPayload: mockState.parseSlashCommandPayload,
resolveCommandText: mockState.resolveCommandText,
}));
import { createSlashCommandHttpHandler } from "./slash-http.js";
function createRequest(body = "token=valid-token"): IncomingMessage {
const req = new PassThrough();
const incoming = req as unknown as IncomingMessage;
incoming.method = "POST";
incoming.headers = {
"content-type": "application/x-www-form-urlencoded",
};
process.nextTick(() => {
req.end(body);
});
return incoming;
}
function createResponse(): {
res: ServerResponse;
getBody: () => string;
} {
let body = "";
const res = {
statusCode: 200,
setHeader() {},
end(chunk?: string | Buffer) {
body = chunk ? String(chunk) : "";
},
} as unknown as ServerResponse;
return {
res,
getBody: () => body,
};
}
const accountFixture: ResolvedMattermostAccount = {
accountId: "default",
enabled: true,
botToken: "bot-token",
baseUrl: "https://chat.example.com",
botTokenSource: "config",
baseUrlSource: "config",
config: {},
};
describe("slash-http cfg threading", () => {
beforeEach(() => {
mockState.readRequestBodyWithLimit.mockClear();
mockState.parseSlashCommandPayload.mockClear();
mockState.resolveCommandText.mockClear();
mockState.buildModelsProviderData.mockClear();
mockState.resolveMattermostModelPickerEntry.mockClear();
mockState.authorizeMattermostCommandInvocation.mockClear();
mockState.createMattermostClient.mockClear();
mockState.fetchMattermostChannel.mockClear();
mockState.sendMessageMattermost.mockClear();
mockState.normalizeMattermostAllowList.mockClear();
});
it("passes cfg through the no-models slash reply send path", async () => {
const cfg = {
channels: {
mattermost: {
botToken: "exec:secret-ref",
},
},
} as OpenClawConfig;
const handler = createSlashCommandHttpHandler({
account: accountFixture,
cfg,
runtime: {} as RuntimeEnv,
commandTokens: new Set(["valid-token"]),
});
const response = createResponse();
await handler(createRequest(), response.res);
expect(response.res.statusCode).toBe(200);
expect(response.getBody()).toContain("Processing");
expect(mockState.sendMessageMattermost).toHaveBeenCalledWith(
"channel:chan-1",
"No models available.",
expect.objectContaining({
cfg,
accountId: "default",
}),
);
});
});

View File

@@ -316,7 +316,6 @@ export function createSlashCommandHttpHandler(params: SlashHttpHandlerParams) {
try {
const to = `channel:${channelId}`;
await sendMessageMattermost(to, "Sorry, something went wrong processing that command.", {
cfg,
accountId: account.accountId,
});
} catch {
@@ -388,7 +387,6 @@ async function handleSlashCommandAsync(params: {
const data = await buildModelsProviderData(cfg, route.agentId);
if (data.providers.length === 0) {
await sendMessageMattermost(to, "No models available.", {
cfg,
accountId: account.accountId,
});
return;
@@ -420,7 +418,6 @@ async function handleSlashCommandAsync(params: {
});
await sendMessageMattermost(to, view.text, {
cfg,
accountId: account.accountId,
buttons: view.buttons,
});

View File

@@ -26,7 +26,7 @@ describe("listMicrosoftVoices", () => {
]),
{ status: 200 },
),
) as unknown as typeof globalThis.fetch;
) as typeof globalThis.fetch;
const voices = await listMicrosoftVoices();
@@ -56,9 +56,7 @@ describe("listMicrosoftVoices", () => {
it("throws on Microsoft voice list failures", async () => {
globalThis.fetch = vi
.fn()
.mockResolvedValue(
new Response("nope", { status: 503 }),
) as unknown as typeof globalThis.fetch;
.mockResolvedValue(new Response("nope", { status: 503 })) as typeof globalThis.fetch;
await expect(listMicrosoftVoices()).rejects.toThrow("Microsoft voices API error (503)");
});

View File

@@ -3,7 +3,7 @@
Bundled MiniMax plugin for both:
- API-key provider setup (`minimax`)
- Token Plan OAuth setup (`minimax-portal`)
- Coding Plan OAuth setup (`minimax-portal`)
## Enable
@@ -34,4 +34,4 @@ openclaw setup --wizard --auth-choice minimax-global-api
## Notes
- MiniMax OAuth uses a user-code login flow.
- OAuth currently targets the Token Plan path.
- OAuth currently targets the Coding Plan path.

View File

@@ -1,176 +0,0 @@
import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generation";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth";
const DEFAULT_MINIMAX_IMAGE_BASE_URL = "https://api.minimax.io";
const DEFAULT_MODEL = "image-01";
const DEFAULT_OUTPUT_MIME = "image/png";
const MINIMAX_SUPPORTED_ASPECT_RATIOS = [
"1:1",
"16:9",
"4:3",
"3:2",
"2:3",
"3:4",
"9:16",
"21:9",
] as const;
type MinimaxImageApiResponse = {
data?: {
image_base64?: string[];
};
metadata?: {
success_count?: number;
failed_count?: number;
};
id?: string;
base_resp?: {
status_code?: number;
status_msg?: string;
};
};
function resolveMinimaxImageBaseUrl(
cfg: Parameters<typeof resolveApiKeyForProvider>[0]["cfg"],
providerId: string,
): string {
const direct = cfg?.models?.providers?.[providerId]?.baseUrl?.trim();
if (!direct) {
return DEFAULT_MINIMAX_IMAGE_BASE_URL;
}
// Extract origin from the configured base URL (which may include path like /anthropic)
try {
return new URL(direct).origin;
} catch {
return DEFAULT_MINIMAX_IMAGE_BASE_URL;
}
}
function buildMinimaxImageProvider(providerId: string): ImageGenerationProvider {
return {
id: providerId,
label: "MiniMax",
defaultModel: DEFAULT_MODEL,
models: [DEFAULT_MODEL],
capabilities: {
generate: {
maxCount: 9,
supportsSize: false,
supportsAspectRatio: true,
supportsResolution: false,
},
edit: {
enabled: true,
maxCount: 9,
maxInputImages: 1,
supportsSize: false,
supportsAspectRatio: true,
supportsResolution: false,
},
geometry: {
aspectRatios: [...MINIMAX_SUPPORTED_ASPECT_RATIOS],
},
},
async generateImage(req) {
const auth = await resolveApiKeyForProvider({
provider: providerId,
cfg: req.cfg,
agentDir: req.agentDir,
store: req.authStore,
});
if (!auth.apiKey) {
throw new Error("MiniMax API key missing");
}
const baseUrl = resolveMinimaxImageBaseUrl(req.cfg, providerId);
const body: Record<string, unknown> = {
model: req.model || DEFAULT_MODEL,
prompt: req.prompt,
response_format: "base64",
n: req.count ?? 1,
};
if (req.aspectRatio?.trim()) {
body.aspect_ratio = req.aspectRatio.trim();
}
// Map input images to subject_reference for image-to-image generation
if (req.inputImages && req.inputImages.length > 0) {
const ref = req.inputImages[0];
const mime = ref.mimeType || "image/jpeg";
const dataUrl = `data:${mime};base64,${ref.buffer.toString("base64")}`;
body.subject_reference = [{ type: "character", image_file: dataUrl }];
}
const controller = new AbortController();
const timeoutMs = req.timeoutMs;
const timeout =
typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
? setTimeout(() => controller.abort(), timeoutMs)
: undefined;
const response = await fetch(`${baseUrl}/v1/image_generation`, {
method: "POST",
headers: {
Authorization: `Bearer ${auth.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
signal: controller.signal,
}).finally(() => {
clearTimeout(timeout);
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`MiniMax image generation failed (${response.status}): ${text || response.statusText}`,
);
}
const data = (await response.json()) as MinimaxImageApiResponse;
const baseResp = data.base_resp;
if (baseResp && typeof baseResp.status_code === "number" && baseResp.status_code !== 0) {
const msg = baseResp.status_msg ?? "";
throw new Error(`MiniMax image generation API error (${baseResp.status_code}): ${msg}`);
}
const base64Images = data.data?.image_base64 ?? [];
const failedCount = data.metadata?.failed_count ?? 0;
if (base64Images.length === 0) {
const reason =
failedCount > 0 ? `${failedCount} image(s) failed to generate` : "no images returned";
throw new Error(`MiniMax image generation returned no images: ${reason}`);
}
const images = base64Images
.map((b64, index) => {
if (!b64) {
return null;
}
return {
buffer: Buffer.from(b64, "base64"),
mimeType: DEFAULT_OUTPUT_MIME,
fileName: `image-${index + 1}.png`,
};
})
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
return {
images,
model: req.model || DEFAULT_MODEL,
};
},
};
}
export function buildMinimaxImageGenerationProvider(): ImageGenerationProvider {
return buildMinimaxImageProvider("minimax");
}
export function buildMinimaxPortalImageGenerationProvider(): ImageGenerationProvider {
return buildMinimaxImageProvider("minimax-portal");
}

View File

@@ -16,10 +16,6 @@ import {
MINIMAX_DEFAULT_MODEL_ID,
} from "openclaw/plugin-sdk/provider-models";
import { fetchMinimaxUsage } from "openclaw/plugin-sdk/provider-usage";
import {
buildMinimaxImageGenerationProvider,
buildMinimaxPortalImageGenerationProvider,
} from "./image-generation-provider.js";
import {
minimaxMediaUnderstandingProvider,
minimaxPortalMediaUnderstandingProvider,
@@ -134,10 +130,22 @@ function createOAuthHandler(region: MiniMaxRegion) {
agents: {
defaults: {
models: {
[portalModelRef("MiniMax-M2")]: { alias: "minimax-m2" },
[portalModelRef("MiniMax-M2.1")]: { alias: "minimax-m2.1" },
[portalModelRef("MiniMax-M2.1-highspeed")]: {
alias: "minimax-m2.1-highspeed",
},
[portalModelRef("MiniMax-M2.7")]: { alias: "minimax-m2.7" },
[portalModelRef("MiniMax-M2.7-highspeed")]: {
alias: "minimax-m2.7-highspeed",
},
[portalModelRef("MiniMax-M2.5")]: { alias: "minimax-m2.5" },
[portalModelRef("MiniMax-M2.5-highspeed")]: {
alias: "minimax-m2.5-highspeed",
},
[portalModelRef("MiniMax-M2.5-Lightning")]: {
alias: "minimax-m2.5-lightning",
},
},
},
},
@@ -235,9 +243,6 @@ export default definePluginEntry({
await fetchMinimaxUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn),
});
api.registerMediaUnderstandingProvider(minimaxMediaUnderstandingProvider);
api.registerMediaUnderstandingProvider(minimaxPortalMediaUnderstandingProvider);
api.registerProvider({
id: PORTAL_PROVIDER_ID,
label: PROVIDER_LABEL,
@@ -280,7 +285,7 @@ export default definePluginEntry({
],
isModernModelRef: ({ modelId }) => isMiniMaxModernModelId(modelId),
});
api.registerImageGenerationProvider(buildMinimaxImageGenerationProvider());
api.registerImageGenerationProvider(buildMinimaxPortalImageGenerationProvider());
api.registerMediaUnderstandingProvider(minimaxMediaUnderstandingProvider);
api.registerMediaUnderstandingProvider(minimaxPortalMediaUnderstandingProvider);
},
});

View File

@@ -26,14 +26,14 @@ describe("minimax model definitions", () => {
it("builds catalog model with name and reasoning from catalog", () => {
const model = buildMinimaxModelDefinition({
id: "MiniMax-M2.7",
id: "MiniMax-M2.1",
cost: MINIMAX_API_COST,
contextWindow: DEFAULT_MINIMAX_CONTEXT_WINDOW,
maxTokens: DEFAULT_MINIMAX_MAX_TOKENS,
});
expect(model).toMatchObject({
id: "MiniMax-M2.7",
name: "MiniMax M2.7",
id: "MiniMax-M2.1",
name: "MiniMax M2.1",
reasoning: true,
});
});

View File

@@ -9,6 +9,7 @@ import {
} from "openclaw/plugin-sdk/provider-models";
const MINIMAX_PORTAL_BASE_URL = "https://api.minimax.io/anthropic";
const MINIMAX_DEFAULT_VISION_MODEL_ID = "MiniMax-VL-01";
const MINIMAX_DEFAULT_CONTEXT_WINDOW = 204800;
const MINIMAX_DEFAULT_MAX_TOKENS = 131072;
const MINIMAX_API_COST = {
@@ -44,14 +45,22 @@ function buildMinimaxTextModel(params: {
}
function buildMinimaxCatalog(): ModelDefinitionConfig[] {
return MINIMAX_TEXT_MODEL_ORDER.map((id) => {
const model = MINIMAX_TEXT_MODEL_CATALOG[id];
return buildMinimaxTextModel({
id,
name: model.name,
reasoning: model.reasoning,
});
});
return [
buildMinimaxModel({
id: MINIMAX_DEFAULT_VISION_MODEL_ID,
name: "MiniMax VL 01",
reasoning: false,
input: ["text", "image"],
}),
...MINIMAX_TEXT_MODEL_ORDER.map((id) => {
const model = MINIMAX_TEXT_MODEL_CATALOG[id];
return buildMinimaxTextModel({
id,
name: model.name,
reasoning: model.reasoning,
});
}),
];
}
export function buildMinimaxProvider(): ModelProviderConfig {

View File

@@ -60,7 +60,7 @@ describe("msteams graph helpers", () => {
status: 200,
headers: { "content-type": "application/json" },
});
}) as unknown as typeof fetch;
}) as typeof fetch;
await expect(
fetchGraphJson<{ value: Array<{ id: string }> }>({
@@ -82,7 +82,7 @@ describe("msteams graph helpers", () => {
globalThis.fetch = vi.fn(async () => {
return new Response("forbidden", { status: 403 });
}) as unknown as typeof fetch;
}) as typeof fetch;
await expect(
fetchGraphJson({
@@ -148,7 +148,7 @@ describe("msteams graph helpers", () => {
headers: { "content-type": "application/json" },
},
);
}) as unknown as typeof fetch;
}) as typeof fetch;
await expect(listTeamsByName("graph-token", "Bob's Team")).resolves.toEqual([
{ id: "team-1", displayName: "Ops" },
@@ -165,7 +165,7 @@ describe("msteams graph helpers", () => {
});
it("returns no graph users for blank queries", async () => {
globalThis.fetch = vi.fn() as unknown as typeof fetch;
globalThis.fetch = vi.fn() as typeof fetch;
await expect(searchGraphUsers({ token: "token-1", query: " " })).resolves.toEqual([]);
expect(globalThis.fetch).not.toHaveBeenCalled();
});
@@ -176,7 +176,7 @@ describe("msteams graph helpers", () => {
status: 200,
headers: { "content-type": "application/json" },
});
}) as unknown as typeof fetch;
}) as typeof fetch;
const result = await searchGraphUsers({
token: "token-2",
@@ -202,7 +202,7 @@ describe("msteams graph helpers", () => {
status: 200,
headers: { "content-type": "application/json" },
});
}) as unknown as typeof fetch;
}) as typeof fetch;
await expect(searchGraphUsers({ token: "token-3", query: "bob", top: 25 })).resolves.toEqual([
{ id: "user-2", displayName: "Bob" },

View File

@@ -421,9 +421,6 @@ class OpenShellSandboxBackendImpl {
await replaceDirectoryContents({
sourceDir: tmpDir,
targetDir: this.params.createParams.workspaceDir,
// Never sync hooks/ from the remote sandbox — mirrored content must not
// become trusted workspace hook code on the host.
excludeDirs: ["hooks"],
});
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });

View File

@@ -1,92 +0,0 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { replaceDirectoryContents } from "./mirror.js";
describe("replaceDirectoryContents", () => {
const dirs: string[] = [];
async function makeTmpDir(): Promise<string> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mirror-test-"));
dirs.push(dir);
return dir;
}
afterEach(async () => {
await Promise.all(dirs.map((d) => fs.rm(d, { recursive: true, force: true })));
dirs.length = 0;
});
it("copies source entries to target", async () => {
const source = await makeTmpDir();
const target = await makeTmpDir();
await fs.writeFile(path.join(source, "a.txt"), "hello");
await fs.writeFile(path.join(target, "old.txt"), "stale");
await replaceDirectoryContents({ sourceDir: source, targetDir: target });
expect(await fs.readFile(path.join(target, "a.txt"), "utf8")).toBe("hello");
await expect(fs.access(path.join(target, "old.txt"))).rejects.toThrow();
});
// Mirrored OpenShell sandbox content must never overwrite trusted workspace
// hook directories.
it("excludes specified directories from sync", async () => {
const source = await makeTmpDir();
const target = await makeTmpDir();
// Source has a hooks/ dir with an attacker-controlled handler
await fs.mkdir(path.join(source, "hooks", "evil"), { recursive: true });
await fs.writeFile(
path.join(source, "hooks", "evil", "handler.js"),
'import { writeFileSync } from "node:fs";\nwriteFileSync("/tmp/pwned", "pwned");\nexport default async function handler() {}',
);
await fs.writeFile(path.join(source, "code.txt"), "legit");
// Target has existing trusted hooks
await fs.mkdir(path.join(target, "hooks", "trusted"), { recursive: true });
await fs.writeFile(path.join(target, "hooks", "trusted", "handler.js"), "// trusted code");
await fs.writeFile(path.join(target, "existing.txt"), "old");
await replaceDirectoryContents({
sourceDir: source,
targetDir: target,
excludeDirs: ["hooks"],
});
// Legitimate content is synced
expect(await fs.readFile(path.join(target, "code.txt"), "utf8")).toBe("legit");
// Old non-excluded content is removed
await expect(fs.access(path.join(target, "existing.txt"))).rejects.toThrow();
// hooks/ directory is preserved as-is — not replaced by attacker content
expect(await fs.readFile(path.join(target, "hooks", "trusted", "handler.js"), "utf8")).toBe(
"// trusted code",
);
await expect(fs.access(path.join(target, "hooks", "evil"))).rejects.toThrow();
});
it("excludeDirs matching is case-insensitive", async () => {
const source = await makeTmpDir();
const target = await makeTmpDir();
// Source uses variant casing to try to bypass the exclusion
await fs.mkdir(path.join(source, "Hooks", "evil"), { recursive: true });
await fs.writeFile(path.join(source, "Hooks", "evil", "handler.js"), "// malicious");
await fs.writeFile(path.join(source, "data.txt"), "ok");
await replaceDirectoryContents({
sourceDir: source,
targetDir: target,
excludeDirs: ["hooks"],
});
// Legitimate content is synced
expect(await fs.readFile(path.join(target, "data.txt"), "utf8")).toBe("ok");
// "Hooks" (variant case) must still be excluded
await expect(fs.access(path.join(target, "Hooks"))).rejects.toThrow();
});
});

View File

@@ -4,30 +4,19 @@ import path from "node:path";
export async function replaceDirectoryContents(params: {
sourceDir: string;
targetDir: string;
/** Top-level directory names to exclude from sync (preserved in target, skipped from source). */
excludeDirs?: string[];
}): Promise<void> {
// Case-insensitive matching: on macOS/Windows the filesystem is typically
// case-insensitive, so "Hooks" would resolve to the same directory as "hooks".
const excluded = new Set((params.excludeDirs ?? []).map((d) => d.toLowerCase()));
const isExcluded = (name: string) => excluded.has(name.toLowerCase());
await fs.mkdir(params.targetDir, { recursive: true });
const existing = await fs.readdir(params.targetDir);
await Promise.all(
existing
.filter((entry) => !isExcluded(entry))
.map((entry) =>
fs.rm(path.join(params.targetDir, entry), {
recursive: true,
force: true,
}),
),
existing.map((entry) =>
fs.rm(path.join(params.targetDir, entry), {
recursive: true,
force: true,
}),
),
);
const sourceEntries = await fs.readdir(params.sourceDir);
for (const entry of sourceEntries) {
if (isExcluded(entry)) {
continue;
}
await fs.cp(path.join(params.sourceDir, entry), path.join(params.targetDir, entry), {
recursive: true,
force: true,

View File

@@ -27,17 +27,12 @@ function createHarness(config: Record<string, unknown>) {
return { command, runtime };
}
function createCommandContext(
args: string,
channel: string = "discord",
gatewayClientScopes?: string[],
) {
function createCommandContext(args: string, channel: string = "discord") {
return {
args,
channel,
channelId: channel,
isAuthorizedSender: true,
gatewayClientScopes,
commandBody: args ? `/voice ${args}` : "/voice",
config: {},
requestConversationBinding: vi.fn(),
@@ -205,87 +200,6 @@ describe("talk-voice plugin", () => {
});
});
it("rejects /voice set from gateway client with only operator.write scope", async () => {
const { command, runtime } = createHarness({
talk: {
provider: "elevenlabs",
providers: {
elevenlabs: {
apiKey: "sk-eleven",
},
},
},
});
vi.mocked(runtime.tts.listVoices).mockResolvedValue([{ id: "voice-a", name: "Claudia" }]);
const result = await command.handler(
createCommandContext("set Claudia", "webchat", ["operator.write"]),
);
expect(result.text).toContain("requires operator.admin");
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
});
it("allows /voice set from gateway client with operator.admin scope", async () => {
const { command, runtime } = createHarness({
talk: {
provider: "elevenlabs",
providers: {
elevenlabs: {
apiKey: "sk-eleven",
},
},
},
});
vi.mocked(runtime.tts.listVoices).mockResolvedValue([{ id: "voice-a", name: "Claudia" }]);
const result = await command.handler(
createCommandContext("set Claudia", "webchat", ["operator.admin"]),
);
expect(runtime.config.writeConfigFile).toHaveBeenCalled();
expect(result.text).toContain("voice-a");
});
it("rejects /voice set from webchat channel with no scopes (TUI/internal)", async () => {
const { command, runtime } = createHarness({
talk: {
provider: "elevenlabs",
providers: {
elevenlabs: {
apiKey: "sk-eleven",
},
},
},
});
vi.mocked(runtime.tts.listVoices).mockResolvedValue([{ id: "voice-a", name: "Claudia" }]);
// gatewayClientScopes omitted — simulates internal webchat session without scopes
const result = await command.handler(createCommandContext("set Claudia", "webchat"));
expect(result.text).toContain("requires operator.admin");
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
});
it("allows /voice set from non-gateway channels without scope check", async () => {
const { command, runtime } = createHarness({
talk: {
provider: "elevenlabs",
providers: {
elevenlabs: {
apiKey: "sk-eleven",
},
},
},
});
vi.mocked(runtime.tts.listVoices).mockResolvedValue([{ id: "voice-a", name: "Claudia" }]);
const result = await command.handler(createCommandContext("set Claudia", "telegram"));
expect(runtime.config.writeConfigFile).toHaveBeenCalled();
expect(result.text).toContain("voice-a");
});
it("returns provider lookup errors cleanly", async () => {
const { command, runtime } = createHarness({
talk: {

View File

@@ -164,14 +164,6 @@ export default definePluginEntry({
}
if (action === "set") {
// Persistent config writes require operator.admin for gateway clients.
// Without this check, a caller with only operator.write could bypass the
// admin-only config.patch RPC by reaching writeConfigFile indirectly
// through chat.send → /voice set.
if (ctx.channel === "webchat" && !ctx.gatewayClientScopes?.includes("operator.admin")) {
return { text: `⚠️ ${commandLabel} set requires operator.admin for gateway clients.` };
}
const query = tokens.slice(1).join(" ").trim();
if (!query) {
return { text: `Usage: ${commandLabel} set <voiceId|name>` };

View File

@@ -193,13 +193,6 @@ export const registerTelegramHandlers = ({
: async () => ({});
return { message, me: ctx.me, getFile };
};
const isSelfAuthoredTelegramMessage = (
ctx: Pick<TelegramContext, "me">,
message: Message,
): boolean => {
const botId = ctx.me?.id;
return typeof botId === "number" && message.from?.id === botId;
};
const inboundDebouncer = createInboundDebouncer<TelegramDebounceEntry>({
debounceMs,
resolveDebounceMs: (entry) =>
@@ -1735,9 +1728,6 @@ export const registerTelegramHandlers = ({
if (!msg) {
return;
}
if (isSelfAuthoredTelegramMessage(ctx, msg)) {
return;
}
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
const isForum = await resolveTelegramForumFlag({
chatId: msg.chat.id,

View File

@@ -507,44 +507,6 @@ describe("createTelegramBot", () => {
}
});
});
it("ignores private self-authored message updates instead of issuing a pairing challenge", async () => {
await withIsolatedStateDirAsync(async () => {
loadConfig.mockReturnValue({
channels: { telegram: { dmPolicy: "pairing" } },
});
readChannelAllowFromStore.mockResolvedValue([]);
upsertChannelPairingRequest.mockClear();
sendMessageSpy.mockClear();
replySpy.mockClear();
createTelegramBot({ token: "tok" });
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
await handler({
message: {
chat: { id: 1234, type: "private", first_name: "Harold" },
message_id: 1884,
date: 1736380800,
from: { id: 7, is_bot: true, first_name: "OpenClaw", username: "openclaw_bot" },
pinned_message: {
message_id: 1883,
date: 1736380799,
chat: { id: 1234, type: "private", first_name: "Harold" },
from: { id: 7, is_bot: true, first_name: "OpenClaw", username: "openclaw_bot" },
text: "Binding: Review pull request 54118 (openclaw)",
},
},
me: { id: 7, is_bot: true, first_name: "OpenClaw", username: "openclaw_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
expect(upsertChannelPairingRequest).not.toHaveBeenCalled();
expect(sendMessageSpy).not.toHaveBeenCalled();
expect(replySpy).not.toHaveBeenCalled();
});
});
it("blocks unauthorized DM media before download and sends pairing reply", async () => {
await withIsolatedStateDirAsync(async () => {
loadConfig.mockReturnValue({

View File

@@ -476,97 +476,6 @@ describe("telegramPlugin duplicate token guard", () => {
expect(await telegramPlugin.config.isConfigured!(alertsAccount, cfg)).toBe(true);
});
// Regression: https://github.com/openclaw/openclaw/issues/53876
// Single-bot setup with channel-level token should report configured.
it("reports configured for single-bot setup with channel-level token", async () => {
const cfg = {
channels: {
telegram: {
botToken: "single-bot-token",
enabled: true,
},
},
} as OpenClawConfig;
const account = resolveAccount(cfg, "default");
expect(await telegramPlugin.config.isConfigured!(account, cfg)).toBe(true);
});
// Regression: https://github.com/openclaw/openclaw/issues/53876
// Binding-created non-default accountId in single-bot setup should report configured.
it("reports configured for binding-created accountId in single-bot setup", async () => {
const cfg = {
channels: {
telegram: {
botToken: "single-bot-token",
enabled: true,
},
},
} as OpenClawConfig;
const account = resolveAccount(cfg, "bot-main");
expect(account.token).toBe("single-bot-token");
expect(await telegramPlugin.config.isConfigured!(account, cfg)).toBe(true);
});
// Regression: multi-bot guard — unknown binding-created accountId in multi-bot
// setup must NOT be reported as configured, matching resolveTelegramToken behaviour.
it("reports not configured for unknown binding-created accountId in multi-bot setup", async () => {
const cfg = {
channels: {
telegram: {
botToken: "channel-level-token",
enabled: true,
accounts: {
knownBot: { botToken: "known-bot-token" },
},
},
},
} as OpenClawConfig;
const account = resolveAccount(cfg, "unknownBot");
expect(await telegramPlugin.config.isConfigured!(account, cfg)).toBe(false);
expect(telegramPlugin.config.unconfiguredReason?.(account, cfg)).toContain("unknown accountId");
});
// Regression: multi-bot guard must use full normalization (same as resolveTelegramToken)
// so that account keys like "Carey Notifications" resolve to "carey-notifications".
it("multi-bot guard normalizes account keys with spaces and mixed case", async () => {
const cfg = {
channels: {
telegram: {
botToken: "channel-level-token",
enabled: true,
accounts: {
"Carey Notifications": { botToken: "carey-token" },
},
},
},
} as OpenClawConfig;
// "carey-notifications" is the normalized form of "Carey Notifications"
const account = resolveAccount(cfg, "carey-notifications");
expect(await telegramPlugin.config.isConfigured!(account, cfg)).toBe(true);
});
// Regression: configured_unavailable token (e.g. unreadable tokenFile) should
// NOT be reported as configured — runtime would fail to authenticate.
it("reports not configured when token is configured_unavailable", async () => {
const cfg = {
channels: {
telegram: {
tokenFile: "/nonexistent/path/to/token",
enabled: true,
},
},
} as OpenClawConfig;
const account = resolveAccount(cfg, "default");
// tokenFile is configured but file doesn't exist → configured_unavailable
expect(await telegramPlugin.config.isConfigured!(account, cfg)).toBe(false);
expect(telegramPlugin.config.unconfiguredReason?.(account, cfg)).toContain("unavailable");
});
it("does not crash startup when a resolved account token is undefined", async () => {
const { monitorTelegramProvider, probeTelegram } = installGatewayRuntime({
probeOk: false,

View File

@@ -88,7 +88,6 @@ function buildTelegramSendOptions(params: {
threadId?: string | number | null;
silent?: boolean | null;
forceDocument?: boolean | null;
gatewayClientScopes?: readonly string[] | null;
}): TelegramSendOptions {
return {
verbose: false,
@@ -100,9 +99,6 @@ function buildTelegramSendOptions(params: {
accountId: params.accountId ?? undefined,
silent: params.silent ?? undefined,
forceDocument: params.forceDocument ?? undefined,
...(Array.isArray(params.gatewayClientScopes)
? { gatewayClientScopes: [...params.gatewayClientScopes] }
: {}),
};
}
@@ -117,7 +113,6 @@ async function sendTelegramOutbound(params: {
replyToId?: string | null;
threadId?: string | number | null;
silent?: boolean | null;
gatewayClientScopes?: readonly string[] | null;
}) {
const send =
resolveOutboundSendDep<TelegramSendFn>(params.deps, "telegram") ??
@@ -133,7 +128,6 @@ async function sendTelegramOutbound(params: {
replyToId: params.replyToId,
threadId: params.threadId,
silent: params.silent,
gatewayClientScopes: params.gatewayClientScopes,
}),
);
}
@@ -716,7 +710,6 @@ export const telegramPlugin = createChatChannelPlugin({
threadId,
silent,
forceDocument,
gatewayClientScopes,
}) => {
const send =
resolveOutboundSendDep<TelegramSendFn>(deps, "telegram") ??
@@ -733,7 +726,6 @@ export const telegramPlugin = createChatChannelPlugin({
threadId,
silent,
forceDocument,
gatewayClientScopes,
}),
});
return attachChannelToResult("telegram", result);
@@ -741,17 +733,7 @@ export const telegramPlugin = createChatChannelPlugin({
},
attachedResults: {
channel: "telegram",
sendText: async ({
cfg,
to,
text,
accountId,
deps,
replyToId,
threadId,
silent,
gatewayClientScopes,
}) =>
sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, silent }) =>
await sendTelegramOutbound({
cfg,
to,
@@ -761,7 +743,6 @@ export const telegramPlugin = createChatChannelPlugin({
replyToId,
threadId,
silent,
gatewayClientScopes,
}),
sendMedia: async ({
cfg,
@@ -774,7 +755,6 @@ export const telegramPlugin = createChatChannelPlugin({
replyToId,
threadId,
silent,
gatewayClientScopes,
}) =>
await sendTelegramOutbound({
cfg,
@@ -787,25 +767,14 @@ export const telegramPlugin = createChatChannelPlugin({
replyToId,
threadId,
silent,
gatewayClientScopes,
}),
sendPoll: async ({
cfg,
to,
poll,
accountId,
threadId,
silent,
isAnonymous,
gatewayClientScopes,
}) =>
sendPoll: async ({ cfg, to, poll, accountId, threadId, silent, isAnonymous }) =>
await getTelegramRuntime().channel.telegram.sendPollTelegram(to, poll, {
cfg,
accountId: accountId ?? undefined,
messageThreadId: parseTelegramThreadId(threadId),
silent: silent ?? undefined,
isAnonymous: isAnonymous ?? undefined,
gatewayClientScopes,
}),
},
},

View File

@@ -30,7 +30,6 @@ function resolveTelegramSendContext(params: {
accountId?: string | null;
replyToId?: string | null;
threadId?: string | number | null;
gatewayClientScopes?: readonly string[];
}): {
send: TelegramSendFn;
baseOpts: {
@@ -40,7 +39,6 @@ function resolveTelegramSendContext(params: {
messageThreadId?: number;
replyToMessageId?: number;
accountId?: string;
gatewayClientScopes?: readonly string[];
};
} {
const send =
@@ -54,7 +52,6 @@ function resolveTelegramSendContext(params: {
messageThreadId: parseTelegramThreadId(params.threadId),
replyToMessageId: parseTelegramReplyToMessageId(params.replyToId),
accountId: params.accountId ?? undefined,
gatewayClientScopes: params.gatewayClientScopes,
},
};
}
@@ -114,23 +111,13 @@ export const telegramOutbound: ChannelOutboundAdapter = {
typeof fallbackLimit === "number" ? Math.min(fallbackLimit, 4096) : 4096,
...createAttachedChannelResultAdapter({
channel: "telegram",
sendText: async ({
cfg,
to,
text,
accountId,
deps,
replyToId,
threadId,
gatewayClientScopes,
}) => {
sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId }) => {
const { send, baseOpts } = resolveTelegramSendContext({
cfg,
deps,
accountId,
replyToId,
threadId,
gatewayClientScopes,
});
return await send(to, text, {
...baseOpts,
@@ -147,7 +134,6 @@ export const telegramOutbound: ChannelOutboundAdapter = {
replyToId,
threadId,
forceDocument,
gatewayClientScopes,
}) => {
const { send, baseOpts } = resolveTelegramSendContext({
cfg,
@@ -155,7 +141,6 @@ export const telegramOutbound: ChannelOutboundAdapter = {
accountId,
replyToId,
threadId,
gatewayClientScopes,
});
return await send(to, text, {
...baseOpts,
@@ -175,7 +160,6 @@ export const telegramOutbound: ChannelOutboundAdapter = {
replyToId,
threadId,
forceDocument,
gatewayClientScopes,
}) => {
const { send, baseOpts } = resolveTelegramSendContext({
cfg,
@@ -183,7 +167,6 @@ export const telegramOutbound: ChannelOutboundAdapter = {
accountId,
replyToId,
threadId,
gatewayClientScopes,
});
const result = await sendTelegramPayloadMessages({
send,

View File

@@ -596,7 +596,6 @@ describe("sendMessageTelegram", () => {
await sendMessageTelegram("https://t.me/mychannel", "hi", {
token: "tok",
api,
gatewayClientScopes: ["operator.write"],
});
expect(getChat).toHaveBeenCalledWith("@mychannel");
@@ -607,7 +606,6 @@ describe("sendMessageTelegram", () => {
expect.objectContaining({
rawTarget: "https://t.me/mychannel",
resolvedChatId: "-100123",
gatewayClientScopes: ["operator.write"],
}),
);
});
@@ -2119,32 +2117,6 @@ describe("editMessageTelegram", () => {
});
describe("sendPollTelegram", () => {
it("propagates gateway client scopes when resolving legacy poll targets", async () => {
const api = {
getChat: vi.fn(async () => ({ id: -100321 })),
sendPoll: vi.fn(async () => ({ message_id: 123, chat: { id: 555 }, poll: { id: "p1" } })),
};
await sendPollTelegram(
"https://t.me/mychannel",
{ question: " Q ", options: [" A ", "B "] },
{
token: "t",
api: api as unknown as Bot["api"],
gatewayClientScopes: ["operator.admin"],
},
);
expect(api.getChat).toHaveBeenCalledWith("@mychannel");
expect(maybePersistResolvedTelegramTarget).toHaveBeenCalledWith(
expect.objectContaining({
rawTarget: "https://t.me/mychannel",
resolvedChatId: "-100321",
gatewayClientScopes: ["operator.admin"],
}),
);
});
it("maps durationSeconds to open_period", async () => {
const api = {
sendPoll: vi.fn(async () => ({ message_id: 123, chat: { id: 555 }, poll: { id: "p1" } })),

View File

@@ -65,7 +65,6 @@ type TelegramSendOpts = {
verbose?: boolean;
mediaUrl?: string;
mediaLocalRoots?: readonly string[];
gatewayClientScopes?: readonly string[];
maxBytes?: number;
api?: TelegramApiOverride;
retry?: RetryConfig;
@@ -316,7 +315,6 @@ async function resolveAndPersistChatId(params: {
lookupTarget: string;
persistTarget: string;
verbose?: boolean;
gatewayClientScopes?: readonly string[];
}): Promise<string> {
const chatId = await resolveChatId(params.lookupTarget, {
api: params.api,
@@ -327,7 +325,6 @@ async function resolveAndPersistChatId(params: {
rawTarget: params.persistTarget,
resolvedChatId: chatId,
verbose: params.verbose,
gatewayClientScopes: params.gatewayClientScopes,
});
return chatId;
}
@@ -635,7 +632,6 @@ export async function sendMessageTelegram(
lookupTarget: target.chatId,
persistTarget: to,
verbose: opts.verbose,
gatewayClientScopes: opts.gatewayClientScopes,
});
const mediaUrl = opts.mediaUrl?.trim();
const mediaMaxBytes =
@@ -1559,7 +1555,6 @@ type TelegramPollOpts = {
verbose?: boolean;
api?: TelegramApiOverride;
retry?: RetryConfig;
gatewayClientScopes?: readonly string[];
/** Message ID to reply to (for threading) */
replyToMessageId?: number;
/** Forum topic thread ID (for forum supergroups) */
@@ -1589,7 +1584,6 @@ export async function sendPollTelegram(
lookupTarget: target.chatId,
persistTarget: to,
verbose: opts.verbose,
gatewayClientScopes: opts.gatewayClientScopes,
});
// Normalize the poll input (validates question, options, maxSelections)

View File

@@ -1,11 +1,9 @@
import { resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution";
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import {
adaptScopedAccountAccessor,
createScopedChannelConfigAdapter,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
import {
buildChannelConfigSchema,
getChatChannelMeta,
@@ -58,39 +56,6 @@ export function formatDuplicateTelegramTokenReason(params: {
);
}
/**
* Returns true when the runtime token resolver (`resolveTelegramToken`) would
* block channel-level fallthrough for the given accountId. This mirrors the
* guard in `token.ts` so that status-check functions (`isConfigured`,
* `unconfiguredReason`, `describeAccount`) stay consistent with the gateway
* runtime behaviour.
*
* The guard fires when:
* 1. The accountId is not the default account, AND
* 2. The config has an explicit `accounts` section with entries, AND
* 3. The accountId is not found in that `accounts` section.
*
* See: https://github.com/openclaw/openclaw/issues/53876
*/
function isBlockedByMultiBotGuard(cfg: OpenClawConfig, accountId: string): boolean {
if (normalizeAccountId(accountId) === DEFAULT_ACCOUNT_ID) {
return false;
}
const accounts = cfg.channels?.telegram?.accounts;
const hasConfiguredAccounts =
!!accounts &&
typeof accounts === "object" &&
!Array.isArray(accounts) &&
Object.keys(accounts).length > 0;
if (!hasConfiguredAccounts) {
return false;
}
// Use resolveNormalizedAccountEntry (same as resolveTelegramToken in token.ts)
// instead of resolveAccountEntry to handle keys that require full normalization
// (e.g. "Carey Notifications" → "carey-notifications").
return !resolveNormalizedAccountEntry(accounts, accountId, normalizeAccountId);
}
export const telegramConfigAdapter = createScopedChannelConfigAdapter<ResolvedTelegramAccount>({
sectionKey: TELEGRAM_CHANNEL,
listAccountIds: listTelegramAccountIds,
@@ -132,32 +97,13 @@ export function createTelegramPluginBase(params: {
config: {
...telegramConfigAdapter,
isConfigured: (account, cfg) => {
// Use inspectTelegramAccount for a complete token resolution that includes
// channel-level fallback paths not available in resolveTelegramAccount.
// This ensures binding-created accountIds that inherit the channel-level
// token are correctly detected as configured.
// See: https://github.com/openclaw/openclaw/issues/53876
if (isBlockedByMultiBotGuard(cfg, account.accountId)) {
return false;
}
const inspected = inspectTelegramAccount({ cfg, accountId: account.accountId });
// Gate on actually available token, not just "configured" — the latter
// includes "configured_unavailable" (unreadable tokenFile, unresolved
// SecretRef) which would pass here but fail at runtime.
if (!inspected.token?.trim()) {
if (!account.token?.trim()) {
return false;
}
return !findTelegramTokenOwnerAccountId({ cfg, accountId: account.accountId });
},
unconfiguredReason: (account, cfg) => {
if (isBlockedByMultiBotGuard(cfg, account.accountId)) {
return `not configured: unknown accountId "${account.accountId}" in multi-bot setup`;
}
const inspected = inspectTelegramAccount({ cfg, accountId: account.accountId });
if (!inspected.token?.trim()) {
if (inspected.tokenStatus === "configured_unavailable") {
return `not configured: token ${inspected.tokenSource} is configured but unavailable`;
}
if (!account.token?.trim()) {
return "not configured";
}
const ownerAccountId = findTelegramTokenOwnerAccountId({
@@ -172,27 +118,15 @@ export function createTelegramPluginBase(params: {
ownerAccountId,
});
},
describeAccount: (account, cfg) => {
if (isBlockedByMultiBotGuard(cfg, account.accountId)) {
return {
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: false,
tokenSource: "none" as const,
};
}
const inspected = inspectTelegramAccount({ cfg, accountId: account.accountId });
return {
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured:
!!inspected.token?.trim() &&
!findTelegramTokenOwnerAccountId({ cfg, accountId: account.accountId }),
tokenSource: inspected.tokenSource,
};
},
describeAccount: (account, cfg) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured:
Boolean(account.token?.trim()) &&
!findTelegramTokenOwnerAccountId({ cfg, accountId: account.accountId }),
tokenSource: account.tokenSource,
}),
},
setup: params.setup,
}) as Pick<

View File

@@ -1,216 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../src/config/config.js";
const readConfigFileSnapshotForWrite = vi.fn();
const writeConfigFile = vi.fn();
const loadCronStore = vi.fn();
const resolveCronStorePath = vi.fn();
const saveCronStore = vi.fn();
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
readConfigFileSnapshotForWrite,
writeConfigFile,
loadCronStore,
resolveCronStorePath,
saveCronStore,
};
});
describe("maybePersistResolvedTelegramTarget", () => {
let maybePersistResolvedTelegramTarget: typeof import("./target-writeback.js").maybePersistResolvedTelegramTarget;
beforeEach(async () => {
vi.resetModules();
({ maybePersistResolvedTelegramTarget } = await import("./target-writeback.js"));
readConfigFileSnapshotForWrite.mockReset();
writeConfigFile.mockReset();
loadCronStore.mockReset();
resolveCronStorePath.mockReset();
saveCronStore.mockReset();
resolveCronStorePath.mockReturnValue("/tmp/cron/jobs.json");
});
it("skips writeback when target is already numeric", async () => {
await maybePersistResolvedTelegramTarget({
cfg: {} as OpenClawConfig,
rawTarget: "-100123",
resolvedChatId: "-100123",
});
expect(readConfigFileSnapshotForWrite).not.toHaveBeenCalled();
expect(loadCronStore).not.toHaveBeenCalled();
});
it("skips config and cron writeback for gateway callers missing operator.admin", async () => {
await maybePersistResolvedTelegramTarget({
cfg: {
cron: { store: "/tmp/cron/jobs.json" },
} as OpenClawConfig,
rawTarget: "t.me/mychannel",
resolvedChatId: "-100123",
gatewayClientScopes: ["operator.write"],
});
expect(readConfigFileSnapshotForWrite).not.toHaveBeenCalled();
expect(writeConfigFile).not.toHaveBeenCalled();
expect(loadCronStore).not.toHaveBeenCalled();
expect(saveCronStore).not.toHaveBeenCalled();
});
it("skips config and cron writeback for gateway callers with an empty scope set", async () => {
await maybePersistResolvedTelegramTarget({
cfg: {
cron: { store: "/tmp/cron/jobs.json" },
} as OpenClawConfig,
rawTarget: "t.me/mychannel",
resolvedChatId: "-100123",
gatewayClientScopes: [],
});
expect(readConfigFileSnapshotForWrite).not.toHaveBeenCalled();
expect(writeConfigFile).not.toHaveBeenCalled();
expect(loadCronStore).not.toHaveBeenCalled();
expect(saveCronStore).not.toHaveBeenCalled();
});
it("writes back matching config and cron targets for gateway callers with operator.admin", async () => {
readConfigFileSnapshotForWrite.mockResolvedValue({
snapshot: {
config: {
channels: {
telegram: {
defaultTo: "t.me/mychannel",
accounts: {
alerts: {
defaultTo: "@mychannel",
},
},
},
},
},
},
writeOptions: { expectedConfigPath: "/tmp/openclaw.json" },
});
loadCronStore.mockResolvedValue({
version: 1,
jobs: [
{ id: "a", delivery: { channel: "telegram", to: "https://t.me/mychannel" } },
{ id: "b", delivery: { channel: "slack", to: "C123" } },
],
});
await maybePersistResolvedTelegramTarget({
cfg: {
cron: { store: "/tmp/cron/jobs.json" },
} as OpenClawConfig,
rawTarget: "t.me/mychannel",
resolvedChatId: "-100123",
gatewayClientScopes: ["operator.admin"],
});
expect(writeConfigFile).toHaveBeenCalledTimes(1);
expect(writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
channels: {
telegram: {
defaultTo: "-100123",
accounts: {
alerts: {
defaultTo: "-100123",
},
},
},
},
}),
expect.objectContaining({ expectedConfigPath: "/tmp/openclaw.json" }),
);
expect(saveCronStore).toHaveBeenCalledTimes(1);
expect(saveCronStore).toHaveBeenCalledWith(
"/tmp/cron/jobs.json",
expect.objectContaining({
jobs: [
{ id: "a", delivery: { channel: "telegram", to: "-100123" } },
{ id: "b", delivery: { channel: "slack", to: "C123" } },
],
}),
);
});
it("preserves topic suffix style in writeback target", async () => {
readConfigFileSnapshotForWrite.mockResolvedValue({
snapshot: {
config: {
channels: {
telegram: {
defaultTo: "t.me/mychannel:topic:9",
},
},
},
},
writeOptions: {},
});
loadCronStore.mockResolvedValue({ version: 1, jobs: [] });
await maybePersistResolvedTelegramTarget({
cfg: {} as OpenClawConfig,
rawTarget: "t.me/mychannel:topic:9",
resolvedChatId: "-100123",
});
expect(writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
channels: {
telegram: {
defaultTo: "-100123:topic:9",
},
},
}),
expect.any(Object),
);
});
it("matches username targets case-insensitively", async () => {
readConfigFileSnapshotForWrite.mockResolvedValue({
snapshot: {
config: {
channels: {
telegram: {
defaultTo: "https://t.me/mychannel",
},
},
},
},
writeOptions: {},
});
loadCronStore.mockResolvedValue({
version: 1,
jobs: [{ id: "a", delivery: { channel: "telegram", to: "https://t.me/mychannel" } }],
});
await maybePersistResolvedTelegramTarget({
cfg: {} as OpenClawConfig,
rawTarget: "@MyChannel",
resolvedChatId: "-100123",
});
expect(writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
channels: {
telegram: {
defaultTo: "-100123",
},
},
}),
expect.any(Object),
);
expect(saveCronStore).toHaveBeenCalledWith(
"/tmp/cron/jobs.json",
expect.objectContaining({
jobs: [{ id: "a", delivery: { channel: "telegram", to: "-100123" } }],
}),
);
});
});

View File

@@ -16,7 +16,6 @@ import {
} from "./targets.js";
const writebackLogger = createSubsystemLogger("telegram/target-writeback");
const TELEGRAM_ADMIN_SCOPE = "operator.admin";
function asObjectRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -142,7 +141,6 @@ export async function maybePersistResolvedTelegramTarget(params: {
rawTarget: string;
resolvedChatId: string;
verbose?: boolean;
gatewayClientScopes?: readonly string[];
}): Promise<void> {
const raw = params.rawTarget.trim();
if (!raw) {
@@ -156,15 +154,6 @@ export async function maybePersistResolvedTelegramTarget(params: {
return;
}
const { matchKey, resolvedTarget } = rewrite;
if (
Array.isArray(params.gatewayClientScopes) &&
!params.gatewayClientScopes.includes(TELEGRAM_ADMIN_SCOPE)
) {
writebackLogger.warn(
`skipping Telegram target writeback for ${raw} because gateway caller is missing ${TELEGRAM_ADMIN_SCOPE}`,
);
return;
}
try {
const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();

View File

@@ -236,42 +236,6 @@ describe("resolveTelegramToken", () => {
/channels\.telegram\.botToken: unresolved SecretRef/i,
);
});
// Regression: https://github.com/openclaw/openclaw/issues/53876
// Binding-created accountIds should inherit the channel-level token in
// single-bot setups (no accounts section).
it("falls through to channel-level token for binding-created accountId without accounts section", () => {
const cfg = {
channels: {
telegram: {
botToken: "channel-level-token",
enabled: true,
},
},
} as OpenClawConfig;
const res = resolveTelegramToken(cfg, { accountId: "bot-main" });
expect(res.token).toBe("channel-level-token");
expect(res.source).toBe("config");
});
it("still blocks fallthrough for unknown accountId when accounts section exists", () => {
vi.stubEnv("TELEGRAM_BOT_TOKEN", "");
const cfg = {
channels: {
telegram: {
botToken: "wrong-bot-token",
accounts: {
knownBot: { botToken: "known-bot-token" },
},
},
},
} as OpenClawConfig;
const res = resolveTelegramToken(cfg, { accountId: "unknownBot" });
expect(res.token).toBe("");
expect(res.source).toBe("none");
});
});
describe("telegram update offset store", () => {

View File

@@ -39,28 +39,13 @@ export function resolveTelegramToken(
);
// When a non-default accountId is explicitly specified but not found in config,
// decide whether to fall through to channel-level defaults based on whether
// the config has an explicit accounts section (multi-bot setup).
//
// Multi-bot: accounts section exists with entries → block fallthrough to prevent
// routing via the wrong bot's token.
//
// Single-bot: no accounts section (or empty) → allow fallthrough so that
// binding-created accountIds inherit the channel-level token.
// See: https://github.com/openclaw/openclaw/issues/53876
// return empty immediately — do NOT fall through to channel-level defaults,
// which would silently route the message via the wrong bot's token.
if (accountId !== DEFAULT_ACCOUNT_ID && !accountCfg) {
const accounts = telegramCfg?.accounts;
const hasConfiguredAccounts =
!!accounts &&
typeof accounts === "object" &&
!Array.isArray(accounts) &&
Object.keys(accounts).length > 0;
if (hasConfiguredAccounts) {
opts.logMissingFile?.(
`channels.telegram.accounts: unknown accountId "${accountId}" — not found in config, refusing channel-level fallback`,
);
return { token: "", source: "none" };
}
opts.logMissingFile?.(
`channels.telegram.accounts: unknown accountId "${accountId}" — not found in config, refusing channel-level fallback`,
);
return { token: "", source: "none" };
}
const accountTokenFile = accountCfg?.tokenFile?.trim();

View File

@@ -11,7 +11,7 @@ describe("twilioApiRequest", () => {
it("posts form bodies with basic auth and parses json", async () => {
globalThis.fetch = vi.fn(async () => {
return new Response(JSON.stringify({ sid: "CA123" }), { status: 200 });
}) as unknown as typeof fetch;
}) as typeof fetch;
await expect(
twilioApiRequest({
@@ -47,7 +47,7 @@ describe("twilioApiRequest", () => {
new Response(null, { status: 204 }),
new Response("missing", { status: 404 }),
];
globalThis.fetch = vi.fn(async () => responses.shift()!) as unknown as typeof fetch;
globalThis.fetch = vi.fn(async () => responses.shift()!) as typeof fetch;
await expect(
twilioApiRequest({
@@ -74,7 +74,7 @@ describe("twilioApiRequest", () => {
it("throws twilio api errors for non-ok responses", async () => {
globalThis.fetch = vi.fn(
async () => new Response("bad request", { status: 400 }),
) as unknown as typeof fetch;
) as typeof fetch;
await expect(
twilioApiRequest({

View File

@@ -21,15 +21,6 @@ function expectResolutionError(params: ResolveParams) {
expect(result.error.message).toContain("WhatsApp");
}
function expectResolutionErrorMessage(params: ResolveParams, expectedMessage: string) {
const result = resolveWhatsAppOutboundTarget(params);
expect(result.ok).toBe(false);
if (result.ok) {
throw new Error("expected resolution to fail");
}
expect(result.error.message).toBe(expectedMessage);
}
function expectResolutionOk(params: ResolveParams, expectedTarget: string) {
const result = resolveWhatsAppOutboundTarget(params);
expect(result).toEqual({ ok: true, to: expectedTarget });
@@ -148,30 +139,7 @@ describe("resolveWhatsAppOutboundTarget", () => {
it("denies message when target is not in allowList", () => {
mockNormalizedDirectMessage(PRIMARY_TARGET, SECONDARY_TARGET);
expectResolutionErrorMessage(
{
to: PRIMARY_TARGET,
allowFrom: [SECONDARY_TARGET],
mode: "implicit",
},
`Target "${SECONDARY_TARGET}" is not listed in the configured WhatsApp allowFrom policy.`,
);
});
it("uses the normalized target in the allowFrom error message", () => {
vi.mocked(normalize.normalizeWhatsAppTarget)
.mockReturnValueOnce(SECONDARY_TARGET)
.mockReturnValueOnce(PRIMARY_TARGET);
vi.mocked(normalize.isWhatsAppGroupJid).mockReturnValueOnce(false);
expectResolutionErrorMessage(
{
to: "(123) 456-7890",
allowFrom: [SECONDARY_TARGET],
mode: "implicit",
},
`Target "${PRIMARY_TARGET}" is not listed in the configured WhatsApp allowFrom policy.`,
);
expectDeniedForTarget({ allowFrom: [SECONDARY_TARGET], mode: "implicit" });
});
it("handles mixed numeric and string allowList entries", () => {

View File

@@ -5,10 +5,6 @@ export type WhatsAppOutboundTargetResolution =
| { ok: true; to: string }
| { ok: false; error: Error };
function whatsappAllowFromPolicyError(target: string): Error {
return new Error(`Target "${target}" is not listed in the configured WhatsApp allowFrom policy.`);
}
export function resolveWhatsAppOutboundTarget(params: {
to: string | null | undefined;
allowFrom: Array<string | number> | null | undefined;
@@ -43,7 +39,7 @@ export function resolveWhatsAppOutboundTarget(params: {
}
return {
ok: false,
error: whatsappAllowFromPolicyError(normalizedTo),
error: missingTargetError("WhatsApp", "<E.164|group JID>"),
};
}

View File

@@ -1,6 +1,6 @@
{
"name": "openclaw",
"version": "2026.3.24",
"version": "2026.3.24-beta.1",
"description": "Multi-channel AI gateway with extensible messaging integrations",
"keywords": [],
"homepage": "https://github.com/openclaw/openclaw#readme",
@@ -541,10 +541,6 @@
"types": "./dist/plugin-sdk/twitch.d.ts",
"default": "./dist/plugin-sdk/twitch.js"
},
"./plugin-sdk/video-generation": {
"types": "./dist/plugin-sdk/video-generation.d.ts",
"default": "./dist/plugin-sdk/video-generation.js"
},
"./plugin-sdk/webhook-ingress": {
"types": "./dist/plugin-sdk/webhook-ingress.d.ts",
"default": "./dist/plugin-sdk/webhook-ingress.js"
@@ -599,9 +595,6 @@
"build:docker": "node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
"build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json",
"build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && pnpm build:plugin-sdk:dts",
"canon:check": "node scripts/canon.mjs check",
"canon:check:json": "node scripts/canon.mjs check --json",
"canon:enforce": "node scripts/canon.mjs enforce --json",
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
"check": "pnpm check:no-conflict-markers && pnpm check:host-env-policy:swift && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
@@ -693,7 +686,7 @@
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/OpenClawProtocol/GatewayModels.swift apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
"protocol:gen": "node --import tsx scripts/protocol-gen.ts",
"protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
"release:check": "pnpm config:docs:check && pnpm plugin-sdk:check-exports && pnpm plugin-sdk:api:check && node scripts/stage-bundled-plugin-runtime-deps.mjs && pnpm ui:build && node --import tsx scripts/release-check.ts",
"release:check": "pnpm config:docs:check && pnpm plugin-sdk:api:check && node scripts/stage-bundled-plugin-runtime-deps.mjs && pnpm ui:build && node --import tsx scripts/release-check.ts",
"release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts",
"release:openclaw:npm:verify-published": "node --import tsx scripts/openclaw-npm-postpublish-verify.ts",
"release:plugins:npm:check": "node --import tsx scripts/plugin-npm-release-check.ts",
@@ -705,26 +698,25 @@
"test:auth:compat": "vitest run --config vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts",
"test:build:singleton": "node scripts/test-built-plugin-singleton.mjs",
"test:changed": "pnpm test -- --changed origin/main",
"test:channels": "node scripts/test-parallel.mjs --surface channels",
"test:channels": "OPENCLAW_TEST_SKIP_DEFAULT=1 OPENCLAW_TEST_INCLUDE_CHANNELS=1 node scripts/test-parallel.mjs",
"test:contracts": "pnpm test:contracts:channels && pnpm test:contracts:plugins",
"test:contracts:channels": "OPENCLAW_TEST_PROFILE=serial pnpm test -- src/channels/plugins/contracts",
"test:contracts:plugins": "OPENCLAW_TEST_PROFILE=serial pnpm test -- src/plugins/contracts",
"test:contracts:channels": "OPENCLAW_TEST_PROFILE=low pnpm test -- src/channels/plugins/contracts",
"test:contracts:plugins": "OPENCLAW_TEST_PROFILE=low pnpm test -- src/plugins/contracts",
"test:coverage": "vitest run --config vitest.unit.config.ts --coverage",
"test:coverage:changed": "vitest run --config vitest.unit.config.ts --coverage --changed origin/main",
"test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:openwebui && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup",
"test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup",
"test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
"test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
"test:docker:gateway-network": "bash scripts/e2e/gateway-network-docker.sh",
"test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
"test:docker:live-models": "bash scripts/test-live-models-docker.sh",
"test:docker:onboard": "bash scripts/e2e/onboard-docker.sh",
"test:docker:openwebui": "bash scripts/e2e/openwebui-docker.sh",
"test:docker:plugins": "bash scripts/e2e/plugins-docker.sh",
"test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:e2e:openshell": "OPENCLAW_E2E_OPENSHELL=1 vitest run --config vitest.e2e.config.ts test/openshell-sandbox.e2e.test.ts",
"test:extension": "node scripts/test-extension.mjs",
"test:extensions": "node scripts/test-parallel.mjs --surface extensions",
"test:extensions": "OPENCLAW_TEST_SKIP_DEFAULT=1 OPENCLAW_TEST_INCLUDE_EXTENSIONS=1 node scripts/test-parallel.mjs",
"test:extensions:memory": "node scripts/profile-extension-memory.mjs",
"test:fast": "vitest run --config vitest.unit.config.ts",
"test:force": "node --import tsx scripts/test-force.ts",
@@ -735,11 +727,13 @@
"test:install:e2e:openai": "OPENCLAW_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh",
"test:install:smoke": "bash scripts/test-install-sh-docker.sh",
"test:live": "OPENCLAW_LIVE_TEST=1 vitest run --config vitest.live.config.ts",
"test:macmini": "OPENCLAW_TEST_PROFILE=macmini node scripts/test-parallel.mjs",
"test:parallels:linux": "bash scripts/e2e/parallels-linux-smoke.sh",
"test:parallels:macos": "bash scripts/e2e/parallels-macos-smoke.sh",
"test:parallels:npm-update": "bash scripts/e2e/parallels-npm-update-smoke.sh",
"test:parallels:windows": "bash scripts/e2e/parallels-windows-smoke.sh",
"test:perf:budget": "node scripts/test-perf-budget.mjs",
"test:perf:find-thread-candidates": "node scripts/test-find-thread-candidates.mjs",
"test:perf:hotspots": "node scripts/test-hotspots.mjs",
"test:perf:imports": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 pnpm test",
"test:perf:imports:changed": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 pnpm test -- --changed origin/main",
@@ -748,7 +742,6 @@
"test:perf:update-memory-hotspots": "node scripts/test-update-memory-hotspots.mjs",
"test:perf:update-timings": "node scripts/test-update-timings.mjs",
"test:sectriage": "pnpm exec vitest run --config vitest.gateway.config.ts && vitest run --config vitest.unit.config.ts --exclude src/daemon/launchd.integration.test.ts --exclude src/process/exec.test.ts",
"test:serial": "node scripts/test-parallel.mjs --profile serial",
"test:startup:memory": "node scripts/check-cli-startup-memory.mjs",
"test:ui": "pnpm lint:ui:no-raw-window-open && pnpm --dir ui test",
"test:voicecall:closedloop": "vitest run extensions/voice-call/src/manager.test.ts extensions/voice-call/src/media-stream.test.ts src/plugins/voice-call.plugin.test.ts --maxWorkers=1",
@@ -843,7 +836,7 @@
"openshell": "0.1.0"
},
"engines": {
"node": ">=22.14.0"
"node": ">=22.16.0"
},
"packageManager": "pnpm@10.32.1",
"pnpm": {

View File

@@ -1,76 +0,0 @@
import { appendFileSync } from "node:fs";
import { buildCIExecutionManifest } from "./test-planner/planner.mjs";
const WORKFLOWS = new Set(["ci", "install-smoke", "ci-bun"]);
const parseArgs = (argv) => {
const parsed = {
workflow: "ci",
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--workflow") {
const nextValue = argv[index + 1] ?? "";
if (!WORKFLOWS.has(nextValue)) {
throw new Error(
`Unsupported --workflow value "${String(nextValue || "<missing>")}". Supported values: ci, install-smoke, ci-bun.`,
);
}
parsed.workflow = nextValue;
index += 1;
}
}
return parsed;
};
const outputPath = process.env.GITHUB_OUTPUT;
if (!outputPath) {
throw new Error("GITHUB_OUTPUT is required");
}
const { workflow } = parseArgs(process.argv.slice(2));
const manifest = buildCIExecutionManifest(undefined, { env: process.env });
const writeOutput = (name, value) => {
appendFileSync(outputPath, `${name}=${value}\n`, "utf8");
};
if (workflow === "ci") {
writeOutput("docs_only", String(manifest.scope.docsOnly));
writeOutput("docs_changed", String(manifest.scope.docsChanged));
writeOutput("run_node", String(manifest.scope.runNode));
writeOutput("run_macos", String(manifest.scope.runMacos));
writeOutput("run_android", String(manifest.scope.runAndroid));
writeOutput("run_skills_python", String(manifest.scope.runSkillsPython));
writeOutput("run_windows", String(manifest.scope.runWindows));
writeOutput("has_changed_extensions", String(manifest.scope.hasChangedExtensions));
writeOutput("changed_extensions_matrix", JSON.stringify(manifest.scope.changedExtensionsMatrix));
writeOutput("run_build_artifacts", String(manifest.jobs.buildArtifacts.enabled));
writeOutput("run_release_check", String(manifest.jobs.releaseCheck.enabled));
writeOutput("run_checks_fast", String(manifest.jobs.checksFast.enabled));
writeOutput("checks_fast_matrix", JSON.stringify(manifest.jobs.checksFast.matrix));
writeOutput("run_checks", String(manifest.jobs.checks.enabled));
writeOutput("checks_matrix", JSON.stringify(manifest.jobs.checks.matrix));
writeOutput("run_extension_fast", String(manifest.jobs.extensionFast.enabled));
writeOutput("extension_fast_matrix", JSON.stringify(manifest.jobs.extensionFast.matrix));
writeOutput("run_check", String(manifest.jobs.check.enabled));
writeOutput("run_check_additional", String(manifest.jobs.checkAdditional.enabled));
writeOutput("run_build_smoke", String(manifest.jobs.buildSmoke.enabled));
writeOutput("run_check_docs", String(manifest.jobs.checkDocs.enabled));
writeOutput("run_skills_python_job", String(manifest.jobs.skillsPython.enabled));
writeOutput("run_checks_windows", String(manifest.jobs.checksWindows.enabled));
writeOutput("checks_windows_matrix", JSON.stringify(manifest.jobs.checksWindows.matrix));
writeOutput("run_macos_node", String(manifest.jobs.macosNode.enabled));
writeOutput("macos_node_matrix", JSON.stringify(manifest.jobs.macosNode.matrix));
writeOutput("run_macos_swift", String(manifest.jobs.macosSwift.enabled));
writeOutput("run_android_job", String(manifest.jobs.android.enabled));
writeOutput("android_matrix", JSON.stringify(manifest.jobs.android.matrix));
writeOutput("required_check_names", JSON.stringify(manifest.requiredCheckNames));
} else if (workflow === "install-smoke") {
writeOutput("docs_only", String(manifest.scope.docsOnly));
writeOutput("run_install_smoke", String(manifest.jobs.installSmoke.enabled));
} else if (workflow === "ci-bun") {
writeOutput("run_bun_checks", String(manifest.jobs.bunChecks.enabled));
writeOutput("bun_checks_matrix", JSON.stringify(manifest.jobs.bunChecks.matrix));
}

View File

@@ -18,7 +18,7 @@ ENV NODE_OPTIONS="--disable-warning=ExperimentalWarning"
USER appuser
WORKDIR /app
COPY --chown=appuser:appuser package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY --chown=appuser:appuser package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc openclaw.mjs ./
COPY --chown=appuser:appuser ui/package.json ./ui/package.json
COPY --chown=appuser:appuser extensions ./extensions
COPY --chown=appuser:appuser patches ./patches
@@ -26,7 +26,7 @@ COPY --chown=appuser:appuser patches ./patches
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/home/appuser/.local/share/pnpm/store,sharing=locked \
pnpm install --frozen-lockfile
COPY --chown=appuser:appuser tsconfig.json tsconfig.plugin-sdk.dts.json tsdown.config.ts vitest.config.ts vitest.e2e.config.ts vitest.performance-config.ts openclaw.mjs ./
COPY --chown=appuser:appuser tsconfig.json tsconfig.plugin-sdk.dts.json tsdown.config.ts vitest.config.ts vitest.e2e.config.ts vitest.performance-config.ts ./
COPY --chown=appuser:appuser src ./src
COPY --chown=appuser:appuser test ./test
COPY --chown=appuser:appuser scripts ./scripts

View File

@@ -1,184 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/live-docker-auth.sh"
IMAGE_NAME="openclaw-openwebui-e2e"
OPENWEBUI_IMAGE="${OPENWEBUI_IMAGE:-ghcr.io/open-webui/open-webui:v0.8.10}"
PROFILE_FILE="${OPENCLAW_PROFILE_FILE:-$HOME/.profile}"
MODEL="${OPENCLAW_OPENWEBUI_MODEL:-openai/gpt-5.4}"
PROMPT_NONCE="OPENWEBUI_DOCKER_E2E_$(date +%s)_$$"
PROMPT="${OPENCLAW_OPENWEBUI_PROMPT:-Reply with exactly this token and nothing else: ${PROMPT_NONCE}}"
PORT="${OPENCLAW_OPENWEBUI_GATEWAY_PORT:-18789}"
WEBUI_PORT="${OPENCLAW_OPENWEBUI_PORT:-8080}"
TOKEN="openwebui-e2e-$(date +%s)-$$"
ADMIN_EMAIL="${OPENCLAW_OPENWEBUI_ADMIN_EMAIL:-openwebui-e2e@example.com}"
ADMIN_PASSWORD="${OPENCLAW_OPENWEBUI_ADMIN_PASSWORD:-OpenWebUI-E2E-Password-$(date +%s)-$$}"
NET_NAME="openclaw-openwebui-e2e-$$"
GW_NAME="openclaw-openwebui-gateway-$$"
OW_NAME="openclaw-openwebui-$$"
PROFILE_MOUNT=()
if [[ -f "$PROFILE_FILE" ]]; then
PROFILE_MOUNT=(-v "$PROFILE_FILE":/home/appuser/.profile:ro)
fi
AUTH_DIRS=()
if [[ -n "${OPENCLAW_DOCKER_AUTH_DIRS:-}" ]]; then
while IFS= read -r auth_dir; do
[[ -n "$auth_dir" ]] || continue
AUTH_DIRS+=("$auth_dir")
done < <(openclaw_live_collect_auth_dirs)
fi
AUTH_DIRS_CSV="$(openclaw_live_join_csv "${AUTH_DIRS[@]}")"
EXTERNAL_AUTH_MOUNTS=()
for auth_dir in "${AUTH_DIRS[@]}"; do
host_path="$HOME/$auth_dir"
if [[ -d "$host_path" ]]; then
EXTERNAL_AUTH_MOUNTS+=(-v "$host_path":/host-auth/"$auth_dir":ro)
fi
done
cleanup() {
docker rm -f "$OW_NAME" >/dev/null 2>&1 || true
docker rm -f "$GW_NAME" >/dev/null 2>&1 || true
docker network rm "$NET_NAME" >/dev/null 2>&1 || true
}
trap cleanup EXIT
echo "Building Docker image..."
docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR"
echo "Pulling Open WebUI image: $OPENWEBUI_IMAGE"
docker pull "$OPENWEBUI_IMAGE" >/dev/null
echo "Creating Docker network..."
docker network create "$NET_NAME" >/dev/null
echo "Starting gateway container..."
docker run -d \
--name "$GW_NAME" \
--network "$NET_NAME" \
-e "OPENCLAW_DOCKER_AUTH_DIRS_RESOLVED=$AUTH_DIRS_CSV" \
-e "OPENCLAW_GATEWAY_TOKEN=$TOKEN" \
-e "OPENCLAW_OPENWEBUI_MODEL=$MODEL" \
-e "OPENCLAW_SKIP_CHANNELS=1" \
-e "OPENCLAW_SKIP_GMAIL_WATCHER=1" \
-e "OPENCLAW_SKIP_CRON=1" \
-e "OPENCLAW_SKIP_CANVAS_HOST=1" \
"${EXTERNAL_AUTH_MOUNTS[@]}" \
"${PROFILE_MOUNT[@]}" \
"$IMAGE_NAME" \
bash -lc '
set -euo pipefail
[ -f "$HOME/.profile" ] && source "$HOME/.profile" || true
IFS="," read -r -a auth_dirs <<<"${OPENCLAW_DOCKER_AUTH_DIRS_RESOLVED:-}"
for auth_dir in "${auth_dirs[@]}"; do
[ -n "$auth_dir" ] || continue
if [ -d "/host-auth/$auth_dir" ]; then
mkdir -p "$HOME/$auth_dir"
cp -R "/host-auth/$auth_dir/." "$HOME/$auth_dir"
chmod -R u+rwX "$HOME/$auth_dir" || true
fi
done
entry=dist/index.mjs
[ -f "$entry" ] || entry=dist/index.js
node "$entry" config set gateway.controlUi.enabled false >/dev/null
node "$entry" config set gateway.mode local >/dev/null
node "$entry" config set gateway.bind lan >/dev/null
node "$entry" config set gateway.auth.mode token >/dev/null
node "$entry" config set gateway.auth.token "$OPENCLAW_GATEWAY_TOKEN" >/dev/null
node "$entry" config set gateway.http.endpoints.chatCompletions.enabled true --strict-json >/dev/null
node "$entry" config set agents.defaults.model.primary "$OPENCLAW_OPENWEBUI_MODEL" >/dev/null
exec node "$entry" gateway --port '"$PORT"' --bind lan --allow-unconfigured > /tmp/openwebui-gateway.log 2>&1
'
echo "Waiting for gateway HTTP surface..."
gateway_ready=0
for _ in $(seq 1 60); do
if [ "$(docker inspect -f '{{.State.Running}}' "$GW_NAME" 2>/dev/null || echo false)" != "true" ]; then
break
fi
if docker exec "$GW_NAME" bash -lc "node --input-type=module -e '
const res = await fetch(\"http://127.0.0.1:$PORT/v1/models\", {
headers: { authorization: \"Bearer $TOKEN\" },
}).catch(() => null);
process.exit(res?.status === 200 ? 0 : 1);
' >/dev/null 2>&1"; then
gateway_ready=1
break
fi
sleep 1
done
if [ "$gateway_ready" -ne 1 ]; then
echo "Gateway failed to start"
docker logs "$GW_NAME" 2>&1 | tail -n 200 || true
exit 1
fi
echo "Starting Open WebUI container..."
docker run -d \
--name "$OW_NAME" \
--network "$NET_NAME" \
-e ENV=prod \
-e WEBUI_NAME="OpenClaw E2E" \
-e WEBUI_SECRET_KEY="openclaw-openwebui-e2e-secret" \
-e OFFLINE_MODE=True \
-e ENABLE_VERSION_UPDATE_CHECK=False \
-e ENABLE_PERSISTENT_CONFIG=False \
-e ENABLE_OLLAMA_API=False \
-e ENABLE_OPENAI_API=True \
-e OPENAI_API_BASE_URLS="http://$GW_NAME:$PORT/v1" \
-e OPENAI_API_KEY="$TOKEN" \
-e OPENAI_API_KEYS="$TOKEN" \
-e RAG_EMBEDDING_MODEL_AUTO_UPDATE=False \
-e RAG_RERANKING_MODEL_AUTO_UPDATE=False \
-e WEBUI_ADMIN_EMAIL="$ADMIN_EMAIL" \
-e WEBUI_ADMIN_PASSWORD="$ADMIN_PASSWORD" \
-e WEBUI_ADMIN_NAME="OpenClaw E2E" \
-e ENABLE_SIGNUP=False \
-e DEFAULT_MODELS="openclaw/default" \
"$OPENWEBUI_IMAGE" >/dev/null
echo "Waiting for Open WebUI..."
ow_ready=0
for _ in $(seq 1 90); do
if [ "$(docker inspect -f '{{.State.Running}}' "$OW_NAME" 2>/dev/null || echo false)" != "true" ]; then
break
fi
if docker exec "$GW_NAME" bash -lc "node --input-type=module -e '
const res = await fetch(\"http://$OW_NAME:$WEBUI_PORT/\").catch(() => null);
process.exit(res && res.status < 500 ? 0 : 1);
' >/dev/null 2>&1"; then
ow_ready=1
break
fi
sleep 1
done
if [ "$ow_ready" -ne 1 ]; then
echo "Open WebUI failed to start"
docker logs "$OW_NAME" 2>&1 | tail -n 200 || true
exit 1
fi
echo "Running Open WebUI -> OpenClaw smoke..."
docker exec \
-e "OPENWEBUI_BASE_URL=http://$OW_NAME:$WEBUI_PORT" \
-e "OPENWEBUI_ADMIN_EMAIL=$ADMIN_EMAIL" \
-e "OPENWEBUI_ADMIN_PASSWORD=$ADMIN_PASSWORD" \
-e "OPENWEBUI_EXPECTED_NONCE=$PROMPT_NONCE" \
-e "OPENWEBUI_PROMPT=$PROMPT" \
"$GW_NAME" \
node /app/scripts/e2e/openwebui-probe.mjs
echo "Open WebUI container logs:"
docker logs "$OW_NAME" 2>&1 | tail -n 80 || true
echo "OK"

View File

@@ -1,95 +0,0 @@
const baseUrl = process.env.OPENWEBUI_BASE_URL ?? "";
const email = process.env.OPENWEBUI_ADMIN_EMAIL ?? "";
const password = process.env.OPENWEBUI_ADMIN_PASSWORD ?? "";
const expectedNonce = process.env.OPENWEBUI_EXPECTED_NONCE ?? "";
const prompt = process.env.OPENWEBUI_PROMPT ?? "";
if (!baseUrl || !email || !password || !expectedNonce || !prompt) {
throw new Error("Missing required OPENWEBUI_* environment variables");
}
function getCookieHeader(res) {
const raw = res.headers.get("set-cookie");
if (!raw) {
return "";
}
return raw
.split(/,(?=[^;]+=[^;]+)/g)
.map((part) => part.split(";", 1)[0]?.trim())
.filter(Boolean)
.join("; ");
}
function buildAuthHeaders(token, cookie) {
const headers = {};
if (token) {
headers.authorization = `Bearer ${token}`;
}
if (cookie) {
headers.cookie = cookie;
}
return headers;
}
const signinRes = await fetch(`${baseUrl}/api/v1/auths/signin`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!signinRes.ok) {
const body = await signinRes.text();
throw new Error(`signin failed: HTTP ${signinRes.status} ${body}`);
}
const signinJson = await signinRes.json();
const token =
signinJson?.token ?? signinJson?.access_token ?? signinJson?.jwt ?? signinJson?.data?.token ?? "";
const cookie = getCookieHeader(signinRes);
const authHeaders = {
...buildAuthHeaders(token, cookie),
accept: "application/json",
};
const modelsRes = await fetch(`${baseUrl}/api/models`, { headers: authHeaders });
if (!modelsRes.ok) {
throw new Error(`/api/models failed: HTTP ${modelsRes.status} ${await modelsRes.text()}`);
}
const modelsJson = await modelsRes.json();
const models = Array.isArray(modelsJson)
? modelsJson
: Array.isArray(modelsJson?.data)
? modelsJson.data
: Array.isArray(modelsJson?.models)
? modelsJson.models
: [];
const modelIds = models
.map((entry) => entry?.id ?? entry?.model ?? entry?.name)
.filter((value) => typeof value === "string");
const targetModel =
modelIds.find((id) => id === "openclaw/default") ?? modelIds.find((id) => id === "openclaw");
if (!targetModel) {
throw new Error(`openclaw model missing from Open WebUI model list: ${JSON.stringify(modelIds)}`);
}
const chatRes = await fetch(`${baseUrl}/api/chat/completions`, {
method: "POST",
headers: {
...authHeaders,
"content-type": "application/json",
},
body: JSON.stringify({
model: targetModel,
messages: [{ role: "user", content: prompt }],
}),
});
if (!chatRes.ok) {
throw new Error(`/api/chat/completions failed: HTTP ${chatRes.status} ${await chatRes.text()}`);
}
const chatJson = await chatRes.json();
const reply =
chatJson?.choices?.[0]?.message?.content ?? chatJson?.message?.content ?? chatJson?.content ?? "";
if (typeof reply !== "string" || !reply.includes(expectedNonce)) {
throw new Error(`chat reply missing nonce: ${JSON.stringify(reply)}`);
}
console.log(JSON.stringify({ ok: true, model: targetModel, reply }, null, 2));

View File

@@ -18,7 +18,7 @@ NC='\033[0m' # No Color
DEFAULT_TAGLINE="All your chats, one OpenClaw."
NODE_DEFAULT_MAJOR=24
NODE_MIN_MAJOR=22
NODE_MIN_MINOR=14
NODE_MIN_MINOR=16
NODE_MIN_VERSION="${NODE_MIN_MAJOR}.${NODE_MIN_MINOR}"
ORIGINAL_PATH="${PATH:-}"

View File

@@ -125,7 +125,6 @@
"tlon",
"tool-send",
"twitch",
"video-generation",
"webhook-ingress",
"webhook-path",
"web-media",

View File

@@ -0,0 +1,195 @@
import { spawnSync } from "node:child_process";
import path from "node:path";
import { pathToFileURL } from "node:url";
import {
booleanFlag,
floatFlag,
intFlag,
parseFlagArgs,
readEnvNumber,
stringFlag,
} from "./lib/arg-utils.mjs";
import { formatMs } from "./lib/vitest-report-cli-utils.mjs";
import { loadTestRunnerBehavior, loadUnitTimingManifest } from "./test-runner-manifest.mjs";
export function parseArgs(argv) {
const envLimit = readEnvNumber("OPENCLAW_TEST_THREAD_CANDIDATE_LIMIT");
return parseFlagArgs(
argv,
{
config: "vitest.unit.config.ts",
limit: Number.isFinite(envLimit) ? Math.max(1, Math.floor(envLimit)) : 20,
minDurationMs: readEnvNumber("OPENCLAW_TEST_THREAD_CANDIDATE_MIN_DURATION_MS") ?? 250,
minGainMs: readEnvNumber("OPENCLAW_TEST_THREAD_CANDIDATE_MIN_GAIN_MS") ?? 100,
minGainPct: readEnvNumber("OPENCLAW_TEST_THREAD_CANDIDATE_MIN_GAIN_PCT") ?? 10,
json: false,
files: [],
},
[
stringFlag("--config", "config"),
intFlag("--limit", "limit", { min: 1 }),
floatFlag("--min-duration-ms", "minDurationMs", { min: 0 }),
floatFlag("--min-gain-ms", "minGainMs", { min: 0 }),
floatFlag("--min-gain-pct", "minGainPct", { min: 0, includeMin: false }),
booleanFlag("--json", "json"),
],
{
ignoreDoubleDash: true,
onUnhandledArg(arg, args) {
if (arg.startsWith("-")) {
throw new Error(`Unknown option: ${arg}`);
}
args.files.push(arg);
return "handled";
},
},
);
}
export function getExistingThreadCandidateExclusions(behavior) {
return new Set([
...(behavior.base?.threadPinned ?? []).map((entry) => entry.file),
...(behavior.base?.threadSingleton ?? []).map((entry) => entry.file),
...(behavior.unit?.isolated ?? []).map((entry) => entry.file),
...(behavior.unit?.threadPinned ?? []).map((entry) => entry.file),
...(behavior.unit?.threadSingleton ?? []).map((entry) => entry.file),
]);
}
export function selectThreadCandidateFiles({
files,
timings,
exclude = new Set(),
limit,
minDurationMs,
includeUnknownDuration = false,
}) {
return files
.map((file) => ({
file,
durationMs: timings.files[file]?.durationMs ?? null,
}))
.filter((entry) => !exclude.has(entry.file))
.filter((entry) =>
entry.durationMs === null ? includeUnknownDuration : entry.durationMs >= minDurationMs,
)
.toSorted((a, b) => b.durationMs - a.durationMs)
.slice(0, limit)
.map((entry) => entry.file);
}
export function summarizeThreadBenchmark({ file, forks, threads, minGainMs, minGainPct }) {
const forkOk = forks.exitCode === 0;
const threadOk = threads.exitCode === 0;
const gainMs = forks.elapsedMs - threads.elapsedMs;
const gainPct = forks.elapsedMs > 0 ? (gainMs / forks.elapsedMs) * 100 : 0;
const recommended =
forkOk &&
threadOk &&
gainMs >= minGainMs &&
gainPct >= minGainPct &&
threads.elapsedMs < forks.elapsedMs;
return {
file,
forks,
threads,
gainMs,
gainPct,
recommended,
};
}
function benchmarkFile({ config, file, pool }) {
const startedAt = process.hrtime.bigint();
const run = spawnSync("pnpm", ["vitest", "run", "--config", config, `--pool=${pool}`, file], {
encoding: "utf8",
env: process.env,
maxBuffer: 20 * 1024 * 1024,
});
const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
return {
pool,
exitCode: run.status ?? 1,
elapsedMs,
stderr: run.stderr ?? "",
stdout: run.stdout ?? "",
};
}
function buildOutput(results) {
return results.map((result) => ({
file: result.file,
forksMs: Math.round(result.forks.elapsedMs),
threadsMs: Math.round(result.threads.elapsedMs),
gainMs: Math.round(result.gainMs),
gainPct: Number(result.gainPct.toFixed(1)),
forksExitCode: result.forks.exitCode,
threadsExitCode: result.threads.exitCode,
recommended: result.recommended,
}));
}
async function main() {
const opts = parseArgs(process.argv.slice(2));
const behavior = loadTestRunnerBehavior();
const timings = loadUnitTimingManifest();
const exclude = getExistingThreadCandidateExclusions(behavior);
const inputFiles = opts.files.length > 0 ? opts.files : Object.keys(timings.files);
const candidates = selectThreadCandidateFiles({
files: inputFiles,
timings,
exclude,
limit: opts.limit,
minDurationMs: opts.minDurationMs,
includeUnknownDuration: opts.files.length > 0,
});
const results = [];
for (const file of candidates) {
const forks = benchmarkFile({ config: opts.config, file, pool: "forks" });
const threads = benchmarkFile({ config: opts.config, file, pool: "threads" });
results.push(
summarizeThreadBenchmark({
file,
forks,
threads,
minGainMs: opts.minGainMs,
minGainPct: opts.minGainPct,
}),
);
}
if (opts.json) {
console.log(JSON.stringify(buildOutput(results), null, 2));
return;
}
console.log(
`[test-find-thread-candidates] tested=${String(results.length)} minGain=${formatMs(
opts.minGainMs,
0,
)} minGainPct=${String(opts.minGainPct)}%`,
);
for (const result of results) {
const status = result.recommended
? "recommend"
: result.forks.exitCode !== 0
? "forks-failed"
: result.threads.exitCode !== 0
? "threads-failed"
: "skip";
console.log(
`${status.padEnd(14, " ")} ${result.file} forks=${formatMs(
result.forks.elapsedMs,
0,
)} threads=${formatMs(result.threads.elapsedMs, 0)} gain=${formatMs(result.gainMs, 0)} (${result.gainPct.toFixed(1)}%)`,
);
}
}
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,187 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import { channelTestPrefixes } from "../../vitest.channel-paths.mjs";
import { isUnitConfigTestFile } from "../../vitest.unit-paths.mjs";
import { dedupeFilesPreserveOrder, loadTestRunnerBehavior } from "../test-runner-manifest.mjs";
const baseConfigPrefixes = ["src/agents/", "src/auto-reply/", "src/commands/", "test/", "ui/"];
export const normalizeRepoPath = (value) => value.split(path.sep).join("/");
const toRepoRelativePath = (value) => {
const relativePath = normalizeRepoPath(path.relative(process.cwd(), path.resolve(value)));
return relativePath.startsWith("../") || relativePath === ".." ? null : relativePath;
};
const walkTestFiles = (rootDir) => {
if (!fs.existsSync(rootDir)) {
return [];
}
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(rootDir, entry.name);
if (entry.isDirectory()) {
files.push(...walkTestFiles(fullPath));
continue;
}
if (!entry.isFile()) {
continue;
}
if (
fullPath.endsWith(".test.ts") ||
fullPath.endsWith(".live.test.ts") ||
fullPath.endsWith(".e2e.test.ts")
) {
files.push(normalizeRepoPath(fullPath));
}
}
return files;
};
export function loadTestCatalog() {
const behaviorManifest = loadTestRunnerBehavior();
const existingFiles = (entries) =>
entries.map((entry) => entry.file).filter((file) => fs.existsSync(file));
const existingUnitConfigFiles = (entries) => existingFiles(entries).filter(isUnitConfigTestFile);
const baseThreadPinnedFiles = existingFiles(behaviorManifest.base?.threadPinned ?? []);
const channelIsolatedManifestFiles = existingFiles(behaviorManifest.channels?.isolated ?? []);
const channelIsolatedPrefixes = behaviorManifest.channels?.isolatedPrefixes ?? [];
const extensionForkIsolatedFiles = existingFiles(behaviorManifest.extensions?.isolated ?? []);
const unitForkIsolatedFiles = existingUnitConfigFiles(behaviorManifest.unit.isolated);
const unitThreadPinnedFiles = existingUnitConfigFiles(behaviorManifest.unit.threadPinned);
const unitBehaviorOverrideSet = new Set([...unitForkIsolatedFiles, ...unitThreadPinnedFiles]);
const allKnownTestFiles = [
...new Set([
...walkTestFiles("src"),
...walkTestFiles("extensions"),
...walkTestFiles("test"),
...walkTestFiles(path.join("ui", "src", "ui")),
]),
];
const channelIsolatedFiles = dedupeFilesPreserveOrder([
...channelIsolatedManifestFiles,
...allKnownTestFiles.filter((file) =>
channelIsolatedPrefixes.some((prefix) => file.startsWith(prefix)),
),
]);
const channelIsolatedFileSet = new Set(channelIsolatedFiles);
const extensionForkIsolatedFileSet = new Set(extensionForkIsolatedFiles);
const baseThreadPinnedFileSet = new Set(baseThreadPinnedFiles);
const unitThreadPinnedFileSet = new Set(unitThreadPinnedFiles);
const unitForkIsolatedFileSet = new Set(unitForkIsolatedFiles);
const classifyTestFile = (fileFilter, options = {}) => {
const normalizedFile = normalizeRepoPath(fileFilter);
const reasons = [];
const isolated =
options.unitMemoryIsolatedFiles?.includes(normalizedFile) ||
unitForkIsolatedFileSet.has(normalizedFile) ||
extensionForkIsolatedFileSet.has(normalizedFile) ||
channelIsolatedFileSet.has(normalizedFile);
if (options.unitMemoryIsolatedFiles?.includes(normalizedFile)) {
reasons.push("unit-memory-isolated");
}
if (unitForkIsolatedFileSet.has(normalizedFile)) {
reasons.push("unit-isolated-manifest");
}
if (extensionForkIsolatedFileSet.has(normalizedFile)) {
reasons.push("extensions-isolated-manifest");
}
if (channelIsolatedFileSet.has(normalizedFile)) {
reasons.push("channels-isolated-rule");
}
let surface = "base";
if (isUnitConfigTestFile(normalizedFile)) {
surface = "unit";
} else if (normalizedFile.endsWith(".live.test.ts")) {
surface = "live";
} else if (normalizedFile.endsWith(".e2e.test.ts")) {
surface = "e2e";
} else if (channelTestPrefixes.some((prefix) => normalizedFile.startsWith(prefix))) {
surface = "channels";
} else if (normalizedFile.startsWith("extensions/")) {
surface = "extensions";
} else if (normalizedFile.startsWith("src/gateway/")) {
surface = "gateway";
} else if (baseConfigPrefixes.some((prefix) => normalizedFile.startsWith(prefix))) {
surface = "base";
} else if (normalizedFile.startsWith("src/")) {
surface = "unit";
}
if (surface === "unit") {
reasons.push("unit-surface");
} else if (surface !== "base") {
reasons.push(`${surface}-surface`);
} else {
reasons.push("base-surface");
}
const legacyBasePinned = baseThreadPinnedFileSet.has(normalizedFile);
if (legacyBasePinned) {
reasons.push("base-pinned-manifest");
}
if (unitThreadPinnedFileSet.has(normalizedFile)) {
reasons.push("unit-pinned-manifest");
}
return {
file: normalizedFile,
surface,
isolated,
legacyBasePinned,
reasons,
};
};
const resolveFilterMatches = (fileFilter) => {
const normalizedFilter = normalizeRepoPath(fileFilter);
const repoRelativeFilter = toRepoRelativePath(fileFilter);
if (fs.existsSync(fileFilter)) {
const stats = fs.statSync(fileFilter);
if (stats.isFile()) {
if (repoRelativeFilter && allKnownTestFiles.includes(repoRelativeFilter)) {
return [repoRelativeFilter];
}
throw new Error(`Explicit path ${fileFilter} is not a known test file.`);
}
if (stats.isDirectory()) {
if (!repoRelativeFilter) {
throw new Error(`Explicit path ${fileFilter} is outside the repo test roots.`);
}
const prefix = repoRelativeFilter.endsWith("/")
? repoRelativeFilter
: `${repoRelativeFilter}/`;
const matches = allKnownTestFiles.filter((file) => file.startsWith(prefix));
if (matches.length === 0) {
throw new Error(`Explicit path ${fileFilter} does not contain known test files.`);
}
return matches;
}
}
if (/[*?[\]{}]/.test(normalizedFilter)) {
return allKnownTestFiles.filter((file) => path.matchesGlob(file, normalizedFilter));
}
return allKnownTestFiles.filter((file) => file.includes(normalizedFilter));
};
return {
allKnownTestFiles,
allKnownUnitFiles: allKnownTestFiles.filter((file) => isUnitConfigTestFile(file)),
baseThreadPinnedFiles,
channelIsolatedFiles,
channelIsolatedFileSet,
channelTestPrefixes,
extensionForkIsolatedFiles,
extensionForkIsolatedFileSet,
unitBehaviorOverrideSet,
unitForkIsolatedFiles,
unitThreadPinnedFiles,
baseThreadPinnedFileSet,
classifyTestFile,
resolveFilterMatches,
};
}
export const testSurfaces = ["unit", "extensions", "channels", "gateway", "live", "e2e", "base"];

View File

@@ -1,669 +0,0 @@
import { spawn } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
getProcessTreeRecords,
parseCompletedTestFileLines,
sampleProcessTreeRssKb,
} from "../test-parallel-memory.mjs";
import {
appendCapturedOutput,
formatCapturedOutputTail,
hasFatalTestRunOutput,
resolveTestRunExitCode,
} from "../test-parallel-utils.mjs";
import { countExplicitEntryFilters, getExplicitEntryFilters } from "./vitest-args.mjs";
export function resolvePnpmCommandInvocation(options = {}) {
const npmExecPath = typeof options.npmExecPath === "string" ? options.npmExecPath.trim() : "";
if (npmExecPath && path.isAbsolute(npmExecPath)) {
const npmExecBase = path.basename(npmExecPath).toLowerCase();
if (npmExecBase.startsWith("pnpm")) {
return {
command: options.nodeExecPath || process.execPath,
args: [npmExecPath],
};
}
}
if (options.platform === "win32") {
return {
command: options.comSpec || "cmd.exe",
args: ["/d", "/s", "/c", "pnpm.cmd"],
};
}
return {
command: "pnpm",
args: [],
};
}
const sanitizeArtifactName = (value) => {
const normalized = value
.trim()
.replace(/[^a-z0-9._-]+/giu, "-")
.replace(/^-+|-+$/gu, "");
return normalized || "artifact";
};
const DEFAULT_CI_MAX_OLD_SPACE_SIZE_MB = 4096;
const WARNING_SUPPRESSION_FLAGS = [
"--disable-warning=ExperimentalWarning",
"--disable-warning=DEP0040",
"--disable-warning=DEP0060",
"--disable-warning=MaxListenersExceededWarning",
];
const formatElapsedMs = (elapsedMs) =>
elapsedMs >= 1000 ? `${(elapsedMs / 1000).toFixed(1)}s` : `${Math.round(elapsedMs)}ms`;
const formatMemoryKb = (rssKb) =>
rssKb >= 1024 ** 2
? `${(rssKb / 1024 ** 2).toFixed(2)}GiB`
: rssKb >= 1024
? `${(rssKb / 1024).toFixed(1)}MiB`
: `${rssKb}KiB`;
const formatMemoryDeltaKb = (rssKb) =>
`${rssKb >= 0 ? "+" : "-"}${formatMemoryKb(Math.abs(rssKb))}`;
export function createExecutionArtifacts(env = process.env) {
let tempArtifactDir = null;
const ensureTempArtifactDir = () => {
if (tempArtifactDir === null) {
tempArtifactDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-test-parallel-"));
}
return tempArtifactDir;
};
const writeTempJsonArtifact = (name, value) => {
const filePath = path.join(ensureTempArtifactDir(), `${sanitizeArtifactName(name)}.json`);
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, "utf8");
return filePath;
};
const cleanupTempArtifacts = () => {
if (tempArtifactDir === null) {
return;
}
if (env.OPENCLAW_TEST_KEEP_TEMP_ARTIFACTS === "1") {
console.error(`[test-parallel] keeping temp artifacts at ${tempArtifactDir}`);
return;
}
fs.rmSync(tempArtifactDir, { recursive: true, force: true });
tempArtifactDir = null;
};
return { ensureTempArtifactDir, writeTempJsonArtifact, cleanupTempArtifacts };
}
const ensureNodeOptionFlag = (nodeOptions, flagPrefix, nextValue) =>
nodeOptions.includes(flagPrefix) ? nodeOptions : `${nodeOptions} ${nextValue}`.trim();
const isNodeLikeProcess = (command) => /(?:^|\/)node(?:$|\.exe$)/iu.test(command);
const getShardLabel = (args) => {
const shardIndex = args.findIndex((arg) => arg === "--shard");
if (shardIndex < 0) {
return "";
}
return typeof args[shardIndex + 1] === "string" ? args[shardIndex + 1] : "";
};
export function formatPlanOutput(plan) {
return [
`runtime=${plan.runtimeCapabilities.runtimeProfileName} mode=${plan.runtimeCapabilities.mode} intent=${plan.runtimeCapabilities.intentProfile} memoryBand=${plan.runtimeCapabilities.memoryBand} loadBand=${plan.runtimeCapabilities.loadBand} vitestMaxWorkers=${String(plan.executionBudget.vitestMaxWorkers ?? "default")} topLevelParallel=${plan.topLevelParallelEnabled ? String(plan.topLevelParallelLimit) : "off"} unitPool=${plan.executionBudget.defaultUnitPool} basePool=${plan.executionBudget.defaultBasePool} threadExpansion=${String(plan.executionBudget.threadExpansionEnabled)} threadPolicy=${plan.executionBudget.threadPoolReason}`,
...plan.selectedUnits.map(
(unit) =>
`${unit.id} filters=${String(countExplicitEntryFilters(unit.args) ?? "all")} maxWorkers=${String(
unit.maxWorkers ?? "default",
)} surface=${unit.surface} isolate=${unit.isolate ? "yes" : "no"} pool=${unit.pool}`,
),
].join("\n");
}
export function formatExplanation(explanation) {
return [
`file=${explanation.file}`,
`runtime=${explanation.runtimeProfile} intent=${explanation.intentProfile} memoryBand=${explanation.memoryBand} loadBand=${explanation.loadBand}`,
`threadExpansion=${String(explanation.threadExpansionEnabled)} threadPolicy=${explanation.threadPoolReason}`,
`surface=${explanation.surface}`,
`isolate=${explanation.isolate ? "yes" : "no"}`,
`pool=${explanation.pool}`,
`maxWorkers=${String(explanation.maxWorkers ?? "default")}`,
`reasons=${explanation.reasons.join(",")}`,
`command=${explanation.args.join(" ")}`,
].join("\n");
}
export async function executePlan(plan, options = {}) {
const env = options.env ?? process.env;
const artifacts = options.artifacts ?? createExecutionArtifacts(env);
const pnpmInvocation = resolvePnpmCommandInvocation({
npmExecPath: env.npm_execpath,
nodeExecPath: process.execPath,
platform: process.platform,
comSpec: env.ComSpec,
});
const children = new Set();
const windowsCiArgs = plan.runtimeCapabilities.isWindowsCi
? ["--dangerouslyIgnoreUnhandledErrors"]
: [];
const silentArgs = env.OPENCLAW_TEST_SHOW_PASSED_LOGS === "1" ? [] : ["--silent=passed-only"];
const rawMemoryTrace = env.OPENCLAW_TEST_MEMORY_TRACE?.trim().toLowerCase();
const memoryTraceEnabled =
process.platform !== "win32" &&
(rawMemoryTrace === "1" ||
rawMemoryTrace === "true" ||
(rawMemoryTrace !== "0" && rawMemoryTrace !== "false" && plan.runtimeCapabilities.isCI));
const parseEnvNumber = (name, fallback) => {
const parsed = Number.parseInt(env[name] ?? "", 10);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
};
const memoryTracePollMs = Math.max(
250,
parseEnvNumber("OPENCLAW_TEST_MEMORY_TRACE_POLL_MS", 1000),
);
const memoryTraceTopCount = Math.max(
1,
parseEnvNumber("OPENCLAW_TEST_MEMORY_TRACE_TOP_COUNT", 6),
);
const requestedHeapSnapshotIntervalMs = Math.max(
0,
parseEnvNumber("OPENCLAW_TEST_HEAPSNAPSHOT_INTERVAL_MS", 0),
);
const heapSnapshotMinIntervalMs = 1000;
const heapSnapshotIntervalMs =
requestedHeapSnapshotIntervalMs > 0
? Math.max(heapSnapshotMinIntervalMs, requestedHeapSnapshotIntervalMs)
: 0;
const heapSnapshotEnabled =
process.platform !== "win32" && heapSnapshotIntervalMs >= heapSnapshotMinIntervalMs;
const heapSnapshotSignal = env.OPENCLAW_TEST_HEAPSNAPSHOT_SIGNAL?.trim() || "SIGUSR2";
const heapSnapshotBaseDir = heapSnapshotEnabled
? path.resolve(
env.OPENCLAW_TEST_HEAPSNAPSHOT_DIR?.trim() ||
path.join(os.tmpdir(), `openclaw-heapsnapshots-${Date.now()}`),
)
: null;
const maxOldSpaceSizeMb = (() => {
const raw = env.OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB ?? "";
const parsed = Number.parseInt(raw, 10);
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
if (plan.runtimeCapabilities.isCI && !plan.runtimeCapabilities.isWindows) {
return DEFAULT_CI_MAX_OLD_SPACE_SIZE_MB;
}
return null;
})();
const shutdown = (signal) => {
for (const child of children) {
child.kill(signal);
}
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("exit", artifacts.cleanupTempArtifacts);
const runOnce = (unit, extraArgs = []) =>
new Promise((resolve) => {
const startedAt = Date.now();
const entryArgs = unit.args;
const explicitEntryFilters = getExplicitEntryFilters(entryArgs);
const args = unit.maxWorkers
? [
...entryArgs,
"--maxWorkers",
String(unit.maxWorkers),
...silentArgs,
...windowsCiArgs,
...extraArgs,
]
: [...entryArgs, ...silentArgs, ...windowsCiArgs, ...extraArgs];
const spawnArgs = [...pnpmInvocation.args, ...args];
const shardLabel = getShardLabel(extraArgs);
const artifactStem = [
sanitizeArtifactName(unit.id),
shardLabel ? `shard-${sanitizeArtifactName(shardLabel)}` : "",
String(startedAt),
]
.filter(Boolean)
.join("-");
const laneLogPath = path.join(artifacts.ensureTempArtifactDir(), `${artifactStem}.log`);
const laneLogStream = fs.createWriteStream(laneLogPath, { flags: "w" });
laneLogStream.write(`[test-parallel] entry=${unit.id}\n`);
laneLogStream.write(`[test-parallel] cwd=${process.cwd()}\n`);
laneLogStream.write(
`[test-parallel] command=${[pnpmInvocation.command, ...spawnArgs].join(" ")}\n\n`,
);
console.log(
`[test-parallel] start ${unit.id} workers=${unit.maxWorkers ?? "default"} filters=${String(
countExplicitEntryFilters(entryArgs) ?? "all",
)}`,
);
const nodeOptions = env.NODE_OPTIONS ?? "";
const nextNodeOptions = WARNING_SUPPRESSION_FLAGS.reduce(
(acc, flag) => (acc.includes(flag) ? acc : `${acc} ${flag}`.trim()),
nodeOptions,
);
const heapSnapshotDir =
heapSnapshotBaseDir === null ? null : path.join(heapSnapshotBaseDir, unit.id);
let resolvedNodeOptions =
maxOldSpaceSizeMb && !nextNodeOptions.includes("--max-old-space-size=")
? `${nextNodeOptions} --max-old-space-size=${maxOldSpaceSizeMb}`.trim()
: nextNodeOptions;
if (heapSnapshotEnabled && heapSnapshotDir) {
try {
fs.mkdirSync(heapSnapshotDir, { recursive: true });
} catch (err) {
console.error(
`[test-parallel] failed to create heap snapshot dir ${heapSnapshotDir}: ${String(err)}`,
);
resolve(1);
return;
}
resolvedNodeOptions = ensureNodeOptionFlag(
resolvedNodeOptions,
"--diagnostic-dir=",
`--diagnostic-dir=${heapSnapshotDir}`,
);
resolvedNodeOptions = ensureNodeOptionFlag(
resolvedNodeOptions,
"--heapsnapshot-signal=",
`--heapsnapshot-signal=${heapSnapshotSignal}`,
);
}
let output = "";
let fatalSeen = false;
let childError = null;
let child;
let pendingLine = "";
let memoryPollTimer = null;
let heapSnapshotTimer = null;
const memoryFileRecords = [];
let initialTreeSample = null;
let latestTreeSample = null;
let peakTreeSample = null;
let heapSnapshotSequence = 0;
const updatePeakTreeSample = (sample, reason) => {
if (!sample) {
return;
}
if (!peakTreeSample || sample.rssKb > peakTreeSample.rssKb) {
peakTreeSample = { ...sample, reason };
}
};
const triggerHeapSnapshot = (reason) => {
if (!heapSnapshotEnabled || !child?.pid || !heapSnapshotDir) {
return;
}
const records = getProcessTreeRecords(child.pid) ?? [];
const targetPids = records
.filter((record) => record.pid !== process.pid && isNodeLikeProcess(record.command))
.map((record) => record.pid);
if (targetPids.length === 0) {
return;
}
heapSnapshotSequence += 1;
let signaledCount = 0;
for (const pid of targetPids) {
try {
process.kill(pid, heapSnapshotSignal);
signaledCount += 1;
} catch {}
}
if (signaledCount > 0) {
console.log(
`[test-parallel][heap] ${unit.id} seq=${String(heapSnapshotSequence)} reason=${reason} signaled=${String(
signaledCount,
)}/${String(targetPids.length)} dir=${heapSnapshotDir}`,
);
}
};
const captureTreeSample = (reason) => {
if (!memoryTraceEnabled || !child?.pid) {
return null;
}
const sample = sampleProcessTreeRssKb(child.pid);
if (!sample) {
return null;
}
latestTreeSample = sample;
if (!initialTreeSample) {
initialTreeSample = sample;
}
updatePeakTreeSample(sample, reason);
return sample;
};
const logMemoryTraceForText = (text) => {
if (!memoryTraceEnabled) {
return;
}
const combined = `${pendingLine}${text}`;
const lines = combined.split(/\r?\n/u);
pendingLine = lines.pop() ?? "";
const completedFiles = parseCompletedTestFileLines(lines.join("\n"));
for (const completedFile of completedFiles) {
const sample = captureTreeSample(completedFile.file);
if (!sample) {
continue;
}
const previousRssKb =
memoryFileRecords.length > 0
? (memoryFileRecords.at(-1)?.rssKb ?? initialTreeSample?.rssKb ?? sample.rssKb)
: (initialTreeSample?.rssKb ?? sample.rssKb);
const deltaKb = sample.rssKb - previousRssKb;
const record = {
...completedFile,
rssKb: sample.rssKb,
processCount: sample.processCount,
deltaKb,
};
memoryFileRecords.push(record);
console.log(
`[test-parallel][mem] ${unit.id} file=${record.file} rss=${formatMemoryKb(
record.rssKb,
)} delta=${formatMemoryDeltaKb(record.deltaKb)} peak=${formatMemoryKb(
peakTreeSample?.rssKb ?? record.rssKb,
)} procs=${record.processCount}${record.durationMs ? ` duration=${formatElapsedMs(record.durationMs)}` : ""}`,
);
}
};
const logMemoryTraceSummary = () => {
if (!memoryTraceEnabled) {
return;
}
captureTreeSample("close");
const fallbackRecord =
memoryFileRecords.length === 0 &&
explicitEntryFilters.length === 1 &&
latestTreeSample &&
initialTreeSample
? [
{
file: explicitEntryFilters[0],
deltaKb: latestTreeSample.rssKb - initialTreeSample.rssKb,
},
]
: [];
const totalDeltaKb =
initialTreeSample && latestTreeSample
? latestTreeSample.rssKb - initialTreeSample.rssKb
: 0;
const topGrowthFiles = [...memoryFileRecords, ...fallbackRecord]
.filter((record) => record.deltaKb > 0 && typeof record.file === "string")
.toSorted((left, right) => right.deltaKb - left.deltaKb)
.slice(0, memoryTraceTopCount)
.map((record) => `${record.file}:${formatMemoryDeltaKb(record.deltaKb)}`);
console.log(
`[test-parallel][mem] summary ${unit.id} files=${memoryFileRecords.length} peak=${formatMemoryKb(
peakTreeSample?.rssKb ?? 0,
)} totalDelta=${formatMemoryDeltaKb(totalDeltaKb)} peakAt=${
peakTreeSample?.reason ?? "n/a"
} top=${topGrowthFiles.length > 0 ? topGrowthFiles.join(", ") : "none"}`,
);
};
try {
child = spawn(pnpmInvocation.command, spawnArgs, {
stdio: ["inherit", "pipe", "pipe"],
env: {
...env,
...unit.env,
VITEST_GROUP: unit.id,
NODE_OPTIONS: resolvedNodeOptions,
},
shell: false,
});
captureTreeSample("spawn");
if (memoryTraceEnabled) {
memoryPollTimer = setInterval(() => {
captureTreeSample("poll");
}, memoryTracePollMs);
}
if (heapSnapshotEnabled) {
heapSnapshotTimer = setInterval(() => {
triggerHeapSnapshot("interval");
}, heapSnapshotIntervalMs);
}
} catch (err) {
laneLogStream.end();
console.error(`[test-parallel] spawn failed: ${String(err)}`);
resolve(1);
return;
}
children.add(child);
child.stdout?.on("data", (chunk) => {
const text = chunk.toString();
fatalSeen ||= hasFatalTestRunOutput(`${output}${text}`);
output = appendCapturedOutput(output, text);
laneLogStream.write(text);
logMemoryTraceForText(text);
process.stdout.write(chunk);
});
child.stderr?.on("data", (chunk) => {
const text = chunk.toString();
fatalSeen ||= hasFatalTestRunOutput(`${output}${text}`);
output = appendCapturedOutput(output, text);
laneLogStream.write(text);
logMemoryTraceForText(text);
process.stderr.write(chunk);
});
child.on("error", (err) => {
childError = err;
laneLogStream.write(`\n[test-parallel] child error: ${String(err)}\n`);
console.error(`[test-parallel] child error: ${String(err)}`);
});
child.on("close", (code, signal) => {
if (memoryPollTimer) {
clearInterval(memoryPollTimer);
}
if (heapSnapshotTimer) {
clearInterval(heapSnapshotTimer);
}
children.delete(child);
const resolvedCode = resolveTestRunExitCode({
code,
signal,
output,
fatalSeen,
childError,
});
const elapsedMs = Date.now() - startedAt;
logMemoryTraceSummary();
if (resolvedCode !== 0) {
const failureTail = formatCapturedOutputTail(output);
const failureArtifactPath = artifacts.writeTempJsonArtifact(`${artifactStem}-failure`, {
entry: unit.id,
command: [pnpmInvocation.command, ...spawnArgs],
elapsedMs,
error: childError ? String(childError) : null,
exitCode: resolvedCode,
fatalSeen,
logPath: laneLogPath,
outputTail: failureTail,
signal: signal ?? null,
});
if (failureTail) {
console.error(`[test-parallel] failure tail ${unit.id}\n${failureTail}`);
}
console.error(
`[test-parallel] failure artifacts ${unit.id} log=${laneLogPath} meta=${failureArtifactPath}`,
);
}
laneLogStream.write(
`\n[test-parallel] done ${unit.id} code=${String(resolvedCode)} signal=${
signal ?? "none"
} elapsed=${formatElapsedMs(elapsedMs)}\n`,
);
laneLogStream.end();
console.log(
`[test-parallel] done ${unit.id} code=${String(resolvedCode)} elapsed=${formatElapsedMs(elapsedMs)}`,
);
resolve(resolvedCode);
});
});
const runUnit = async (unit, extraArgs = []) => {
if (unit.fixedShardIndex !== undefined) {
if (plan.shardIndexOverride !== null && plan.shardIndexOverride !== unit.fixedShardIndex) {
return 0;
}
return runOnce(unit, extraArgs);
}
const explicitFilterCount = countExplicitEntryFilters(unit.args);
const topLevelAssignedShard = plan.topLevelSingleShardAssignments.get(unit);
if (topLevelAssignedShard !== undefined) {
if (plan.shardIndexOverride !== null && plan.shardIndexOverride !== topLevelAssignedShard) {
return 0;
}
return runOnce(unit, extraArgs);
}
const effectiveShardCount =
explicitFilterCount === null
? plan.shardCount
: Math.min(plan.shardCount, Math.max(1, explicitFilterCount - 1));
if (effectiveShardCount <= 1) {
if (plan.shardIndexOverride !== null && plan.shardIndexOverride > effectiveShardCount) {
return 0;
}
return runOnce(unit, extraArgs);
}
if (plan.shardIndexOverride !== null) {
if (plan.shardIndexOverride > effectiveShardCount) {
return 0;
}
return runOnce(unit, [
"--shard",
`${plan.shardIndexOverride}/${effectiveShardCount}`,
...extraArgs,
]);
}
for (let shardIndex = 1; shardIndex <= effectiveShardCount; shardIndex += 1) {
// eslint-disable-next-line no-await-in-loop
const code = await runOnce(unit, [
"--shard",
`${shardIndex}/${effectiveShardCount}`,
...extraArgs,
]);
if (code !== 0) {
return code;
}
}
return 0;
};
const runUnitsWithLimit = async (units, extraArgs = [], concurrency = 1) => {
if (units.length === 0) {
return undefined;
}
const normalizedConcurrency = Math.max(1, Math.floor(concurrency));
if (normalizedConcurrency <= 1) {
for (const unit of units) {
// eslint-disable-next-line no-await-in-loop
const code = await runUnit(unit, extraArgs);
if (code !== 0) {
return code;
}
}
return undefined;
}
let nextIndex = 0;
let firstFailure;
const worker = async () => {
while (firstFailure === undefined) {
const unitIndex = nextIndex;
nextIndex += 1;
if (unitIndex >= units.length) {
return;
}
const code = await runUnit(units[unitIndex], extraArgs);
if (code !== 0 && firstFailure === undefined) {
firstFailure = code;
}
}
};
const workerCount = Math.min(normalizedConcurrency, units.length);
await Promise.all(Array.from({ length: workerCount }, () => worker()));
return firstFailure;
};
const runUnits = async (units, extraArgs = []) => {
if (plan.topLevelParallelEnabled) {
return runUnitsWithLimit(units, extraArgs, plan.topLevelParallelLimit);
}
return runUnitsWithLimit(units, extraArgs);
};
if (plan.passthroughMetadataOnly) {
return runOnce(
{
id: "vitest-meta",
args: ["vitest", "run"],
maxWorkers: null,
},
plan.passthroughOptionArgs,
);
}
if (plan.targetedUnits.length > 0) {
if (plan.passthroughRequiresSingleRun && plan.targetedUnits.length > 1) {
console.error(
"[test-parallel] The provided Vitest args require a single run, but the selected test filters span multiple wrapper configs. Run one target/config at a time.",
);
return 2;
}
const failedTargetedParallel = await runUnits(plan.parallelUnits, plan.passthroughOptionArgs);
if (failedTargetedParallel !== undefined) {
return failedTargetedParallel;
}
for (const unit of plan.serialUnits) {
// eslint-disable-next-line no-await-in-loop
const code = await runUnit(unit, plan.passthroughOptionArgs);
if (code !== 0) {
return code;
}
}
return 0;
}
if (plan.passthroughRequiresSingleRun && plan.passthroughOptionArgs.length > 0) {
console.error(
"[test-parallel] The provided Vitest args require a single run. Use the dedicated npm script for that workflow (for example `pnpm test:coverage`) or target a single test file/filter.",
);
return 2;
}
if (plan.serialPrefixUnits.length > 0) {
const failedSerialPrefix = await runUnitsWithLimit(
plan.serialPrefixUnits,
plan.passthroughOptionArgs,
1,
);
if (failedSerialPrefix !== undefined) {
return failedSerialPrefix;
}
const failedDeferredParallel = plan.deferredRunConcurrency
? await runUnitsWithLimit(
plan.deferredParallelUnits,
plan.passthroughOptionArgs,
plan.deferredRunConcurrency,
)
: await runUnits(plan.deferredParallelUnits, plan.passthroughOptionArgs);
if (failedDeferredParallel !== undefined) {
return failedDeferredParallel;
}
} else {
const failedParallel = await runUnits(plan.parallelUnits, plan.passthroughOptionArgs);
if (failedParallel !== undefined) {
return failedParallel;
}
}
for (const unit of plan.serialUnits) {
// eslint-disable-next-line no-await-in-loop
const code = await runUnit(unit, plan.passthroughOptionArgs);
if (code !== 0) {
return code;
}
}
return 0;
}

File diff suppressed because it is too large Load Diff

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