mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 15:03:05 +08:00
Compare commits
21 Commits
omarshahin
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7641ec8ba7 | ||
|
|
28cbeaaa80 | ||
|
|
e773981088 | ||
|
|
9d4d07175d | ||
|
|
cdb7e64994 | ||
|
|
7daba184b5 | ||
|
|
3d1935dcc1 | ||
|
|
b832153d0a | ||
|
|
9bd2b4b34b | ||
|
|
02abd3adb5 | ||
|
|
0dda56e0f6 | ||
|
|
365d78605d | ||
|
|
a2c64b08ff | ||
|
|
90decc657d | ||
|
|
e670de672d | ||
|
|
c4aba64b58 | ||
|
|
3a024f6a8d | ||
|
|
c866b087eb | ||
|
|
620cca1d7f | ||
|
|
353012b8a8 | ||
|
|
a37c6c935a |
@@ -4,7 +4,6 @@ import { execFileSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
const repo = "openclaw/openclaw";
|
||||
const commitAssociationQueryBatchSize = 20;
|
||||
const excludedHandles = new Set(["openclaw", "clawsweeper", "claude", "codex", "steipete"]);
|
||||
const nonEditorialTypes = new Set([
|
||||
"build",
|
||||
@@ -619,25 +618,13 @@ function graphql(query) {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
try {
|
||||
const response = githubApi(["graphql", "-f", `query=${query}`]);
|
||||
if (response?.data && typeof response.data === "object") {
|
||||
return response.data;
|
||||
}
|
||||
const errors = Array.isArray(response?.errors)
|
||||
? response.errors.map((error) => error?.message).filter(Boolean)
|
||||
: [];
|
||||
const detail = [...errors, response?.message].filter(Boolean).join("\n");
|
||||
throw new Error(
|
||||
detail
|
||||
? `GitHub GraphQL response did not include data:\n${detail}`
|
||||
: "GitHub GraphQL response did not include data.",
|
||||
);
|
||||
return githubApi(["graphql", "-f", `query=${query}`]).data;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const message = [error?.message, error?.stdout, error?.stderr].filter(Boolean).join("\n");
|
||||
// Historical ranges batch hundreds of objects; only retry transient transport failures.
|
||||
if (
|
||||
!/(?:operation timed out|ECONNRESET|ETIMEDOUT|EAI_AGAIN|TLS handshake timeout|stream error: .*CANCEL|unexpected end of JSON input|upstream connect error|connection termination|connection reset by peer|error connecting to api\.github\.com|Unexpected token '<'|something went wrong|temporarily unavailable|internal server error|rate limit)/i.test(
|
||||
!/(?:operation timed out|ECONNRESET|ETIMEDOUT|EAI_AGAIN|TLS handshake timeout|stream error: .*CANCEL|unexpected end of JSON input|upstream connect error|connection termination|error connecting to api\.github\.com|Unexpected token '<')/i.test(
|
||||
message,
|
||||
)
|
||||
) {
|
||||
@@ -670,8 +657,8 @@ function resolveAssociatedPullRequests(commitHashes, targetTimestamp) {
|
||||
pending.push({ commitHash, cursor: connection.pageInfo.endCursor });
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < commitHashes.length; index += commitAssociationQueryBatchSize) {
|
||||
const chunk = commitHashes.slice(index, index + commitAssociationQueryBatchSize);
|
||||
for (let index = 0; index < commitHashes.length; index += 40) {
|
||||
const chunk = commitHashes.slice(index, index + 40);
|
||||
const fields = chunk
|
||||
.map(
|
||||
(hash, offset) =>
|
||||
|
||||
@@ -22,7 +22,7 @@ paths:
|
||||
- src/plugins/memory-*.ts
|
||||
- src/gateway/server-startup-memory.ts
|
||||
- src/commands/doctor-memory-search.ts
|
||||
- src/commands/doctor/cron/dreaming-payload-migration.ts
|
||||
- src/commands/doctor-cron-dreaming-payload-migration.ts
|
||||
|
||||
paths-ignore:
|
||||
- "**/node_modules"
|
||||
|
||||
@@ -19,6 +19,7 @@ paths:
|
||||
- src/plugins/bundled-compat.ts
|
||||
- src/plugins/bundled-dir.ts
|
||||
- src/plugins/bundled-plugin-metadata.ts
|
||||
- src/plugins/bundled-public-surface-runtime-root.ts
|
||||
- src/plugins/plugin-sdk-dist-alias.ts
|
||||
- src/plugins/captured-registration.ts
|
||||
- src/plugins/config-activation-shared.ts
|
||||
@@ -45,6 +46,7 @@ paths:
|
||||
- src/plugins/runtime-state.ts
|
||||
- src/plugins/runtime.ts
|
||||
- src/plugins/sdk-alias.ts
|
||||
- src/plugins/source-loader.ts
|
||||
- src/plugins/types.ts
|
||||
- src/plugins/validation-diagnostics.ts
|
||||
- src/plugins/web-provider-public-artifacts*.ts
|
||||
|
||||
@@ -51,6 +51,7 @@ paths:
|
||||
- src/plugins/runtime
|
||||
- src/plugins/runtime-state.ts
|
||||
- src/plugins/runtime.ts
|
||||
- src/plugins/source-loader.ts
|
||||
- src/plugins/update.ts
|
||||
- src/plugins/validation-diagnostics.ts
|
||||
- src/plugin-sdk/*entry*.ts
|
||||
|
||||
12
.github/labeler.yml
vendored
12
.github/labeler.yml
vendored
@@ -41,6 +41,12 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/google-meet/**"
|
||||
- "docs/plugins/google-meet.md"
|
||||
"plugin: meeting-notes":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/meeting-notes/**"
|
||||
- "docs/plugins/meeting-notes.md"
|
||||
- "src/meeting-notes/**"
|
||||
"plugin: workboard":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -246,12 +252,12 @@
|
||||
- "src/agents/sandbox*.ts"
|
||||
- "src/commands/sandbox*.ts"
|
||||
- "src/cli/sandbox-cli.ts"
|
||||
- "src/docker-setup.e2e.test.ts"
|
||||
- "src/docker-setup.test.ts"
|
||||
- "src/config/**/*sandbox*"
|
||||
- "docs/cli/sandbox.md"
|
||||
- "docs/gateway/sandbox*.md"
|
||||
- "docs/install/docker.md"
|
||||
- "docs/tools/multi-agent-sandbox-tools.md"
|
||||
- "docs/multi-agent-sandbox-tools.md"
|
||||
|
||||
"agents":
|
||||
- changed-files:
|
||||
@@ -264,7 +270,7 @@
|
||||
- ".github/workflows/opengrep-*.yml"
|
||||
- ".semgrepignore"
|
||||
- "docs/cli/security.md"
|
||||
- "docs/gateway/security/**"
|
||||
- "docs/gateway/security.md"
|
||||
- "security/**"
|
||||
|
||||
"extensions: admin-http-rpc":
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -819,7 +819,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_core_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -909,7 +908,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.plugin_contracts_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -990,7 +988,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.channel_contracts_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1143,7 +1140,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 6
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1252,7 +1248,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
include:
|
||||
- check_name: check-guards
|
||||
@@ -1394,7 +1389,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
include:
|
||||
- check_name: check-additional-boundaries-a
|
||||
@@ -1769,7 +1763,6 @@ jobs:
|
||||
shell: bash
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.checks_windows_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -2099,7 +2092,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.android_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
30
.github/workflows/clawsweeper-dispatch.yml
vendored
30
.github/workflows/clawsweeper-dispatch.yml
vendored
@@ -81,27 +81,9 @@ jobs:
|
||||
repositories: clawsweeper
|
||||
permission-contents: write
|
||||
|
||||
- name: Pre-filter ClawSweeper comment
|
||||
id: comment_filter
|
||||
if: ${{ github.event_name == 'issue_comment' }}
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if grep -Eiq '(^|[[:space:]])@(clawsweeper|openclaw-clawsweeper)\b(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|autoclose|auto([[:space:]]+|-)?merge)\b' <<< "$COMMENT_BODY"; then
|
||||
echo "is_command=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_command=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create target comment token
|
||||
id: target_token
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'issue_comment' &&
|
||||
steps.comment_filter.outputs.is_command == 'true' &&
|
||||
env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true'
|
||||
}}
|
||||
if: ${{ github.event_name == 'issue_comment' && env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }}
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
|
||||
@@ -231,11 +213,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Acknowledge and dispatch ClawSweeper comment
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'issue_comment' &&
|
||||
steps.comment_filter.outputs.is_command == 'true'
|
||||
}}
|
||||
if: ${{ github.event_name == 'issue_comment' }}
|
||||
env:
|
||||
DISPATCH_TOKEN: ${{ steps.token.outputs.token }}
|
||||
TARGET_TOKEN: ${{ steps.target_token.outputs.token }}
|
||||
@@ -254,6 +232,10 @@ jobs:
|
||||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
body_file="$RUNNER_TEMP/clawsweeper-comment-body.txt"
|
||||
printf '%s\n' "$COMMENT_BODY" > "$body_file"
|
||||
if ! grep -Eiq '(^|[[:space:]])@(clawsweeper|openclaw-clawsweeper)\b(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|automerge|autoclose)\b' "$body_file"; then
|
||||
echo "No ClawSweeper command found in comment."
|
||||
exit 0
|
||||
fi
|
||||
if [ -n "$TARGET_TOKEN" ]; then
|
||||
err="$(mktemp)"
|
||||
if GH_TOKEN="$TARGET_TOKEN" gh_api_with_retry -X POST \
|
||||
|
||||
@@ -96,7 +96,7 @@ on:
|
||||
- "src/auto-reply/reply/post-compaction-context.ts"
|
||||
- "src/auto-reply/reply/queue/**"
|
||||
- "src/auto-reply/reply/startup-context.ts"
|
||||
- "src/commands/doctor/cron/dreaming-payload-migration.ts"
|
||||
- "src/commands/doctor-cron-dreaming-payload-migration.ts"
|
||||
- "src/commands/doctor-memory-search.ts"
|
||||
- "src/commands/doctor-session-*.ts"
|
||||
- "src/commands/session-store-targets.ts"
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
packages/gateway-protocol/src/*|packages/gateway-protocol/src/**/*|src/gateway/method-scopes.ts|src/gateway/server-methods/*|src/gateway/server-methods.ts|src/gateway/server-methods-list.ts)
|
||||
gateway=true
|
||||
;;
|
||||
packages/memory-host-sdk/*|src/commands/doctor/cron/dreaming-payload-migration.ts|src/commands/doctor-memory-search.ts|src/gateway/server-startup-memory.ts|src/memory/*|src/memory-host-sdk/*)
|
||||
packages/memory-host-sdk/*|src/commands/doctor-cron-dreaming-payload-migration.ts|src/commands/doctor-memory-search.ts|src/gateway/server-startup-memory.ts|src/memory/*|src/memory-host-sdk/*)
|
||||
memory=true
|
||||
;;
|
||||
src/infra/outbound/base-session-key.ts|src/infra/outbound/delivery-queue*.ts|src/infra/outbound/outbound-session.ts|src/infra/outbound/session-binding*.ts|src/infra/outbound/session-context.ts|src/infra/outbound/targets-session.ts)
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
src/model-catalog/*|src/plugins/*provider*.ts|src/plugins/capability-provider-runtime.ts|src/plugins/compaction-provider.ts|src/plugins/memory-embedding-provider*.ts|src/plugins/memory-embedding-providers*.ts|src/plugins/migration-provider-runtime.ts|src/plugins/synthetic-auth.runtime.ts|src/plugins/web-fetch-providers*.ts|src/plugins/web-search-providers*.ts)
|
||||
provider=true
|
||||
;;
|
||||
src/plugins/activation-planner.ts|src/plugins/api-builder.ts|src/plugins/bundled-*.ts|src/plugins/captured-registration.ts|src/plugins/config-*.ts|src/plugins/discovery.ts|src/plugins/effective-plugin-ids.ts|src/plugins/externalized-bundled-plugins.ts|src/plugins/installed-plugin-index*.ts|src/plugins/loader*.ts|src/plugins/manifest*.ts|src/plugins/module-export.ts|src/plugins/package-entrypoints.ts|src/plugins/plugin-registry*.ts|src/plugins/public-surface*.ts|src/plugins/registry.ts|src/plugins/registry-types.ts|src/plugins/runtime|src/plugins/runtime/*|src/plugins/runtime-state.ts|src/plugins/runtime.ts|src/plugins/sdk-alias.ts|src/plugins/types.ts|src/plugins/validation-diagnostics.ts)
|
||||
src/plugins/activation-planner.ts|src/plugins/api-builder.ts|src/plugins/bundled-*.ts|src/plugins/captured-registration.ts|src/plugins/config-*.ts|src/plugins/discovery.ts|src/plugins/effective-plugin-ids.ts|src/plugins/externalized-bundled-plugins.ts|src/plugins/installed-plugin-index*.ts|src/plugins/loader*.ts|src/plugins/manifest*.ts|src/plugins/module-export.ts|src/plugins/package-entrypoints.ts|src/plugins/plugin-registry*.ts|src/plugins/public-surface*.ts|src/plugins/registry.ts|src/plugins/registry-types.ts|src/plugins/runtime|src/plugins/runtime/*|src/plugins/runtime-state.ts|src/plugins/runtime.ts|src/plugins/sdk-alias.ts|src/plugins/source-loader.ts|src/plugins/types.ts|src/plugins/validation-diagnostics.ts)
|
||||
plugin=true
|
||||
;;
|
||||
packages/plugin-package-contract/*|packages/plugin-sdk/*)
|
||||
|
||||
107
.github/workflows/full-release-validation.yml
vendored
107
.github/workflows/full-release-validation.yml
vendored
@@ -70,7 +70,7 @@ on:
|
||||
default: ""
|
||||
type: string
|
||||
npm_telegram_package_spec:
|
||||
description: Optional published package spec for the focused package Telegram E2E rerun
|
||||
description: Optional published package spec for the package Telegram E2E lane
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
@@ -95,7 +95,7 @@ on:
|
||||
default: ""
|
||||
type: string
|
||||
npm_telegram_provider_mode:
|
||||
description: Provider mode for the focused package Telegram E2E rerun
|
||||
description: Provider mode for the package Telegram E2E lane
|
||||
required: false
|
||||
default: mock-openai
|
||||
type: choice
|
||||
@@ -103,7 +103,7 @@ on:
|
||||
- mock-openai
|
||||
- live-frontier
|
||||
npm_telegram_scenario:
|
||||
description: Optional comma-separated Telegram scenario ids for the focused package Telegram E2E rerun
|
||||
description: Optional comma-separated Telegram scenario ids for the package Telegram lane
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
@@ -200,16 +200,14 @@ jobs:
|
||||
if [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Published release package: \`${RELEASE_PACKAGE_SPEC}\`"
|
||||
fi
|
||||
if [[ "$RERUN_GROUP" == "npm-telegram" && -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then
|
||||
if [[ -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Published-package Telegram E2E: \`${NPM_TELEGRAM_PACKAGE_SPEC}\`"
|
||||
elif [[ "$RERUN_GROUP" == "npm-telegram" && -n "${RELEASE_PACKAGE_SPEC// }" ]]; then
|
||||
elif [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Published-package Telegram E2E: \`${RELEASE_PACKAGE_SPEC}\`"
|
||||
elif [[ "$RERUN_GROUP" == "npm-telegram" ]]; then
|
||||
echo "- Package Telegram E2E: focused rerun requires \`release_package_spec\` or \`npm_telegram_package_spec\`"
|
||||
elif [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "release-checks" || "$RERUN_GROUP" == "package" ]]; then
|
||||
echo "- Package Telegram E2E: OpenClaw Release Checks Package Acceptance"
|
||||
elif [[ "$RERUN_GROUP" == "all" && "$RELEASE_PROFILE" == "full" ]]; then
|
||||
echo "- Package Telegram E2E: parent \`release-package-under-test\` artifact"
|
||||
else
|
||||
echo "- Package Telegram E2E: skipped by rerun group"
|
||||
echo "- Package Telegram E2E: skipped unless \`release_profile=full\`, \`release_package_spec\`, or \`npm_telegram_package_spec\` is provided"
|
||||
fi
|
||||
if [[ -n "${EVIDENCE_PACKAGE_SPEC// }" ]]; then
|
||||
echo "- Private evidence package proof: \`${EVIDENCE_PACKAGE_SPEC}\`"
|
||||
@@ -766,10 +764,80 @@ jobs:
|
||||
|
||||
dispatch_and_wait openclaw-release-checks.yml "${args[@]}"
|
||||
|
||||
prepare_release_package:
|
||||
name: Prepare release package artifact
|
||||
needs: [resolve_target, docker_runtime_assets_preflight]
|
||||
if: ${{ always() && needs.resolve_target.result == 'success' && inputs.npm_telegram_package_spec == '' && inputs.release_package_spec == '' && inputs.rerun_group == 'all' && inputs.release_profile == 'full' && needs.docker_runtime_assets_preflight.result == 'success' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
artifact_name: ${{ steps.artifact.outputs.name }}
|
||||
package_sha256: ${{ steps.package.outputs.sha256 }}
|
||||
package_version: ${{ steps.package.outputs.package_version }}
|
||||
source_sha: ${{ steps.package.outputs.source_sha }}
|
||||
steps:
|
||||
- name: Checkout trusted workflow ref
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ github.ref_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set artifact metadata
|
||||
id: artifact
|
||||
run: echo "name=release-package-under-test" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "true"
|
||||
install-deps: "false"
|
||||
|
||||
- name: Resolve release package artifact
|
||||
id: package
|
||||
shell: bash
|
||||
env:
|
||||
PACKAGE_REF: ${{ needs.resolve_target.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node scripts/resolve-openclaw-package-candidate.mjs \
|
||||
--source ref \
|
||||
--package-ref "$PACKAGE_REF" \
|
||||
--output-dir .artifacts/docker-e2e-package \
|
||||
--output-name openclaw-current.tgz \
|
||||
--metadata .artifacts/docker-e2e-package/package-candidate.json \
|
||||
--github-output "$GITHUB_OUTPUT"
|
||||
digest="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).sha256")"
|
||||
version="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).version")"
|
||||
source_sha="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).packageSourceSha")"
|
||||
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "## Release package artifact"
|
||||
echo
|
||||
echo "- Artifact: \`release-package-under-test\`"
|
||||
echo "- Package ref: \`$PACKAGE_REF\`"
|
||||
echo "- SHA-256: \`$digest\`"
|
||||
echo "- Version: \`$version\`"
|
||||
echo "- Source SHA: \`$source_sha\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload release package artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: release-package-under-test
|
||||
path: |
|
||||
.artifacts/docker-e2e-package/openclaw-current.tgz
|
||||
.artifacts/docker-e2e-package/package-candidate.json
|
||||
if-no-files-found: error
|
||||
|
||||
npm_telegram:
|
||||
name: Run package Telegram E2E
|
||||
needs: [resolve_target]
|
||||
if: ${{ always() && needs.resolve_target.result == 'success' && inputs.rerun_group == 'npm-telegram' && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '') }}
|
||||
needs: [resolve_target, prepare_release_package]
|
||||
if: ${{ always() && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group) && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '' || (inputs.rerun_group == 'all' && inputs.release_profile == 'full')) }}
|
||||
continue-on-error: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: ${{ inputs.release_profile == 'full' && 360 || 60 }}
|
||||
@@ -785,6 +853,8 @@ jobs:
|
||||
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
|
||||
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
|
||||
PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec || inputs.release_package_spec }}
|
||||
PACKAGE_ARTIFACT_NAME: ${{ needs.prepare_release_package.outputs.artifact_name }}
|
||||
PREPARE_PACKAGE_RESULT: ${{ needs.prepare_release_package.result }}
|
||||
PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }}
|
||||
SCENARIO: ${{ inputs.npm_telegram_scenario }}
|
||||
run: |
|
||||
@@ -813,7 +883,18 @@ jobs:
|
||||
return "$status"
|
||||
}
|
||||
|
||||
args=(-f package_spec="$PACKAGE_SPEC" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
|
||||
args=(-f package_spec="${PACKAGE_SPEC:-openclaw@beta}" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
|
||||
if [[ -z "${PACKAGE_SPEC// }" ]]; then
|
||||
if [[ "$PREPARE_PACKAGE_RESULT" != "success" || -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then
|
||||
echo "Full release Telegram requires either npm_telegram_package_spec or a prepared release-package-under-test artifact." >&2
|
||||
exit 1
|
||||
fi
|
||||
args+=(
|
||||
-f package_artifact_name="$PACKAGE_ARTIFACT_NAME"
|
||||
-f package_artifact_run_id="${GITHUB_RUN_ID}"
|
||||
-f package_label="full-release-${TARGET_SHA:0:12}"
|
||||
)
|
||||
fi
|
||||
if [[ -n "${SCENARIO// }" ]]; then
|
||||
args+=(-f scenario="$SCENARIO")
|
||||
fi
|
||||
|
||||
@@ -717,6 +717,7 @@ jobs:
|
||||
published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }}
|
||||
published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}
|
||||
telegram_mode: mock-openai
|
||||
telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-status-command,telegram-other-bot-command-gating,telegram-context-command,telegram-mentioned-message-reply,telegram-long-final-reuses-preview,telegram-mention-gating
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||
|
||||
25
.github/workflows/workflow-sanity.yml
vendored
25
.github/workflows/workflow-sanity.yml
vendored
@@ -129,28 +129,11 @@ jobs:
|
||||
trusted_config="$RUNNER_TEMP/pre-commit-base.yaml"
|
||||
trusted_zizmor_config="$RUNNER_TEMP/zizmor-base.yml"
|
||||
|
||||
fetch_base_ref() {
|
||||
local ref="$1"
|
||||
local target="$2"
|
||||
local fetch_status
|
||||
for attempt in 1 2 3; do
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+${ref}:${target}" && return 0
|
||||
fetch_status="$?"
|
||||
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
if [ "$attempt" = "3" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
echo "::warning::trusted base fetch for '$ref' timed out on attempt $attempt; retrying"
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
|
||||
fetch_base_ref "$BASE_SHA" "refs/remotes/origin/security-base" ||
|
||||
fetch_base_ref "refs/heads/${BASE_REF}" "refs/remotes/origin/${BASE_REF}"
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+${BASE_SHA}:refs/remotes/origin/security-base" ||
|
||||
timeout --signal=TERM --kill-after=10s 30s git fetch --no-tags --depth=1 origin \
|
||||
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
|
||||
fi
|
||||
|
||||
if git cat-file -e "${BASE_SHA}:.pre-commit-config.yaml" 2>/dev/null; then
|
||||
|
||||
68
CHANGELOG.md
68
CHANGELOG.md
@@ -6,34 +6,34 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Highlights
|
||||
|
||||
- **Richer Telegram delivery:** Telegram now sends rich HTML, preserves rich markdown and sticker paths, renders progress drafts and command output more faithfully, normalizes HTML tables safely, and keeps mentions and spooled handlers on the right delivery path. (#93286, #93164, #93124, #93364, #93130, #93002, #93088, #93281, #94891, #94856) Thanks @obviyus, @vincentkoc, @goutamadwant, @kesslerio, @NianJiuZst, @SweetSophia, @Marvinthebored, @aaajiao, @zhangguiping-xydt, @zhangqueping, and @jairrab.
|
||||
- **Richer Telegram delivery:** Telegram now sends rich HTML, preserves rich markdown and sticker paths, renders progress drafts and command output more faithfully, and keeps mentions and spooled handlers on the right delivery path. (#93286, #93164, #93124, #93364, #93130, #93088, #93281) Thanks @obviyus, @vincentkoc, @goutamadwant, @kesslerio, @NianJiuZst, @SweetSophia, @Marvinthebored, and @aaajiao.
|
||||
- **More dependable agent recovery:** retries, terminal outcomes, usage after compaction, session history repair, and reply reconciliation now keep more interrupted or partial turns moving toward a visible final result. (#92191, #93073, #93228, #93084, #93469, #93291, #90943) Thanks @ai-hpc, @lml2468, @fuller-stack-dev, @Hollychou924, @leno23, @de1tydev, @425072024, @wuwahe3, @drvoss, @yetval, @sandieman2, and @vincentkoc.
|
||||
- **A stronger Codex integration:** Codex gains automatic plugin approvals, GPT-5.3 Spark OAuth routing, remote-node `exec` as a dynamic tool, and more reliable app-server teardown and terminal outcomes. (#92625, #89133, #93654, #91767, #93287) Thanks @kevinslin, @VACInc, @vincentkoc, @JPKay-AI, and @aliahnaf2013-max.
|
||||
- **Standalone official provider plugins:** external provider packages are now first-class npm releases, externally installed channel plugins load at Gateway startup, and StepFun is available from npm and ClawHub. (#93470) Thanks @sunlit-deng, @cxdnicole, and @vincentkoc.
|
||||
- **Standalone official provider plugins:** external provider packages are now first-class npm releases, externally installed channel plugins load at Gateway startup, and StepFun is intentionally npm-only because its ClawHub package name is unavailable. (#93470) Thanks @sunlit-deng, @cxdnicole, and @vincentkoc.
|
||||
- **More capable web and native clients:** the Control UI adds a session workspace rail and extension health, iOS adds Watch controls, and Android shows chat context. (#92856, #91952, #93387, #92837) Thanks @Solvely-Colin, @jalehman, @joshavant, and @Tosko4.
|
||||
- **More useful search and skills:** Codex Hosted Search is available, key-free search providers remain deliberate opt-ins, and ClawHub skill installs retain verified source provenance. (#93446, #93616, #93283, #93506) Thanks @fuller-stack-dev, @davemorin, @momothemage, @nmccready-tars, and @vincentkoc.
|
||||
|
||||
### Changes
|
||||
|
||||
- Providers and auth: add Codex Hosted Search, improve Gemini CLI OAuth behind proxies, and keep external provider onboarding on current choices and package metadata. (#93446, #92815) Thanks @fuller-stack-dev, @yetval, @EvetteYoung, and @vincentkoc.
|
||||
- Plugins and installs: externalized official providers publish as independent npm packages, Gateway discovers installed channel plugins at startup, and StepFun installs from npm or ClawHub. (#93470) Thanks @sunlit-deng, @cxdnicole, and @vincentkoc.
|
||||
- Plugins and installs: externalized official providers publish as independent npm packages, Gateway discovers installed channel plugins at startup, and StepFun installs exclusively from npm. (#93470) Thanks @sunlit-deng, @cxdnicole, and @vincentkoc.
|
||||
- Dashboard and mobile: add a session workspace rail, plugin health in status, compact cron lists, and iOS Watch controls. (#92856, #91952, #93395, #93387) Thanks @Solvely-Colin, @jalehman, @yu-xin-c, @centralpc, @joshavant, and @vincentkoc.
|
||||
- Codex, observability, and skills: add automatic plugin approvals and SecretRefs, preserve ClawHub skill provenance, add OpenTelemetry log export, and expose remote-node execution to Codex when a node is connected. (#92625, #94324, #93283, #94561, #93654) Thanks @kevinslin, @kevinlin-openai, @momothemage, @nmccready-tars, @jesse-merhi, @vincentkoc, and @JPKay-AI.
|
||||
- QA and release engineering: QA scenarios now use YAML, with broader profile evidence and release coverage for the plugin and channel matrix. Thanks @vincentkoc.
|
||||
- Codex and skills: add automatic plugin approvals, preserve ClawHub skill provenance, and expose remote-node execution to Codex when a node is connected. (#92625, #93283, #93654) Thanks @kevinslin, @momothemage, @nmccready-tars, @vincentkoc, and @JPKay-AI.
|
||||
- QA and release engineering: QA scenarios now use YAML, with broader profile evidence and release coverage for the plugin and channel matrix.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security and privacy: redact secrets from debug/config output, block internal HTTP session overrides, audit open-DM tool exposure, and retain plugin write ownership checks. (#93333, #88496, #93443, #92883, #93353) Thanks @Alix-007, @jason-allen-oneal, @coygeek, @RichardCao, @yu-xin-c, @cjg20ss, @eleqtrizit, and @vincentkoc.
|
||||
- Agent and session runtime: retry thinking-only and empty post-tool turns, prevent duplicate hook execution, preserve pending subagent delivery, preserve fresh usage through compaction, and repair partial JSON/history artifacts. (#92191, #93073, #93009, #93084, #93469, #94349, #92383, #94257) Thanks @ai-hpc, @lml2468, @fuller-stack-dev, @zenglingbiao, @dertbv, @Hollychou924, @leno23, @de1tydev, @425072024, @wuwahe3, @drvoss, @vincentkoc, @sallyom, @oiGaDio, @Hidetsugu55, and @Nas01010101.
|
||||
- Channels and replies: fix Telegram rich delivery, table rendering, action-error handling, progress draft cleanup before visible tool output, and ingress recovery; preserve command progress detail across channel adapters; retain WhatsApp opening text after a media failure; keep Mattermost thread replies intact; and harden Discord action handling. (#93286, #93364, #93281, #93002, #93076, #93334, #93424, #93488, #94868, #94891, #94856, #94810, #93823) Thanks @obviyus, @NianJiuZst, @mcaxtr, @zhangguiping-xydt, @rushindrasinha, @amknight, @lzyyzznl, @darealgege, @vincentkoc, @zhangqueping, @jairrab, @ZOOWH, @parveshsaini, and @yetval.
|
||||
- Agent and session runtime: retry thinking-only and empty post-tool turns, prevent duplicate hook execution, preserve fresh usage through compaction, and repair partial JSON/history artifacts. (#92191, #93073, #93009, #93084, #93469) Thanks @ai-hpc, @lml2468, @fuller-stack-dev, @zenglingbiao, @dertbv, @Hollychou924, @leno23, @de1tydev, @425072024, @wuwahe3, @drvoss, and @vincentkoc.
|
||||
- Channels and replies: fix Telegram rich delivery and ingress recovery, preserve WhatsApp auth and media error reporting, keep Mattermost thread replies intact, and harden Discord action handling. (#93286, #93364, #93281, #93076, #93334, #93424, #93488) Thanks @obviyus, @NianJiuZst, @mcaxtr, @rushindrasinha, @amknight, @lzyyzznl, @darealgege, and @vincentkoc.
|
||||
- Storage and migrations: avoid SQLite WAL on network filesystems, clean reindex artifacts, keep setup state out of workspace dot-directories, and import default-agent auth profiles into SQLite. (#93454, #92891, #93182, #93295, #93520, #93156) Thanks @vincentkoc, @ZengWen-DT, @Zeng-wen, @potterdigital, @Alix-007, @Pick-cat, @sallyom, @1qh, and @Tazio7.
|
||||
- Provider and model behavior: fix Gemini CLI proxy OAuth, restore Codex Spark OAuth routing, correct Bedrock embedding model IDs, and preserve configured defaults in embedded runs. (#92815, #89133, #93452, #93428) Thanks @yetval, @EvetteYoung, @VACInc, @LiuwqGit, @aleck31, @zenglingbiao, @danielgerlag, and @vincentkoc.
|
||||
- CLI, TUI, and apps: accept global flags after subcommands, keep terminal output and activity indicators visible, preserve CJK IME composition, and refresh stale UI state. (#93455, #93460, #93006, #93427, #93498, #93606) Thanks @ooiuuii, @Alix-007, @ZengWen-DT, @Zeng-wen, @AlethiaQuizForge, @Zhaoqj2016, @liuhao1024, @BrianClaw1955, @vincentkoc, and @NicoBoom13.
|
||||
- Operations and updates: harden official plugin recovery, restart managed Gateways after failed update handoff, keep safe cron delivery defaults, avoid Node-specific npm prefixes, and keep package validation paths reliable. (#93325, #92111, #93650, #94453, #91685) Thanks @vincentkoc, @yetval, @ofan, @yaanfpv, @jincheng-xydt, @sallyom, @davectr, and @nxmxbbd.
|
||||
- Operations and updates: harden official plugin recovery, restart managed Gateways after failed update handoff, avoid Node-specific npm prefixes, and keep package validation paths reliable. (#93325, #92111, #93650) Thanks @vincentkoc, @yetval, @ofan, and @yaanfpv.
|
||||
|
||||
### Complete contribution record
|
||||
|
||||
This audited record covers the complete v2026.6.8..HEAD history: 423 merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.
|
||||
This audited record covers the complete v2026.6.8..HEAD~1 history: 375 merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.
|
||||
|
||||
#### Pull requests
|
||||
|
||||
@@ -57,7 +57,6 @@ This audited record covers the complete v2026.6.8..HEAD history: 423 merged PRs.
|
||||
- **PR #88792** fix(state): harden sqlite path caching. Thanks @vincentkoc.
|
||||
- **PR #93022** fix(gateway): repair usage cost aggregation across agents. Thanks @luke-skywalker-open-claw and @stablegenius49.
|
||||
- **PR #93020** fix(telegram): cool down transient sendChatAction failures. Related #56096. Thanks @Boulea7 and @sumaiazaman and @Pick-cat and @cal-rufus.
|
||||
- **PR #93002** fix(telegram): clear progress drafts before visible tool output. Thanks @zhangguiping-xydt.
|
||||
- **PR #89160** fix(agents): detect truncated API responses to prevent silent session hang. Related #89051. Thanks @joelnishanth and @ArthurusDent.
|
||||
- **PR #93009** fix(agents): make wrapToolWithBeforeToolCallHook idempotent to prevent double hook execution (fixes #92973). Thanks @zenglingbiao and @dertbv.
|
||||
- **PR #92991** fix(agents): tolerate missing attribution baseUrl. Related #92974. Thanks @samrusani and @Haderach-Ram.
|
||||
@@ -176,7 +175,7 @@ This audited record covers the complete v2026.6.8..HEAD history: 423 merged PRs.
|
||||
- **PR #90003** feat(policy): cover exec approvals artifact. Thanks @giodl73-repo.
|
||||
- **PR #93448** fix(guards): allow auth profile sqlite reader. Thanks @amknight.
|
||||
- **PR #93424** fix(mattermost): keep message tool replies in threads. Thanks @amknight and @vincentkoc.
|
||||
- **PR #93418** fix(telegram): forward Bot API 10.1 rich_message content to agent. Related #93410. Thanks @xzh-icenter and @vincentkoc and @0pen7ech.
|
||||
- **PR #93418** fix(telegram): forward Bot API 10.1 rich_message content to agent. Related #93410. Thanks @xzh-xydt and @vincentkoc and @0pen7ech.
|
||||
- **PR #93175** test(qa): taxonomy profiles: includeAllCategories for release profile, update some coverage. Thanks @RomneyDa.
|
||||
- **PR #93456** fix(agents): handle string assistant message content. Thanks @vincentkoc.
|
||||
- **PR #93441** fix(outbound): ignore schema-padded poll metadata on send. Related #43015. Thanks @weichengdeng and @charzhou.
|
||||
@@ -413,53 +412,6 @@ This audited record covers the complete v2026.6.8..HEAD history: 423 merged PRs.
|
||||
- **PR #94658** test(sqlite): use shared temp directory helper. Thanks @vincentkoc.
|
||||
- **PR #92135** fix(openai-embedding): preserve openai/ prefix for non-native base URLs. Related #92124. Thanks @xialonglee and @Kambrian.
|
||||
- **PR #93737** refactor: add session maintenance transaction seam. Thanks @jalehman.
|
||||
- **PR #93685** refactor(auto-reply): add lifecycle storage seams. Thanks @jalehman.
|
||||
- **PR #94349** fix(agents): preserve pending subagent completion announces. Related #93323. Thanks @sallyom and @oiGaDio.
|
||||
- **PR #93174** test: fold channel message flows into qa e2e. Thanks @RomneyDa.
|
||||
- **PR #94093** Prevent Codex thread rotation from losing next-step context. Thanks @VACInc.
|
||||
- **PR #53920** fix(scripts): avoid mutating tracked auth-monitor template during setup. Thanks @JackWuGlobal.
|
||||
- **PR #94702** Standardize QA coverage IDs on dotted names. Thanks @RomneyDa.
|
||||
- **PR #81825** fix(skills/1password): stop forcing tmux for desktop app auth (#52540). Thanks @koshaji and @tylerbittner.
|
||||
- **PR #94725** fix(doctor): warn on volatile SQLite state. Thanks @vincentkoc.
|
||||
- **PR #88551** fix(agents): skip auth gate for CLI-owned transport. Thanks @yu-xin-c.
|
||||
- **PR #88581** feat(commands): add /name to rename the current session from chat. Thanks @BSG2000.
|
||||
- **PR #94324** feat(codex): support app-server SecretRefs. Thanks @kevinlin-openai and @kevinslin.
|
||||
- **PR #90882** fix: add self-knowledge docs rule to system prompt. Related #90713. Thanks @SutraHsing.
|
||||
- **PR #94684** fix: #80507 show dry-run output for message send/poll. Thanks @lzyyzznl and @YB0y.
|
||||
- **PR #93823** fix(whatsapp): keep opening text chunk when first media fails on multi-chunk reply. Thanks @yetval.
|
||||
- **PR #89203** refactor: route SDK session compatibility through seam. Thanks @jalehman.
|
||||
- **PR #94453** fix: default cron runMode to "due" instead of "force" (#94270). Thanks @jincheng-xydt and @sallyom and @davectr.
|
||||
- **PR #94746** fix(note): prevent clack from re-breaking copy-sensitive tokens. Related #94730. Thanks @xzh-icenter and @berkgungor.
|
||||
- **PR #89904** refactor: route sdk session compatibility through accessor. Thanks @jalehman.
|
||||
- **PR #86719** fix(skills): retarget stale plugin skill symlinks. Related #85925. Thanks @stevenepalmer and @shakkernerd.
|
||||
- **PR #94337** fix(tui): show 0 not ? for fresh-session context tokens in footer. Thanks @mushuiyu886.
|
||||
- **PR #94539** fix(android): group settings by intent. Thanks @Tosko4.
|
||||
- **PR #92383** fix(gateway): never return an empty chat.history transcript. Thanks @Hidetsugu55.
|
||||
- **PR #92574** test(browser): cover action-input CLI request bodies. Related #83877. Thanks @yu-xin-c and @davinci282828.
|
||||
- **PR #92873** test(diffs): add viewerState, toolbar toggle, shadow root, and hydrateProps tests (fixes #83915). Thanks @liuhao1024 and @davinci282828.
|
||||
- **PR #94257** fix(sessions): preserve Media\* index alignment when reading user-turn fields. Thanks @Nas01010101.
|
||||
- **PR #94756** fix(codex): bound turn/start text when context budget is non-positive. Related #94748. Thanks @Nas01010101.
|
||||
- **PR #94729** fix(skills/trello): add curl to requires.bins to match body examples (fixes #94727). Thanks @liuhao1024 and @berkgungor.
|
||||
- **PR #94790** feat(slack): log INFO receipt for inbound app_mention events. Related #94691. Thanks @ZengWen-DT and @BryceMurray.
|
||||
- **PR #81696** fix: guard tool event callbacks (AI-assisted). Thanks @enjoylife1243.
|
||||
- **PR #94809** chore: forward-port alpha release fixes.
|
||||
- **PR #94612** fix(macos): open NSOpenPanel for embedded Control UI file inputs (#94468). Thanks @bbblending and @DINGDANGMAOUP.
|
||||
- **PR #89806** fix(feishu): avoid axios interceptor internals. Related #83913. Thanks @sweetcornna and @davinci282828.
|
||||
- **PR #91923** fix(ios): clean up notification settings state. Thanks @zats.
|
||||
- **PR #91345** fix: suggest close CLI commands. Related #83999. Thanks @glenn-agent and @HannesOberreiter.
|
||||
- **PR #94561** Add stdout diagnostics OTEL log exporter. Thanks @jesse-merhi.
|
||||
- **PR #91013** fix(gateway): ignore stale abort markers for fresh chat events. Related #91012. Thanks @nxmxbbd.
|
||||
- **PR #89279** fix(tasks): deliver ACP completions to bound Discord threads. Related #84022. Thanks @anyech and @h-mascot.
|
||||
- **PR #91656** test(cron): expand parseAbsoluteTimeMs test coverage to 39 cases. Related #91654. Thanks @SpecialLeon.
|
||||
- **PR #94810** fix(telegram): classify sendChatAction 401 by structured error_code, not bare substring match. Related #94787. Thanks @ZOOWH and @parveshsaini.
|
||||
- **PR #94737** fix(reply): clarify provider internal error copy. Thanks @snowzlmbot.
|
||||
- **PR #94868** fix(channels): preserve command progress detail. Thanks @vincentkoc.
|
||||
- **PR #94891** fix(telegram): send progress previews as html text. Thanks @obviyus.
|
||||
- **PR #94683** fix(outbound): keep direct-only targets out of group sessions. Related #92384. Thanks @scotthuang and @haiwei01.
|
||||
- **PR #92477** fix: migrate watch app to single-target app (Xcode 27+ compat). Thanks @zats and @joshavant.
|
||||
- **PR #94812** test(perf): compare saved CLI startup benchmarks. Thanks @FelixIsaac.
|
||||
- **PR #94856** fix(telegram): normalize all HTML tables before entity-escaping in rich messages. Related #94317. Thanks @zhangqueping and @jairrab.
|
||||
- **PR #91685** fix(cron): refuse keyless implicit isolated cron delivery inherited from shared agent-main bucket. Thanks @nxmxbbd.
|
||||
|
||||
## 2026.6.8
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# Source of truth: apps/android/version.json
|
||||
# Generated by scripts/android-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_ANDROID_VERSION_NAME=2026.6.9
|
||||
OPENCLAW_ANDROID_VERSION_CODE=2026060901
|
||||
OPENCLAW_ANDROID_VERSION_NAME=2026.6.2
|
||||
OPENCLAW_ANDROID_VERSION_CODE=2026060201
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
Maintenance update for the current OpenClaw Android release.
|
||||
OpenClaw is now available on Android.
|
||||
|
||||
Connect to your OpenClaw Gateway to chat with your assistant, use realtime Talk mode, review approvals, and bring Android device capabilities like camera, location, screen, and notifications into your private automation workflows.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "2026.6.9",
|
||||
"versionCode": 2026060901
|
||||
"version": "2026.6.2",
|
||||
"versionCode": 2026060201
|
||||
}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.6.9 - 2026-06-20
|
||||
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
- Added Apple Watch controls for common agent actions.
|
||||
- Improved Gateway setup, notification settings, and share-extension identity handling.
|
||||
- Updated the Watch app integration for current Xcode compatibility.
|
||||
|
||||
## 2026.6.2 - 2026-06-02
|
||||
|
||||
OpenClaw is now available on iPhone.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.6.9
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.9
|
||||
OPENCLAW_IOS_VERSION = 2026.6.2
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.2
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -54,8 +54,6 @@ struct SettingsProTab: View {
|
||||
@State var locationStatusText: String?
|
||||
@State var previousLocationModeRaw: String = OpenClawLocationMode.off.rawValue
|
||||
@State var notificationStatus: SettingsNotificationStatus = .checking
|
||||
@State var isRequestingNotificationAuthorization = false
|
||||
@State var showNotificationRelayDisclosure = false
|
||||
@State var diagnosticsLastRunText = "Not run"
|
||||
@State var diagnosticsIssueCount: Int?
|
||||
@State var showTalkIssueDetails = false
|
||||
@@ -63,18 +61,15 @@ struct SettingsProTab: View {
|
||||
let initialRoute: SettingsRoute?
|
||||
let directRoute: SettingsRoute?
|
||||
let headerLeadingAction: OpenClawSidebarHeaderAction?
|
||||
let onRouteChange: ((SettingsRoute?) -> Void)?
|
||||
|
||||
init(
|
||||
initialRoute: SettingsRoute? = nil,
|
||||
directRoute: SettingsRoute? = nil,
|
||||
headerLeadingAction: OpenClawSidebarHeaderAction? = nil,
|
||||
onRouteChange: ((SettingsRoute?) -> Void)? = nil)
|
||||
headerLeadingAction: OpenClawSidebarHeaderAction? = nil)
|
||||
{
|
||||
self.initialRoute = initialRoute
|
||||
self.directRoute = directRoute
|
||||
self.headerLeadingAction = headerLeadingAction
|
||||
self.onRouteChange = onRouteChange
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -122,7 +117,6 @@ struct SettingsProTab: View {
|
||||
self.refreshNotificationSettings()
|
||||
self.applyPendingGatewaySetupLinkIfNeeded()
|
||||
self.applyInitialRouteIfNeeded()
|
||||
self.notifyRouteChange()
|
||||
}
|
||||
.onChange(of: self.scenePhase) { _, phase in
|
||||
if phase == .active {
|
||||
@@ -159,9 +153,6 @@ struct SettingsProTab: View {
|
||||
.onChange(of: self.appModel.gatewaySetupRequestID) { _, _ in
|
||||
self.applyPendingGatewaySetupLinkIfNeeded()
|
||||
}
|
||||
.onChange(of: self.navigationPath) { _, _ in
|
||||
self.notifyRouteChange()
|
||||
}
|
||||
}
|
||||
|
||||
private func settingsModalPresentation(_ content: some View) -> some View {
|
||||
@@ -226,19 +217,6 @@ struct SettingsProTab: View {
|
||||
} message: {
|
||||
Text(self.scannerError ?? "")
|
||||
}
|
||||
.alert("Enable OpenClaw Hosted Push Relay?", isPresented: self.$showNotificationRelayDisclosure) {
|
||||
Button("Continue") {
|
||||
self.requestNotificationAuthorizationFromSettings()
|
||||
}
|
||||
Button("Not Now", role: .cancel) {}
|
||||
} message: {
|
||||
Text(self.notificationRelayDisclosureMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func openNotificationsRouteFromApprovals() {
|
||||
guard self.directRoute == nil else { return }
|
||||
self.navigationPath = [.notifications]
|
||||
}
|
||||
|
||||
private func applyInitialRouteIfNeeded() {
|
||||
@@ -247,12 +225,4 @@ struct SettingsProTab: View {
|
||||
guard self.navigationPath != [initialRoute] else { return }
|
||||
self.navigationPath = [initialRoute]
|
||||
}
|
||||
|
||||
private func notifyRouteChange() {
|
||||
if let directRoute {
|
||||
self.onRouteChange?(directRoute)
|
||||
return
|
||||
}
|
||||
self.onRouteChange?(self.navigationPath.last)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,30 +426,15 @@ extension SettingsProTab {
|
||||
self.openNotificationSettings()
|
||||
return
|
||||
}
|
||||
guard self.notificationStatus == .notSet else { return }
|
||||
|
||||
if PushBuildConfig.current.usesOpenClawHostedRelay {
|
||||
self.showNotificationRelayDisclosure = true
|
||||
return
|
||||
}
|
||||
self.requestNotificationAuthorizationFromSettings()
|
||||
}
|
||||
|
||||
func requestNotificationAuthorizationFromSettings() {
|
||||
guard !self.isRequestingNotificationAuthorization else { return }
|
||||
self.isRequestingNotificationAuthorization = true
|
||||
Task {
|
||||
let granted = await (try? UNUserNotificationCenter.current().requestAuthorization(options: [
|
||||
.alert,
|
||||
.badge,
|
||||
.sound,
|
||||
])) ?? false
|
||||
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||||
await MainActor.run {
|
||||
self.isRequestingNotificationAuthorization = false
|
||||
self.notificationStatus = SettingsNotificationStatus(settings.authorizationStatus)
|
||||
guard granted, self.notificationStatus.allowsNotifications else { return }
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
self.notificationStatus = granted ? .allowed : .notAllowed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -676,9 +661,6 @@ extension SettingsProTab {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return "Live gateway requests are disabled in demo mode."
|
||||
}
|
||||
if self.notificationsNeedAttention {
|
||||
return "Foreground approvals still appear while OpenClaw is connected."
|
||||
}
|
||||
return self.gatewayConnected ? "Gateway requests will appear here." : "Connect to the gateway."
|
||||
}
|
||||
|
||||
@@ -718,19 +700,7 @@ extension SettingsProTab {
|
||||
}
|
||||
|
||||
var approvalsDetail: String {
|
||||
if self.notificationsNeedAttention {
|
||||
return self.pendingApproval == nil ? "Notifications off" : "1 waiting, notifications off"
|
||||
}
|
||||
return self.pendingApproval == nil ? "No approvals waiting" : "1 request waiting"
|
||||
}
|
||||
|
||||
var notificationsNeedAttention: Bool {
|
||||
switch self.notificationStatus {
|
||||
case .allowed, .checking:
|
||||
false
|
||||
case .notAllowed, .notSet, .unknown:
|
||||
true
|
||||
}
|
||||
self.pendingApproval == nil ? "No approvals waiting" : "1 request waiting"
|
||||
}
|
||||
|
||||
var approvalItems: [SettingsApprovalItem] {
|
||||
@@ -801,33 +771,4 @@ extension SettingsProTab {
|
||||
var notificationActionText: String {
|
||||
self.notificationStatus.actionTitle
|
||||
}
|
||||
|
||||
var notificationStatusDetail: String {
|
||||
switch self.notificationStatus {
|
||||
case .checking:
|
||||
"Checking iOS notification permission."
|
||||
case .allowed:
|
||||
"OpenClaw can show approval prompts and event alerts when the app is not active."
|
||||
case .notAllowed:
|
||||
"Notifications have been denied. Enable them in iOS Settings."
|
||||
case .notSet:
|
||||
"Enable notifications to receive approval prompts and event alerts outside the app."
|
||||
case .unknown:
|
||||
"OpenClaw cannot determine the current notification permission state."
|
||||
}
|
||||
}
|
||||
|
||||
var notificationRelayDetail: String {
|
||||
if PushBuildConfig.current.usesOpenClawHostedRelay {
|
||||
return """
|
||||
This build uses OpenClaw's hosted push relay at ios-push-relay.openclaw.ai for notification \
|
||||
delivery data.
|
||||
"""
|
||||
}
|
||||
return "This build is not configured to use OpenClaw's hosted push relay."
|
||||
}
|
||||
|
||||
var notificationRelayDisclosureMessage: String {
|
||||
"Enabling this sends delivery data through OpenClaw's hosted push relay."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,57 +308,15 @@ extension SettingsProTab {
|
||||
self.detailStatusCard(
|
||||
icon: "checkmark.shield.fill",
|
||||
title: "Approvals",
|
||||
detail: self.notificationsNeedAttention
|
||||
? "Out-of-app approval alerts need notification permission."
|
||||
: (self.pendingApproval == nil ? "No gateway actions are waiting for review." :
|
||||
"Review the pending gateway action."),
|
||||
value: self.notificationsNeedAttention
|
||||
? "Alerts Off"
|
||||
: (self.pendingApproval == nil ? "clear" : "1 waiting"),
|
||||
color: self.notificationsNeedAttention ? OpenClawBrand.warn :
|
||||
(self.pendingApproval == nil ? OpenClawBrand.ok : OpenClawBrand.warn))
|
||||
|
||||
if self.notificationsNeedAttention {
|
||||
self.approvalNotificationsWarningCard
|
||||
}
|
||||
detail: self.pendingApproval == nil ? "No gateway actions are waiting for review." :
|
||||
"Review the pending gateway action.",
|
||||
value: self.pendingApproval == nil ? "clear" : "1 waiting",
|
||||
color: self.pendingApproval == nil ? OpenClawBrand.ok : OpenClawBrand.warn)
|
||||
|
||||
self.approvalsReviewCard
|
||||
}
|
||||
}
|
||||
|
||||
var approvalNotificationsWarningCard: some View {
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
ProIconBadge(systemName: "bell.slash.fill", color: OpenClawBrand.warn)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Notifications are off")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text(
|
||||
"""
|
||||
Enable Notifications to receive approval notifications while OpenClaw is not open.
|
||||
""")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
if self.directRoute == nil {
|
||||
Button {
|
||||
self.openNotificationsRouteFromApprovals()
|
||||
} label: {
|
||||
Label("Open Notifications", systemImage: "bell.badge")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
}
|
||||
|
||||
var approvalsReviewCard: some View {
|
||||
ProCard(radius: SettingsLayout.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@@ -532,7 +490,7 @@ extension SettingsProTab {
|
||||
self.detailStatusCard(
|
||||
icon: "bell",
|
||||
title: "Notifications",
|
||||
detail: self.notificationStatusDetail,
|
||||
detail: "Approvals and event alerts from OpenClaw.",
|
||||
value: self.notificationStatusText,
|
||||
color: self.notificationStatus.color)
|
||||
|
||||
@@ -548,25 +506,10 @@ extension SettingsProTab {
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
.disabled(self.notificationStatus == .checking || self.isRequestingNotificationAuthorization)
|
||||
|
||||
Text(self.notificationStatusDetail)
|
||||
Text("OpenClaw uses notifications for approval prompts and mirrored event alerts.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Divider()
|
||||
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "network")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.frame(width: 22, height: 22)
|
||||
Text(self.notificationRelayDetail)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
|
||||
@@ -89,48 +89,28 @@ enum SettingsNotificationStatus: Equatable {
|
||||
var text: String {
|
||||
switch self {
|
||||
case .checking: "Checking"
|
||||
case .allowed: "Enabled"
|
||||
case .notAllowed: "Denied"
|
||||
case .notSet: "Not Enabled"
|
||||
case .allowed: "Allowed"
|
||||
case .notAllowed: "Not Allowed"
|
||||
case .notSet: "Not Set"
|
||||
case .unknown: "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
var actionTitle: String {
|
||||
switch self {
|
||||
case .notSet:
|
||||
"Enable Notifications"
|
||||
case .checking:
|
||||
"Checking"
|
||||
case .allowed:
|
||||
"Manage in iOS Settings"
|
||||
case .notAllowed, .unknown:
|
||||
"Open iOS Settings"
|
||||
case .notSet, .checking:
|
||||
"Request Access"
|
||||
case .allowed, .notAllowed, .unknown:
|
||||
"Open System Settings"
|
||||
}
|
||||
}
|
||||
|
||||
var actionIcon: String {
|
||||
switch self {
|
||||
case .allowed:
|
||||
"gear"
|
||||
case .notAllowed, .unknown:
|
||||
"gear.badge"
|
||||
case .checking:
|
||||
"hourglass"
|
||||
case .notSet:
|
||||
"bell.badge"
|
||||
}
|
||||
self == .allowed ? "gear" : "bell.badge"
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .allowed:
|
||||
OpenClawBrand.ok
|
||||
case .notAllowed, .unknown:
|
||||
OpenClawBrand.warn
|
||||
case .checking, .notSet:
|
||||
.secondary
|
||||
}
|
||||
self == .allowed ? OpenClawBrand.ok : .secondary
|
||||
}
|
||||
|
||||
var shouldOpenNotificationSettings: Bool {
|
||||
@@ -141,10 +121,6 @@ enum SettingsNotificationStatus: Equatable {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
var allowsNotifications: Bool {
|
||||
self == .allowed
|
||||
}
|
||||
}
|
||||
|
||||
enum SettingsDiagnosticIssue: String, Equatable, CaseIterable {
|
||||
|
||||
@@ -2,14 +2,11 @@ import SwiftUI
|
||||
|
||||
private struct ExecApprovalPromptDialogModifier: ViewModifier {
|
||||
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
|
||||
let suppressedApprovalID: String?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay {
|
||||
if let prompt = self.appModel.pendingExecApprovalPrompt,
|
||||
prompt.id != self.suppressedApprovalID
|
||||
{
|
||||
if let prompt = self.appModel.pendingExecApprovalPrompt {
|
||||
ZStack {
|
||||
Color.black.opacity(0.38)
|
||||
.ignoresSafeArea()
|
||||
@@ -61,7 +58,7 @@ private struct ExecApprovalPromptCard: View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Exec approval required")
|
||||
.font(.headline)
|
||||
Text("Review this exec request before continuing. Your decision will be sent back to the gateway.")
|
||||
Text("OpenClaw opened from a notification. Review this exec request before continuing.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -191,7 +188,7 @@ private struct ExecApprovalPromptMetadataRow: View {
|
||||
}
|
||||
|
||||
extension View {
|
||||
func execApprovalPromptDialog(suppressedApprovalID: String? = nil) -> some View {
|
||||
self.modifier(ExecApprovalPromptDialogModifier(suppressedApprovalID: suppressedApprovalID))
|
||||
func execApprovalPromptDialog() -> some View {
|
||||
self.modifier(ExecApprovalPromptDialogModifier())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
private struct NotificationPermissionGuidanceDialogModifier: ViewModifier {
|
||||
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
|
||||
let openNotifications: (String) -> Void
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay {
|
||||
if let prompt = self.appModel.pendingNotificationPermissionGuidancePrompt {
|
||||
ZStack {
|
||||
Color.black.opacity(0.38)
|
||||
.ignoresSafeArea()
|
||||
|
||||
NotificationPermissionGuidanceCard(
|
||||
onOpenNotifications: {
|
||||
let approvalId = prompt.approvalId
|
||||
self.appModel.dismissNotificationPermissionGuidancePrompt(
|
||||
suppressFuture: false)
|
||||
self.openNotifications(approvalId)
|
||||
},
|
||||
onDismiss: {
|
||||
self.appModel.dismissNotificationPermissionGuidancePrompt(
|
||||
suppressFuture: false)
|
||||
},
|
||||
onSuppressFuture: {
|
||||
self.appModel.dismissNotificationPermissionGuidancePrompt(
|
||||
suppressFuture: true)
|
||||
})
|
||||
.padding(.horizontal, 20)
|
||||
.frame(maxWidth: 460)
|
||||
.transition(.scale(scale: 0.98).combined(with: .opacity))
|
||||
}
|
||||
.zIndex(2)
|
||||
.id(prompt.id)
|
||||
}
|
||||
}
|
||||
.animation(
|
||||
.easeInOut(duration: 0.18),
|
||||
value: self.appModel.pendingNotificationPermissionGuidancePrompt?.id)
|
||||
}
|
||||
}
|
||||
|
||||
private struct NotificationPermissionGuidanceCard: View {
|
||||
let onOpenNotifications: () -> Void
|
||||
let onDismiss: () -> Void
|
||||
let onSuppressFuture: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Notifications are off")
|
||||
.font(.headline)
|
||||
Text(
|
||||
"""
|
||||
Exec approvals can only be reviewed while OpenClaw is open and connected.
|
||||
|
||||
Enable Notifications to receive approval notifications while OpenClaw is not open.
|
||||
""")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
VStack(spacing: 10) {
|
||||
Button {
|
||||
self.onOpenNotifications()
|
||||
} label: {
|
||||
Text("Open Notifications Settings")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
Button(role: .cancel) {
|
||||
self.onDismiss()
|
||||
} label: {
|
||||
Text("Not Now")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button {
|
||||
self.onSuppressFuture()
|
||||
} label: {
|
||||
Text("Don't show again")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
.controlSize(.large)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(18)
|
||||
.proPanelSurface(tint: OpenClawBrand.warn, radius: 20, isProminent: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func notificationPermissionGuidanceDialog(openNotifications: @escaping (String) -> Void) -> some View {
|
||||
self.modifier(NotificationPermissionGuidanceDialogModifier(openNotifications: openNotifications))
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,6 @@ private struct WatchChatPreview {
|
||||
var statusText: String?
|
||||
}
|
||||
|
||||
private struct ExecApprovalGatewayEventPayload: Decodable {
|
||||
var id: String
|
||||
}
|
||||
|
||||
/// Ensures notification requests return promptly even if the system prompt blocks.
|
||||
private final class NotificationInvokeLatch<T: Sendable>: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
@@ -87,11 +83,6 @@ final class NodeAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationPermissionGuidancePrompt: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let approvalId: String
|
||||
}
|
||||
|
||||
private enum ExecApprovalResolutionOutcome {
|
||||
case resolved
|
||||
case stale
|
||||
@@ -105,8 +96,6 @@ final class NodeAppModel {
|
||||
}
|
||||
|
||||
private let deepLinkLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "DeepLink")
|
||||
private nonisolated static let execApprovalNotificationGuidanceSuppressedKey =
|
||||
"notifications.execApprovalGuidance.suppressed"
|
||||
private let pushWakeLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "PushWake")
|
||||
private let pendingActionLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "PendingAction")
|
||||
private let locationWakeLogger = Logger(subsystem: "ai.openclawfoundation.app", category: "LocationWake")
|
||||
@@ -167,7 +156,6 @@ final class NodeAppModel {
|
||||
private(set) var pendingExecApprovalPromptResolving: Bool = false
|
||||
private(set) var pendingExecApprovalPromptErrorText: String?
|
||||
private var pendingExecApprovalPromptRequestGeneration: Int = 0
|
||||
private(set) var pendingNotificationPermissionGuidancePrompt: NotificationPermissionGuidancePrompt?
|
||||
private var queuedAgentDeepLinkPrompt: AgentDeepLinkPrompt?
|
||||
private var lastAgentDeepLinkPromptAt: Date = .distantPast
|
||||
@ObservationIgnored private var queuedAgentDeepLinkPromptTask: Task<Void, Never>?
|
||||
@@ -907,50 +895,26 @@ final class NodeAppModel {
|
||||
for await evt in stream {
|
||||
if Task.isCancelled { return }
|
||||
guard let payload = evt.payload else { continue }
|
||||
await self.handleOperatorGatewayServerEvent(evt)
|
||||
switch evt.event {
|
||||
case "voicewake.changed":
|
||||
struct Payload: Decodable { var triggers: [String] }
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue }
|
||||
let triggers = VoiceWakePreferences.sanitizeTriggerWords(decoded.triggers)
|
||||
VoiceWakePreferences.saveTriggerWords(triggers)
|
||||
case "talk.mode":
|
||||
struct Payload: Decodable {
|
||||
var enabled: Bool
|
||||
var phase: String?
|
||||
}
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue }
|
||||
self.applyTalkModeSync(enabled: decoded.enabled, phase: decoded.phase)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleOperatorGatewayServerEvent(_ evt: EventFrame) async {
|
||||
guard let payload = evt.payload else { return }
|
||||
switch evt.event {
|
||||
case "voicewake.changed":
|
||||
struct Payload: Decodable { var triggers: [String] }
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { return }
|
||||
let triggers = VoiceWakePreferences.sanitizeTriggerWords(decoded.triggers)
|
||||
VoiceWakePreferences.saveTriggerWords(triggers)
|
||||
case "talk.mode":
|
||||
struct Payload: Decodable {
|
||||
var enabled: Bool
|
||||
var phase: String?
|
||||
}
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { return }
|
||||
self.applyTalkModeSync(enabled: decoded.enabled, phase: decoded.phase)
|
||||
case ExecApprovalNotificationBridge.requestedKind:
|
||||
guard let approvalId = Self.execApprovalEventID(from: payload) else { return }
|
||||
await self.presentNotificationPermissionGuidanceForExecApprovalIfNeeded(approvalId: approvalId)
|
||||
await self.presentExecApprovalNotificationPrompt(
|
||||
ExecApprovalNotificationPrompt(approvalId: approvalId))
|
||||
case ExecApprovalNotificationBridge.resolvedKind:
|
||||
guard let approvalId = Self.execApprovalEventID(from: payload) else { return }
|
||||
await self.handleExecApprovalResolvedRemotePush(approvalId: approvalId)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func execApprovalEventID(from payload: AnyCodable) -> String? {
|
||||
guard let decoded = try? GatewayPayloadDecoding.decode(
|
||||
payload,
|
||||
as: ExecApprovalGatewayEventPayload.self)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let approvalId = decoded.id.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return approvalId.isEmpty ? nil : approvalId
|
||||
}
|
||||
|
||||
private func applyTalkModeSync(enabled: Bool, phase: String?) {
|
||||
_ = phase
|
||||
guard self.talkMode.isEnabled != enabled else { return }
|
||||
@@ -1368,8 +1332,8 @@ final class NodeAppModel {
|
||||
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: empty notification"))
|
||||
}
|
||||
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
guard Self.isNotificationAuthorizationAllowed(status) else {
|
||||
let finalStatus = await self.requestNotificationAuthorizationIfNeeded()
|
||||
guard finalStatus == .authorized || finalStatus == .provisional || finalStatus == .ephemeral else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
@@ -1421,18 +1385,9 @@ final class NodeAppModel {
|
||||
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: empty chat.push text"))
|
||||
}
|
||||
|
||||
let shouldSpeak = params.speak ?? true
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
let notificationsAllowed = Self.isNotificationAuthorizationAllowed(status)
|
||||
if !notificationsAllowed, !shouldSpeak {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(code: .unavailable, message: "NOT_AUTHORIZED: notifications"))
|
||||
}
|
||||
|
||||
let finalStatus = await self.requestNotificationAuthorizationIfNeeded()
|
||||
let messageId = UUID().uuidString
|
||||
if notificationsAllowed {
|
||||
if finalStatus == .authorized || finalStatus == .provisional || finalStatus == .ephemeral {
|
||||
let addResult = await self.runNotificationCall(timeoutSeconds: 2.0) { [notificationCenter] in
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "OpenClaw"
|
||||
@@ -1453,7 +1408,7 @@ final class NodeAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
if shouldSpeak {
|
||||
if params.speak ?? true {
|
||||
let toSpeak = text
|
||||
Task { @MainActor in
|
||||
try? await TalkSystemSpeechSynthesizer.shared.speak(text: toSpeak)
|
||||
@@ -1465,6 +1420,26 @@ final class NodeAppModel {
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
}
|
||||
|
||||
private func requestNotificationAuthorizationIfNeeded() async -> NotificationAuthorizationStatus {
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
guard status == .notDetermined else { return status }
|
||||
|
||||
// Avoid hanging invoke requests if the permission prompt is never answered.
|
||||
_ = await self.runNotificationCall(timeoutSeconds: 2.0) { [notificationCenter] in
|
||||
_ = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
let updatedStatus = await self.notificationAuthorizationStatus()
|
||||
if Self.isNotificationAuthorizationAllowed(updatedStatus) {
|
||||
// Refresh APNs registration immediately after the first permission grant so the
|
||||
// gateway can receive a push registration without requiring an app relaunch.
|
||||
await MainActor.run {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
return updatedStatus
|
||||
}
|
||||
|
||||
private func notificationAuthorizationStatus() async -> NotificationAuthorizationStatus {
|
||||
let result = await self.runNotificationCall(timeoutSeconds: 1.5) { [notificationCenter] in
|
||||
await notificationCenter.authorizationStatus()
|
||||
@@ -1488,29 +1463,6 @@ final class NodeAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentNotificationPermissionGuidanceForExecApprovalIfNeeded(approvalId: String) async {
|
||||
guard !self.execApprovalNotificationGuidanceSuppressed else { return }
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
guard !Self.isNotificationAuthorizationAllowed(status) else { return }
|
||||
self.pendingNotificationPermissionGuidancePrompt =
|
||||
NotificationPermissionGuidancePrompt(approvalId: approvalId)
|
||||
}
|
||||
|
||||
var execApprovalNotificationGuidanceSuppressed: Bool {
|
||||
UserDefaults.standard.bool(forKey: Self.execApprovalNotificationGuidanceSuppressedKey)
|
||||
}
|
||||
|
||||
func dismissNotificationPermissionGuidancePrompt(suppressFuture: Bool) {
|
||||
if suppressFuture {
|
||||
UserDefaults.standard.set(true, forKey: Self.execApprovalNotificationGuidanceSuppressedKey)
|
||||
}
|
||||
self.pendingNotificationPermissionGuidancePrompt = nil
|
||||
}
|
||||
|
||||
func resetExecApprovalNotificationGuidanceSuppression() {
|
||||
UserDefaults.standard.removeObject(forKey: Self.execApprovalNotificationGuidanceSuppressedKey)
|
||||
}
|
||||
|
||||
private func runNotificationCall<T: Sendable>(
|
||||
timeoutSeconds: Double,
|
||||
operation: @escaping @Sendable () async throws -> T) async -> Result<T, NotificationCallError>
|
||||
@@ -2383,6 +2335,10 @@ extension NodeAppModel {
|
||||
nodeOptions: nodeOptions,
|
||||
sessionBox: sessionBox)
|
||||
}
|
||||
|
||||
// QR bootstrap onboarding should surface the system notification permission
|
||||
// prompt immediately so visible APNs alerts work without a second manual step.
|
||||
_ = await self.requestNotificationAuthorizationIfNeeded()
|
||||
}
|
||||
|
||||
private func refreshBackgroundReconnectSuppressionIfNeeded(source: String) {
|
||||
@@ -3960,15 +3916,11 @@ extension NodeAppModel {
|
||||
let hadWatchPrompt = self.watchExecApprovalPromptsByID[normalizedApprovalID] != nil
|
||||
let hadPendingPrompt = self.pendingExecApprovalPrompt?.id == normalizedApprovalID
|
||||
let hadPendingRecoveryID = self.pendingWatchExecApprovalRecoveryIDs.contains(normalizedApprovalID)
|
||||
let hadGuidancePrompt = self.pendingNotificationPermissionGuidancePrompt?.approvalId == normalizedApprovalID
|
||||
let hadApprovalSurface = hadWatchPrompt || hadPendingPrompt || hadPendingRecoveryID
|
||||
guard hadApprovalSurface || hadGuidancePrompt else {
|
||||
guard hadWatchPrompt || hadPendingPrompt || hadPendingRecoveryID else {
|
||||
return
|
||||
}
|
||||
|
||||
if hadApprovalSurface {
|
||||
await self.publishWatchExecApprovalExpired(approvalId: normalizedApprovalID, reason: .resolved)
|
||||
}
|
||||
await self.publishWatchExecApprovalExpired(approvalId: normalizedApprovalID, reason: .resolved)
|
||||
self.clearPendingExecApprovalPromptIfMatches(normalizedApprovalID)
|
||||
}
|
||||
|
||||
@@ -4449,17 +4401,10 @@ extension NodeAppModel {
|
||||
|
||||
private func clearPendingExecApprovalPromptIfMatches(_ approvalId: String) {
|
||||
let normalizedApprovalID = approvalId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.clearNotificationPermissionGuidancePromptIfMatches(normalizedApprovalID)
|
||||
guard self.pendingExecApprovalPrompt?.id == normalizedApprovalID else { return }
|
||||
self.dismissPendingExecApprovalPrompt()
|
||||
}
|
||||
|
||||
private func clearNotificationPermissionGuidancePromptIfMatches(_ approvalId: String) {
|
||||
let normalizedApprovalID = approvalId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard self.pendingNotificationPermissionGuidancePrompt?.approvalId == normalizedApprovalID else { return }
|
||||
self.pendingNotificationPermissionGuidancePrompt = nil
|
||||
}
|
||||
|
||||
private nonisolated static func isApprovalNotificationStaleError(_ error: Error) -> Bool {
|
||||
guard let gatewayError = error as? GatewayResponseError else { return false }
|
||||
if gatewayError.code != "INVALID_REQUEST" {
|
||||
@@ -5165,20 +5110,6 @@ extension NodeAppModel {
|
||||
self.pendingExecApprovalPrompt
|
||||
}
|
||||
|
||||
func _test_pendingNotificationPermissionGuidancePrompt() -> NotificationPermissionGuidancePrompt? {
|
||||
self.pendingNotificationPermissionGuidancePrompt
|
||||
}
|
||||
|
||||
func _debug_presentNotificationPermissionGuidancePromptForScreenshot() {
|
||||
self.resetExecApprovalNotificationGuidanceSuppression()
|
||||
self.pendingNotificationPermissionGuidancePrompt =
|
||||
NotificationPermissionGuidancePrompt(approvalId: "screenshot-exec-approval")
|
||||
}
|
||||
|
||||
func _test_resetExecApprovalNotificationGuidanceSuppression() {
|
||||
self.resetExecApprovalNotificationGuidanceSuppression()
|
||||
}
|
||||
|
||||
func _test_recordPendingWatchExecApprovalRecoveryID(_ approvalId: String) {
|
||||
self.appendPendingWatchExecApprovalRecoveryID(approvalId)
|
||||
}
|
||||
@@ -5208,14 +5139,6 @@ extension NodeAppModel {
|
||||
isBackgrounded: isBackgrounded)
|
||||
}
|
||||
|
||||
nonisolated static func _test_execApprovalEventID(from payload: AnyCodable) -> String? {
|
||||
self.execApprovalEventID(from: payload)
|
||||
}
|
||||
|
||||
func _test_handleOperatorGatewayServerEvent(_ event: EventFrame) async {
|
||||
await self.handleOperatorGatewayServerEvent(event)
|
||||
}
|
||||
|
||||
nonisolated static func _test_watchExecApprovalIDsNeedingFetch(
|
||||
candidateIDs: [String],
|
||||
cachedApprovalIDs: [String]) -> [String]
|
||||
@@ -5312,6 +5235,24 @@ extension NodeAppModel {
|
||||
func _test_restartGatewaySessionsAfterForegroundStaleConnection() async {
|
||||
await self.restartGatewaySessionsAfterForegroundStaleConnection()
|
||||
}
|
||||
|
||||
func _test_handleSuccessfulBootstrapGatewayOnboarding() async {
|
||||
await self.handleSuccessfulBootstrapGatewayOnboarding(
|
||||
url: URL(string: "wss://gateway.example")!,
|
||||
stableID: "test-gateway",
|
||||
token: nil,
|
||||
password: nil,
|
||||
nodeOptions: GatewayConnectOptions(
|
||||
role: "node",
|
||||
scopes: [],
|
||||
caps: [],
|
||||
commands: [],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-ios",
|
||||
clientMode: "node",
|
||||
clientDisplayName: nil),
|
||||
sessionBox: nil)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// swiftlint:enable type_body_length file_length
|
||||
|
||||
@@ -409,7 +409,7 @@ enum WatchPromptNotificationBridge {
|
||||
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let body = params.body.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !title.isEmpty || !body.isEmpty else { return }
|
||||
guard await self.isNotificationAuthorizationAllowed() else { return }
|
||||
guard await self.requestNotificationAuthorizationIfNeeded() else { return }
|
||||
|
||||
let normalizedActions = (params.actions ?? []).compactMap { action -> OpenClawWatchAction? in
|
||||
let id = action.id.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -516,10 +516,29 @@ enum WatchPromptNotificationBridge {
|
||||
}
|
||||
}
|
||||
|
||||
private static func isNotificationAuthorizationAllowed() async -> Bool {
|
||||
private static func requestNotificationAuthorizationIfNeeded() async -> Bool {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let status = await self.notificationAuthorizationStatus(center: center)
|
||||
return self.isAuthorizationStatusAllowed(status)
|
||||
switch status {
|
||||
case .authorized, .provisional, .ephemeral:
|
||||
return true
|
||||
case .notDetermined:
|
||||
let granted = await (try? center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
|
||||
if !granted { return false }
|
||||
let updatedStatus = await self.notificationAuthorizationStatus(center: center)
|
||||
if self.isAuthorizationStatusAllowed(updatedStatus) {
|
||||
// Refresh APNs registration immediately after the first permission grant so the
|
||||
// gateway can receive a push registration without requiring an app relaunch.
|
||||
await MainActor.run {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
return self.isAuthorizationStatusAllowed(updatedStatus)
|
||||
case .denied:
|
||||
return false
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func isAuthorizationStatusAllowed(_ status: UNAuthorizationStatus) -> Bool {
|
||||
@@ -616,9 +635,6 @@ struct OpenClawApp: App {
|
||||
UserDefaults.standard.set(true, forKey: "gateway.hasConnectedOnce")
|
||||
UserDefaults.standard.set(true, forKey: "onboarding.quickSetupDismissed")
|
||||
appModel.enterScreenshotFixtureMode()
|
||||
if Self.screenshotNotificationGuidanceEnabled {
|
||||
appModel._debug_presentNotificationPermissionGuidancePromptForScreenshot()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
OpenClawAppModelRegistry.appModel = appModel
|
||||
@@ -670,14 +686,6 @@ struct OpenClawApp: App {
|
||||
#endif
|
||||
}
|
||||
|
||||
private static var screenshotNotificationGuidanceEnabled: Bool {
|
||||
#if DEBUG
|
||||
ProcessInfo.processInfo.arguments.contains("--openclaw-screenshot-notification-guidance")
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func applyAppearancePreference() {
|
||||
let style = self.appearancePreference.userInterfaceStyle
|
||||
|
||||
@@ -22,20 +22,6 @@ struct PushBuildConfig {
|
||||
let apnsEnvironment: PushAPNsEnvironment
|
||||
|
||||
static let current = PushBuildConfig()
|
||||
static let openClawHostedRelayHost = "ios-push-relay.openclaw.ai"
|
||||
|
||||
var usesOpenClawHostedRelay: Bool {
|
||||
guard self.transport == .relay, self.distribution == .official else { return false }
|
||||
guard let relayBaseURL = self.relayBaseURL,
|
||||
let components = URLComponents(url: relayBaseURL, resolvingAgainstBaseURL: false)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
return components.scheme?.lowercased() == "https"
|
||||
&& components.host?.lowercased() == Self.openClawHostedRelayHost
|
||||
&& components.user == nil
|
||||
&& components.password == nil
|
||||
}
|
||||
|
||||
init(bundle: Bundle = .main) {
|
||||
self.transport = Self.readEnum(
|
||||
|
||||
@@ -24,8 +24,6 @@ struct RootTabs: View {
|
||||
AppAppearancePreference.system.rawValue
|
||||
@State private var selectedTab: AppTab = Self.initialTab
|
||||
@State private var selectedSidebarDestination: SidebarDestination = Self.initialSidebarDestination
|
||||
@State private var selectedSettingsRoute: SettingsRoute? = Self.initialSidebarDestination.settingsRoute
|
||||
@State private var selectedSettingsRouteRequestID: Int = 0
|
||||
@State private var isSidebarVisible: Bool = Self.initialSidebarVisibility ?? false
|
||||
@State private var sidebarVisibilityUserOverridden: Bool = Self.initialSidebarVisibility != nil
|
||||
@State private var isSidebarDrawerLayout: Bool = false
|
||||
@@ -41,7 +39,6 @@ struct RootTabs: View {
|
||||
@State private var didApplyInitialAppearance: Bool = false
|
||||
@State private var didApplyInitialChatSession: Bool = false
|
||||
@State private var handledGatewaySetupRequestID: Int = 0
|
||||
@State private var suppressedExecApprovalPromptIDForNotificationSettings: String?
|
||||
|
||||
private static var initialTab: AppTab {
|
||||
let arguments = ProcessInfo.processInfo.arguments
|
||||
@@ -164,10 +161,8 @@ struct RootTabs: View {
|
||||
.tabItem { Label("Agent", systemImage: "person.2.fill") }
|
||||
.tag(AppTab.agent)
|
||||
|
||||
SettingsProTab(
|
||||
initialRoute: self.selectedSettingsRoute,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
.id(self.settingsTabViewID)
|
||||
SettingsProTab(initialRoute: self.selectedSidebarDestination.settingsRoute)
|
||||
.id(self.selectedSidebarDestination.settingsRoute.map { "\($0)" } ?? "settings")
|
||||
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
||||
.tag(AppTab.settings)
|
||||
}
|
||||
@@ -240,7 +235,7 @@ struct RootTabs: View {
|
||||
|
||||
private var sidebarDetailShell: some View {
|
||||
self.sidebarDetail
|
||||
.id(self.sidebarDetailShellID)
|
||||
.id(self.selectedSidebarDestination.id)
|
||||
}
|
||||
|
||||
private var sidebarColumn: some View {
|
||||
@@ -468,21 +463,11 @@ struct RootTabs: View {
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
gatewayAction: { self.selectSidebarDestination(.gateway) })
|
||||
case .settings:
|
||||
if let selectedSettingsRoute {
|
||||
SettingsProTab(
|
||||
directRoute: selectedSettingsRoute,
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
} else {
|
||||
SettingsProTab(
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
}
|
||||
SettingsProTab(headerLeadingAction: self.sidebarHeaderLeadingAction)
|
||||
case .gateway:
|
||||
SettingsProTab(
|
||||
directRoute: self.selectedSettingsRoute ?? self.selectedSidebarDestination.settingsRoute ?? .gateway,
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
onRouteChange: self.handleSettingsRouteChange)
|
||||
directRoute: self.selectedSidebarDestination.settingsRoute ?? .gateway,
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,21 +492,6 @@ struct RootTabs: View {
|
||||
return UIDevice.current.userInterfaceIdiom
|
||||
}
|
||||
|
||||
private var sidebarDetailShellID: String {
|
||||
let routeID = self.selectedSettingsRoute.map { "\($0)" } ?? "root"
|
||||
return "\(self.selectedSidebarDestination.id):\(routeID):\(self.selectedSettingsRouteRequestID)"
|
||||
}
|
||||
|
||||
private var settingsTabViewID: String {
|
||||
let routeID = self.selectedSettingsRoute.map { "\($0)" } ?? "settings"
|
||||
return "\(routeID):\(self.selectedSettingsRouteRequestID)"
|
||||
}
|
||||
|
||||
private var activeExecApprovalPromptSuppressionID: String? {
|
||||
guard self.selectedTab == .settings, self.selectedSettingsRoute == .notifications else { return nil }
|
||||
return self.suppressedExecApprovalPromptIDForNotificationSettings
|
||||
}
|
||||
|
||||
private var shouldCollapseSidebarAfterSelection: Bool {
|
||||
Self.shouldCollapseSidebarAfterSelection(
|
||||
layoutMode: self.isSidebarDrawerLayout ? .drawer : .split)
|
||||
@@ -735,11 +705,6 @@ struct RootTabs: View {
|
||||
.onChange(of: self.appModel.gatewaySetupRequestID) { _, _ in
|
||||
self.maybeOpenSettingsForGatewaySetup()
|
||||
}
|
||||
.onChange(of: self.appModel.pendingExecApprovalPrompt?.id) { _, newValue in
|
||||
if newValue != self.suppressedExecApprovalPromptIDForNotificationSettings {
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rootPresentation(_ content: some View) -> some View {
|
||||
@@ -777,12 +742,7 @@ struct RootTabs: View {
|
||||
}
|
||||
.gatewayTrustPromptAlert()
|
||||
.deepLinkAgentPromptAlert()
|
||||
.execApprovalPromptDialog(
|
||||
suppressedApprovalID: self.activeExecApprovalPromptSuppressionID)
|
||||
.notificationPermissionGuidanceDialog(openNotifications: { approvalId in
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = approvalId
|
||||
self.selectSettingsRoute(.notifications)
|
||||
})
|
||||
.execApprovalPromptDialog()
|
||||
}
|
||||
|
||||
private var appearancePreference: AppAppearancePreference {
|
||||
@@ -914,15 +874,9 @@ struct RootTabs: View {
|
||||
private func homeCanvasName(for agent: AgentSummary) -> String {
|
||||
self.normalized(agent.name) ?? agent.id
|
||||
}
|
||||
}
|
||||
|
||||
extension RootTabs {
|
||||
private func selectSidebarDestination(_ destination: SidebarDestination) {
|
||||
if destination.settingsRoute != .notifications {
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
self.selectedSidebarDestination = destination
|
||||
self.selectedSettingsRoute = destination.settingsRoute
|
||||
self.selectedTab = destination.appTab
|
||||
guard self.usesSidebarTabs, self.shouldCollapseSidebarAfterSelection else { return }
|
||||
withAnimation(.easeInOut(duration: 0.22)) {
|
||||
@@ -930,31 +884,6 @@ extension RootTabs {
|
||||
}
|
||||
}
|
||||
|
||||
private func selectSettingsRoute(_ route: SettingsRoute) {
|
||||
if route != .notifications {
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
self.selectedSettingsRoute = route
|
||||
self.selectedSettingsRouteRequestID &+= 1
|
||||
self.selectedSidebarDestination = .settings
|
||||
self.selectedTab = .settings
|
||||
guard self.usesSidebarTabs, self.shouldCollapseSidebarAfterSelection else { return }
|
||||
withAnimation(.easeInOut(duration: 0.22)) {
|
||||
self.setSidebarVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleSettingsRouteChange(_ route: SettingsRoute?) {
|
||||
guard route != .notifications else { return }
|
||||
if route == nil {
|
||||
self.selectedSettingsRoute = nil
|
||||
if self.selectedTab == .settings {
|
||||
self.selectedSidebarDestination = .settings
|
||||
}
|
||||
}
|
||||
self.suppressedExecApprovalPromptIDForNotificationSettings = nil
|
||||
}
|
||||
|
||||
private func showSidebar() {
|
||||
self.sidebarVisibilityUserOverridden = true
|
||||
withAnimation(.easeInOut(duration: 0.22)) {
|
||||
|
||||
@@ -16,6 +16,7 @@ enum NotificationAuthorizationStatus {
|
||||
|
||||
protocol NotificationCentering: Sendable {
|
||||
func authorizationStatus() async -> NotificationAuthorizationStatus
|
||||
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool
|
||||
func add(_ request: UNNotificationRequest) async throws
|
||||
func removePendingNotificationRequests(withIdentifiers identifiers: [String]) async
|
||||
func removeDeliveredNotifications(withIdentifiers identifiers: [String]) async
|
||||
@@ -47,6 +48,10 @@ struct LiveNotificationCenter: NotificationCentering, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool {
|
||||
try await self.center.requestAuthorization(options: options)
|
||||
}
|
||||
|
||||
func add(_ request: UNNotificationRequest) async throws {
|
||||
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
||||
self.center.add(request) { error in
|
||||
|
||||
@@ -50,7 +50,6 @@ Sources/Gateway/GatewayDiscoveryModel.swift
|
||||
Sources/Gateway/GatewayHealthMonitor.swift
|
||||
Sources/Gateway/GatewayProblemView.swift
|
||||
Sources/Gateway/GatewayQuickSetupSheet.swift
|
||||
Sources/Gateway/NotificationPermissionGuidanceDialog.swift
|
||||
Sources/Gateway/GatewayServiceResolver.swift
|
||||
Sources/Gateway/GatewaySettingsStore.swift
|
||||
Sources/Gateway/GatewayTrustPromptAlert.swift
|
||||
|
||||
@@ -14,6 +14,10 @@ private final class MockNotificationCenter: NotificationCentering, @unchecked Se
|
||||
self.authorization
|
||||
}
|
||||
|
||||
func requestAuthorization(options _: UNAuthorizationOptions) async throws -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func add(_ request: UNNotificationRequest) async throws {
|
||||
self.addedRequests.append(request)
|
||||
}
|
||||
|
||||
@@ -200,16 +200,25 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
|
||||
|
||||
private final class MockBootstrapNotificationCenter: NotificationCentering, @unchecked Sendable {
|
||||
var status: NotificationAuthorizationStatus = .notDetermined
|
||||
var addCalls = 0
|
||||
var requestAuthorizationResult = false
|
||||
var requestAuthorizationCalls = 0
|
||||
|
||||
func authorizationStatus() async -> NotificationAuthorizationStatus {
|
||||
self.status
|
||||
}
|
||||
|
||||
func add(_: UNNotificationRequest) async throws {
|
||||
self.addCalls += 1
|
||||
func requestAuthorization(options _: UNAuthorizationOptions) async throws -> Bool {
|
||||
self.requestAuthorizationCalls += 1
|
||||
if self.requestAuthorizationResult {
|
||||
self.status = .authorized
|
||||
} else {
|
||||
self.status = .denied
|
||||
}
|
||||
return self.requestAuthorizationResult
|
||||
}
|
||||
|
||||
func add(_: UNNotificationRequest) async throws {}
|
||||
|
||||
func removePendingNotificationRequests(withIdentifiers _: [String]) async {}
|
||||
|
||||
func removeDeliveredNotifications(withIdentifiers _: [String]) async {}
|
||||
@@ -1151,35 +1160,6 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
isBackgrounded: false))
|
||||
}
|
||||
|
||||
@Test func execApprovalEventIDDecodesGatewayPayload() {
|
||||
#expect(NodeAppModel._test_execApprovalEventID(from: AnyCodable(["id": " approval-1 "])) == "approval-1")
|
||||
#expect(NodeAppModel._test_execApprovalEventID(from: AnyCodable(["id": " "])) == nil)
|
||||
#expect(NodeAppModel._test_execApprovalEventID(from: AnyCodable(["other": "approval-1"])) == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor func operatorGatewayResolvedEventClearsPendingApprovalPrompt() async throws {
|
||||
let appModel = NodeAppModel()
|
||||
try appModel._test_presentExecApprovalPrompt(
|
||||
#require(
|
||||
NodeAppModel._test_makeExecApprovalPrompt(
|
||||
id: "approval-event-resolved",
|
||||
commandText: "echo clear",
|
||||
allowedDecisions: ["allow-once", "deny"],
|
||||
host: "gateway",
|
||||
nodeId: nil,
|
||||
agentId: nil,
|
||||
expiresAtMs: Int(Date().timeIntervalSince1970 * 1000) + 60000)))
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.resolvedKind,
|
||||
payload: AnyCodable(["id": "approval-event-resolved"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
|
||||
#expect(appModel._test_pendingExecApprovalPrompt() == nil)
|
||||
}
|
||||
|
||||
@Test func watchExecApprovalHydrateFetchesOnlyMissingIDs() {
|
||||
let idsToFetch = NodeAppModel._test_watchExecApprovalIDsNeedingFetch(
|
||||
candidateIDs: ["cached", "pending", "cached", "other", "", " pending "],
|
||||
@@ -1221,65 +1201,13 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
hasStoredOperatorToken: false))
|
||||
}
|
||||
|
||||
@Test @MainActor func operatorGatewayRequestedEventShowsNotificationGuidanceWhenNotificationsOff() async throws {
|
||||
@Test @MainActor func successfulBootstrapOnboardingRequestsNotificationAuthorization() async {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .notDetermined
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
appModel._test_resetExecApprovalNotificationGuidanceSuppression()
|
||||
defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.requestedKind,
|
||||
payload: AnyCodable(["id": "approval-notifications-off"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
await appModel._test_handleSuccessfulBootstrapGatewayOnboarding()
|
||||
|
||||
let prompt = try #require(appModel._test_pendingNotificationPermissionGuidancePrompt())
|
||||
#expect(prompt.approvalId == "approval-notifications-off")
|
||||
}
|
||||
|
||||
@Test @MainActor func suppressedOperatorGatewayRequestedEventDoesNotShowNotificationGuidance() async {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .denied
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
appModel._test_resetExecApprovalNotificationGuidanceSuppression()
|
||||
defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
|
||||
appModel.dismissNotificationPermissionGuidancePrompt(suppressFuture: true)
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.requestedKind,
|
||||
payload: AnyCodable(["id": "approval-suppressed"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
|
||||
#expect(appModel._test_pendingNotificationPermissionGuidancePrompt() == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor func operatorGatewayResolvedEventClearsNotificationGuidancePrompt() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .denied
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
appModel._test_resetExecApprovalNotificationGuidanceSuppression()
|
||||
defer { appModel._test_resetExecApprovalNotificationGuidanceSuppression() }
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.requestedKind,
|
||||
payload: AnyCodable(["id": "approval-guidance-resolved"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
_ = try #require(appModel._test_pendingNotificationPermissionGuidancePrompt())
|
||||
|
||||
await appModel._test_handleOperatorGatewayServerEvent(EventFrame(
|
||||
type: "event",
|
||||
event: ExecApprovalNotificationBridge.resolvedKind,
|
||||
payload: AnyCodable(["id": "approval-guidance-resolved"]),
|
||||
seq: nil,
|
||||
stateversion: nil))
|
||||
|
||||
#expect(appModel._test_pendingNotificationPermissionGuidancePrompt() == nil)
|
||||
#expect(center.requestAuthorizationCalls == 1)
|
||||
}
|
||||
|
||||
@Test func clearingBootstrapTokenStripsReconnectConfigEvenWithoutPersistence() throws {
|
||||
@@ -1341,78 +1269,6 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
|
||||
#expect(res.error?.message.contains("CAMERA_DISABLED") == true)
|
||||
}
|
||||
|
||||
@Test @MainActor func systemNotifyReturnsUnavailableWhenNotificationsOff() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .notDetermined
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawSystemNotifyParams(title: "Approval", body: "Review request")
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "notify-off",
|
||||
command: OpenClawSystemCommand.notify.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok == false)
|
||||
#expect(res.error?.code == .unavailable)
|
||||
#expect(res.error?.message == "NOT_AUTHORIZED: notifications")
|
||||
#expect(center.addCalls == 0)
|
||||
}
|
||||
|
||||
@Test @MainActor func systemNotifySchedulesWhenNotificationsAreAlreadyAllowed() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .authorized
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawSystemNotifyParams(title: "Approval", body: "Review request")
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "notify-on",
|
||||
command: OpenClawSystemCommand.notify.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok)
|
||||
#expect(center.addCalls == 1)
|
||||
}
|
||||
|
||||
@Test @MainActor func chatPushWithoutSpeechReturnsUnavailableWhenNotificationsOff() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .notDetermined
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawChatPushParams(text: "Build finished", speak: false)
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "chat-push-off",
|
||||
command: OpenClawChatCommand.push.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok == false)
|
||||
#expect(res.error?.code == .unavailable)
|
||||
#expect(res.error?.message == "NOT_AUTHORIZED: notifications")
|
||||
#expect(center.addCalls == 0)
|
||||
}
|
||||
|
||||
@Test @MainActor func chatPushSchedulesWhenNotificationsAreAlreadyAllowed() async throws {
|
||||
let center = MockBootstrapNotificationCenter()
|
||||
center.status = .authorized
|
||||
let appModel = NodeAppModel(notificationCenter: center)
|
||||
let params = OpenClawChatPushParams(text: "Build finished", speak: false)
|
||||
let paramsData = try JSONEncoder().encode(params)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "chat-push-on",
|
||||
command: OpenClawChatCommand.push.rawValue,
|
||||
paramsJSON: String(decoding: paramsData, as: UTF8.self))
|
||||
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
|
||||
#expect(res.ok)
|
||||
#expect(center.addCalls == 1)
|
||||
}
|
||||
|
||||
@Test @MainActor func handleInvokeRejectsInvalidScreenFormat() async {
|
||||
let appModel = NodeAppModel()
|
||||
let params = OpenClawScreenRecordParams(format: "gif")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
struct RootTabsSourceGuardTests {
|
||||
@Test func `hidden sidebar reveal uses destination header without reserved rail`() throws {
|
||||
@Suite struct RootTabsSourceGuardTests {
|
||||
@Test func hiddenSidebarRevealUsesDestinationHeaderWithoutReservedRail() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let componentSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
|
||||
|
||||
@@ -38,7 +38,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!source.contains("shouldShowOverviewHeaderSidebarReveal"))
|
||||
}
|
||||
|
||||
@Test func `i pad split uses sliding sidebar while portrait keeps drawer overlay`() throws {
|
||||
@Test func iPadSplitUsesSlidingSidebarWhilePortraitKeepsDrawerOverlay() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let splitContent = try Self.extract(
|
||||
source,
|
||||
@@ -67,7 +67,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!drawerContent.contains("NavigationSplitView"))
|
||||
}
|
||||
|
||||
@Test func `sidebar keeps navigation model destination only`() throws {
|
||||
@Test func sidebarKeepsNavigationModelDestinationOnly() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let navigationSource = try String(contentsOf: Self.rootTabsNavigationSourceURL(), encoding: .utf8)
|
||||
let sidebarColumn = try Self.extract(
|
||||
@@ -114,7 +114,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(navigationSource.contains("SidebarGroup(title: \"REFERENCE\", destinations: [.docs])"))
|
||||
}
|
||||
|
||||
@Test func `sidebar routes use destination headers instead of repeated product branding`() throws {
|
||||
@Test func sidebarRoutesUseDestinationHeadersInsteadOfRepeatedProductBranding() throws {
|
||||
let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let agentOverviewSource = try String(contentsOf: Self.agentProTabOverviewSourceURL(), encoding: .utf8)
|
||||
let docsSource = try String(contentsOf: Self.docsSourceURL(), encoding: .utf8)
|
||||
@@ -148,7 +148,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!docsSource.contains("Text(\"OpenClaw Docs\")"))
|
||||
}
|
||||
|
||||
@Test func `agents direct route keeps single sidebar control`() throws {
|
||||
@Test func agentsDirectRouteKeepsSingleSidebarControl() throws {
|
||||
let source = try String(contentsOf: Self.agentProTabSourceURL(), encoding: .utf8)
|
||||
let destinationsSource = try String(contentsOf: Self.agentProTabDestinationsSourceURL(), encoding: .utf8)
|
||||
let nodesSource = try String(contentsOf: Self.agentProNodesDestinationSourceURL(), encoding: .utf8)
|
||||
@@ -165,7 +165,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(dreamingSource.contains("OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)"))
|
||||
}
|
||||
|
||||
@Test func `routed headers use shared adaptive layout`() throws {
|
||||
@Test func routedHeadersUseSharedAdaptiveLayout() throws {
|
||||
let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
|
||||
let featureChromeSource = try String(contentsOf: Self.iPadSidebarScreenChromeSourceURL(), encoding: .utf8)
|
||||
let docsSource = try String(contentsOf: Self.docsSourceURL(), encoding: .utf8)
|
||||
@@ -187,7 +187,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(settingsSource.contains("OpenClawAdaptiveHeaderRow("))
|
||||
}
|
||||
|
||||
@Test func `phone hub keeps docs as destination only`() throws {
|
||||
@Test func phoneHubKeepsDocsAsDestinationOnly() throws {
|
||||
let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("case .docs:"))
|
||||
@@ -198,7 +198,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!source.contains("https://docs.openclaw.ai"))
|
||||
}
|
||||
|
||||
@Test func `root shell preview matrix covers phone and I pad states`() throws {
|
||||
@Test func rootShellPreviewMatrixCoversPhoneAndIPadStates() throws {
|
||||
let source = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\n \"Shell iPhone portrait\""))
|
||||
@@ -211,7 +211,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(source.contains("#Preview(\n \"Shell iPad gateway error\""))
|
||||
}
|
||||
|
||||
@Test func `shared chat preview matrix covers connection states`() throws {
|
||||
@Test func sharedChatPreviewMatrixCoversConnectionStates() throws {
|
||||
let source = try String(contentsOf: Self.sharedChatPreviewSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Chat connected\")"))
|
||||
@@ -226,7 +226,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(source.contains("Gateway not connected. Check Tailscale and retry."))
|
||||
}
|
||||
|
||||
@Test func `phone hub keeps content above floating tab bar`() throws {
|
||||
@Test func phoneHubKeepsContentAboveFloatingTabBar() throws {
|
||||
let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains(".safeAreaPadding(.bottom, self.bottomScrollInset)"))
|
||||
@@ -235,7 +235,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!source.contains("bottomTabBarClearance"))
|
||||
}
|
||||
|
||||
@Test func `phone hub header stays task first`() throws {
|
||||
@Test func phoneHubHeaderStaysTaskFirst() throws {
|
||||
let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("private var gatewayActionRow: some View"))
|
||||
@@ -253,7 +253,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!source.contains("private func metric(label:"))
|
||||
}
|
||||
|
||||
@Test func `workboard uses real gateway methods`() throws {
|
||||
@Test func workboardUsesRealGatewayMethods() throws {
|
||||
let source = try String(contentsOf: Self.iPadWorkboardScreenSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("workboard.cards.list"))
|
||||
@@ -268,7 +268,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(!source.contains("Multi-column queue control"))
|
||||
}
|
||||
|
||||
@Test func `workboard create action surfaces unavailable reasons`() throws {
|
||||
@Test func workboardCreateActionSurfacesUnavailableReasons() throws {
|
||||
let source = try String(contentsOf: Self.iPadWorkboardScreenSourceURL(), encoding: .utf8)
|
||||
let createFunction = try Self.extract(
|
||||
source,
|
||||
@@ -295,7 +295,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(createFunction.contains("return true"))
|
||||
}
|
||||
|
||||
@Test func `task scope controls send real gateway params`() throws {
|
||||
@Test func taskScopeControlsSendRealGatewayParams() throws {
|
||||
let source = try Self.iPadTaskFeatureScreensSource()
|
||||
|
||||
#expect(source.contains("private var boardScopeMenu: some View"))
|
||||
@@ -314,7 +314,7 @@ struct RootTabsSourceGuardTests {
|
||||
"params: EmptyParams(),\n timeoutSeconds: 20)\n let response = try JSONDecoder().decode(IPadSkillProposalManifest.self"))
|
||||
}
|
||||
|
||||
@Test func `compact task rows keep phone native actions`() throws {
|
||||
@Test func compactTaskRowsKeepPhoneNativeActions() throws {
|
||||
let source = try Self.iPadTaskFeatureScreensSource()
|
||||
let compactControls = try Self.extract(
|
||||
source,
|
||||
@@ -348,7 +348,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(compactControls.contains("Label(\"Dispatch\""))
|
||||
}
|
||||
|
||||
@Test func `skill workshop uses kanban lanes on wide I pad`() throws {
|
||||
@Test func skillWorkshopUsesKanbanLanesOnWideIPad() throws {
|
||||
let source = try String(contentsOf: Self.iPadSkillWorkshopScreenSourceURL(), encoding: .utf8)
|
||||
let previewSource = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
let content = try Self.extract(
|
||||
@@ -376,7 +376,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(previewSource.contains("status: \"manual_QA\""))
|
||||
}
|
||||
|
||||
@Test func `compact task rows have populated phone previews`() throws {
|
||||
@Test func compactTaskRowsHavePopulatedPhonePreviews() throws {
|
||||
let source = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Workboard phone queue rows\")"))
|
||||
@@ -387,7 +387,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(source.contains("IPadSkillWorkshopPreviewFixtures.proposals"))
|
||||
}
|
||||
|
||||
@Test func `task screen preview matrices cover primary states`() throws {
|
||||
@Test func taskScreenPreviewMatricesCoverPrimaryStates() throws {
|
||||
let source = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Workboard states\")"))
|
||||
@@ -412,7 +412,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(source.contains("\"manual_QA\""))
|
||||
}
|
||||
|
||||
@Test func `activity preview matrix covers connection states`() throws {
|
||||
@Test func activityPreviewMatrixCoversConnectionStates() throws {
|
||||
let source = try String(contentsOf: Self.iPadSidebarFeaturePreviewsSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(source.contains("#Preview(\"Activity states\")"))
|
||||
@@ -426,7 +426,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(source.contains("title: \"Loading sessions\""))
|
||||
}
|
||||
|
||||
@Test func `routed feature screens reuse shared pro components`() throws {
|
||||
@Test func routedFeatureScreensReuseSharedProComponents() throws {
|
||||
let source = try Self.iPadTaskFeatureScreensSource()
|
||||
let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8)
|
||||
let channelsSource = try String(contentsOf: Self.channelsSourceURL(), encoding: .utf8)
|
||||
@@ -445,22 +445,20 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(componentsSource.contains("struct ProStatusRow"))
|
||||
}
|
||||
|
||||
@Test func `activity screen stays split from task feature screens`() throws {
|
||||
@Test func activityScreenStaysSplitFromTaskFeatureScreens() throws {
|
||||
let taskSource = try Self.iPadTaskFeatureScreensSource()
|
||||
let activitySource = try String(contentsOf: Self.iPadActivityScreenSourceURL(), encoding: .utf8)
|
||||
let appModelSource = try String(contentsOf: Self.nodeAppModelSourceURL(), encoding: .utf8)
|
||||
let projectSource = try String(contentsOf: Self.xcodeProjectSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(activitySource.contains("struct IPadActivityScreen: View"))
|
||||
#expect(activitySource.contains("self.appModel.makeChatTransport()"))
|
||||
#expect(appModelSource.contains("return IOSGatewayChatTransport(gateway: self.operatorSession)"))
|
||||
#expect(activitySource.contains("IOSGatewayChatTransport(gateway: self.appModel.operatorSession)"))
|
||||
#expect(activitySource.contains("IPadSidebarScreenChrome("))
|
||||
#expect(!taskSource.contains("struct IPadActivityScreen"))
|
||||
#expect(!taskSource.contains("import OpenClawChatUI"))
|
||||
#expect(projectSource.contains("IPadActivityScreen.swift in Sources"))
|
||||
}
|
||||
|
||||
@Test func `routed feature chrome stays split from task feature screens`() throws {
|
||||
@Test func routedFeatureChromeStaysSplitFromTaskFeatureScreens() throws {
|
||||
let taskSource = try Self.iPadTaskFeatureScreensSource()
|
||||
let chromeSource = try String(contentsOf: Self.iPadSidebarScreenChromeSourceURL(), encoding: .utf8)
|
||||
let projectSource = try String(contentsOf: Self.xcodeProjectSourceURL(), encoding: .utf8)
|
||||
@@ -472,7 +470,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(projectSource.contains("IPadSidebarScreenChrome.swift in Sources"))
|
||||
}
|
||||
|
||||
@Test func `routed feature chrome keeps gateway pill actionable`() throws {
|
||||
@Test func routedFeatureChromeKeepsGatewayPillActionable() throws {
|
||||
let chromeSource = try String(contentsOf: Self.iPadSidebarScreenChromeSourceURL(), encoding: .utf8)
|
||||
let featureSource = try Self.iPadTaskFeatureScreensSource()
|
||||
let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
@@ -488,18 +486,14 @@ struct RootTabsSourceGuardTests {
|
||||
.count == 1)
|
||||
}
|
||||
|
||||
@Test func `routed gateway pills open gateway settings`() throws {
|
||||
@Test func routedGatewayPillsOpenGatewaySettings() throws {
|
||||
let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
|
||||
let agentSource = try String(contentsOf: Self.agentProTabSourceURL(), encoding: .utf8)
|
||||
let agentOverviewSource = try String(contentsOf: Self.agentProTabOverviewSourceURL(), encoding: .utf8)
|
||||
let overviewSource = try String(contentsOf: Self.commandCenterSourceURL(), encoding: .utf8)
|
||||
let chatSource = try String(contentsOf: Self.chatProTabSourceURL(), encoding: .utf8)
|
||||
let docsSource = try String(contentsOf: Self.docsSourceURL(), encoding: .utf8)
|
||||
let settingsTabSource = try String(contentsOf: Self.settingsProTabSourceURL(), encoding: .utf8)
|
||||
let settingsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
|
||||
let notificationGuidanceSource = try String(
|
||||
contentsOf: Self.notificationPermissionGuidanceDialogSourceURL(),
|
||||
encoding: .utf8)
|
||||
|
||||
#expect(rootSource.matches(of: /openSettings: \{ self\.selectSidebarDestination\(\.gateway\) \}/).count >= 2)
|
||||
#expect(rootSource.matches(of: /gatewayAction: \{ self\.selectSidebarDestination\(\.gateway\) \}/).count == 1)
|
||||
@@ -518,39 +512,15 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(docsSource.contains("let gatewayAction: (() -> Void)?"))
|
||||
#expect(settingsSource.contains("NavigationLink(value: SettingsRoute.gateway)"))
|
||||
#expect(rootSource.contains("case .settings:"))
|
||||
#expect(rootSource
|
||||
.matches(of: /SettingsProTab\(\s*headerLeadingAction: self\.sidebarHeaderLeadingAction,/)
|
||||
.count >= 1)
|
||||
#expect(rootSource
|
||||
.contains(
|
||||
"directRoute: self.selectedSettingsRoute ?? self.selectedSidebarDestination.settingsRoute ?? .gateway"))
|
||||
#expect(rootSource.matches(of: /SettingsProTab\(\s*initialRoute: self\.selectedSettingsRoute,/).count == 1)
|
||||
#expect(rootSource.contains(".id(self.settingsTabViewID)"))
|
||||
#expect(rootSource.contains("@State private var selectedSettingsRouteRequestID: Int = 0"))
|
||||
#expect(rootSource.contains("self.selectedSettingsRouteRequestID &+= 1"))
|
||||
#expect(rootSource.contains("@State private var suppressedExecApprovalPromptIDForNotificationSettings"))
|
||||
#expect(rootSource.contains("private var activeExecApprovalPromptSuppressionID: String?"))
|
||||
#expect(rootSource.contains("suppressedApprovalID: self.activeExecApprovalPromptSuppressionID"))
|
||||
#expect(rootSource.contains("if destination.settingsRoute != .notifications"))
|
||||
#expect(rootSource.contains("if route != .notifications"))
|
||||
#expect(rootSource.contains("if route == nil"))
|
||||
#expect(rootSource.contains("self.selectedSettingsRoute = nil"))
|
||||
#expect(rootSource.contains("self.selectedSidebarDestination = .settings"))
|
||||
#expect(rootSource.contains("self.suppressedExecApprovalPromptIDForNotificationSettings = approvalId"))
|
||||
#expect(rootSource.contains("onRouteChange: self.handleSettingsRouteChange"))
|
||||
#expect(rootSource.contains("private func handleSettingsRouteChange(_ route: SettingsRoute?)"))
|
||||
#expect(settingsTabSource.contains("let onRouteChange: ((SettingsRoute?) -> Void)?"))
|
||||
#expect(settingsTabSource.contains("self.onRouteChange?(self.navigationPath.last)"))
|
||||
#expect(notificationGuidanceSource.contains("onSuppressFuture"))
|
||||
#expect(notificationGuidanceSource.contains("suppressFuture: true"))
|
||||
#expect(notificationGuidanceSource.contains("Text(\"Don't show again\")"))
|
||||
#expect(rootSource.contains("private func selectSettingsRoute(_ route: SettingsRoute)"))
|
||||
#expect(rootSource.contains("SettingsProTab(headerLeadingAction: self.sidebarHeaderLeadingAction)"))
|
||||
#expect(rootSource.contains("directRoute: self.selectedSidebarDestination.settingsRoute ?? .gateway"))
|
||||
#expect(rootSource.contains("SettingsProTab(initialRoute: self.selectedSidebarDestination.settingsRoute)"))
|
||||
#expect(settingsSource.contains("title: \"Channels / Integrations\""))
|
||||
#expect(settingsSource.contains("route: .channels"))
|
||||
#expect(docsSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")"))
|
||||
}
|
||||
|
||||
@Test func `gateway settings keeps pairing trust diagnostics and tailscale actions`() throws {
|
||||
@Test func gatewaySettingsKeepsPairingTrustDiagnosticsAndTailscaleActions() throws {
|
||||
let settingsSource = try String(contentsOf: Self.settingsProTabSourceURL(), encoding: .utf8)
|
||||
let sectionsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
|
||||
let actionsSource = try String(contentsOf: Self.settingsProTabActionsSourceURL(), encoding: .utf8)
|
||||
@@ -596,7 +566,7 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(controllerSource.contains("trustRotatedGatewayCertificate(from problem: GatewayConnectionProblem)"))
|
||||
}
|
||||
|
||||
@Test func `gateway settings preview matrix covers primary states`() throws {
|
||||
@Test func gatewaySettingsPreviewMatrixCoversPrimaryStates() throws {
|
||||
let supportSource = try String(contentsOf: Self.settingsProTabSupportSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(supportSource.contains("#Preview(\"Gateway settings states\")"))
|
||||
@@ -615,15 +585,12 @@ struct RootTabsSourceGuardTests {
|
||||
#expect(supportSource.contains("self.previewButton(\"Diagnose\""))
|
||||
}
|
||||
|
||||
@Test func `native chat uses gateway transport`() throws {
|
||||
@Test func nativeChatUsesGatewayTransport() throws {
|
||||
let chatSource = try String(contentsOf: Self.chatProTabSourceURL(), encoding: .utf8)
|
||||
let channelsSource = try String(contentsOf: Self.channelsSourceURL(), encoding: .utf8)
|
||||
let settingsSectionsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
|
||||
let appModelSource = try String(contentsOf: Self.nodeAppModelSourceURL(), encoding: .utf8)
|
||||
|
||||
#expect(chatSource.matches(of: /self\.appModel\.makeChatTransport\(\)/).count == 2)
|
||||
#expect(appModelSource.contains("return IOSGatewayChatTransport(gateway: self.operatorSession)"))
|
||||
#expect(settingsSectionsSource.contains("Message routing and external channel clients."))
|
||||
#expect(chatSource.contains("IOSGatewayChatTransport(gateway: self.appModel.operatorSession)"))
|
||||
#expect(channelsSource.contains("Message routing and external channel clients."))
|
||||
#expect(channelsSource.contains("\"clickclack\": SettingsChannelFallbackMetadata"))
|
||||
#expect(channelsSource.contains("label: \"ClickClack\""))
|
||||
#expect(channelsSource.contains("Self-hosted chat bot routing."))
|
||||
@@ -636,13 +603,6 @@ struct RootTabsSourceGuardTests {
|
||||
.appendingPathComponent("Sources/RootTabs.swift")
|
||||
}
|
||||
|
||||
private static func nodeAppModelSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("Sources/Model/NodeAppModel.swift")
|
||||
}
|
||||
|
||||
private static func phoneHubSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
@@ -786,13 +746,6 @@ struct RootTabsSourceGuardTests {
|
||||
.appendingPathComponent("Sources/Design/SettingsProTab.swift")
|
||||
}
|
||||
|
||||
private static func notificationPermissionGuidanceDialogSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("Sources/Gateway/NotificationPermissionGuidanceDialog.swift")
|
||||
}
|
||||
|
||||
private static func settingsProTabActionsSourceURL() -> URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
Maintenance update for the current OpenClaw release.
|
||||
OpenClaw is now available on iPhone.
|
||||
|
||||
- Added Apple Watch controls for common agent actions.
|
||||
- Improved Gateway setup, notification settings, and share-extension identity handling.
|
||||
- Updated the Watch app integration for current Xcode compatibility.
|
||||
Connect to your OpenClaw Gateway to chat with your assistant, use realtime Talk mode, review approvals, share content from iOS, and bring device capabilities like camera, location, screen, and notifications into your private automation workflows.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.6.9"
|
||||
"version": "2026.6.2"
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.6.9</string>
|
||||
<string>2026.6.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026060900</string>
|
||||
<string>2026060200</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -69,17 +69,6 @@
|
||||
"fileName"
|
||||
]
|
||||
},
|
||||
"api": {
|
||||
"emoji": "🌐",
|
||||
"title": "API",
|
||||
"detailKeys": [
|
||||
"url",
|
||||
"endpoint",
|
||||
"path",
|
||||
"method",
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"browser": {
|
||||
"emoji": "🌐",
|
||||
"title": "Browser",
|
||||
|
||||
@@ -6592,7 +6592,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
public let turnsourceto: AnyCodable?
|
||||
public let turnsourceaccountid: AnyCodable?
|
||||
public let turnsourcethreadid: AnyCodable?
|
||||
public let approvalreviewerdeviceids: [String]?
|
||||
public let requiredeliveryroute: Bool?
|
||||
public let suppressdelivery: Bool?
|
||||
public let timeoutms: Int?
|
||||
@@ -6619,7 +6618,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
turnsourceto: AnyCodable?,
|
||||
turnsourceaccountid: AnyCodable?,
|
||||
turnsourcethreadid: AnyCodable?,
|
||||
approvalreviewerdeviceids: [String]?,
|
||||
requiredeliveryroute: Bool? = nil,
|
||||
suppressdelivery: Bool? = nil,
|
||||
timeoutms: Int?,
|
||||
@@ -6645,7 +6643,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
self.turnsourceto = turnsourceto
|
||||
self.turnsourceaccountid = turnsourceaccountid
|
||||
self.turnsourcethreadid = turnsourcethreadid
|
||||
self.approvalreviewerdeviceids = approvalreviewerdeviceids
|
||||
self.requiredeliveryroute = requiredeliveryroute
|
||||
self.suppressdelivery = suppressdelivery
|
||||
self.timeoutms = timeoutms
|
||||
@@ -6673,7 +6670,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
case turnsourceto = "turnSourceTo"
|
||||
case turnsourceaccountid = "turnSourceAccountId"
|
||||
case turnsourcethreadid = "turnSourceThreadId"
|
||||
case approvalreviewerdeviceids = "approvalReviewerDeviceIds"
|
||||
case requiredeliveryroute = "requireDeliveryRoute"
|
||||
case suppressdelivery = "suppressDelivery"
|
||||
case timeoutms = "timeoutMs"
|
||||
@@ -7277,9 +7273,7 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
public let sessionid: String?
|
||||
public let message: String
|
||||
public let thinking: String?
|
||||
public let fastmodevalue: AnyCodable?
|
||||
public var fastmode: Bool? { fastmodevalue?.value as? Bool }
|
||||
public let fastautoonseconds: Int?
|
||||
public let fastmode: Bool?
|
||||
public let deliver: Bool?
|
||||
public let originatingchannel: String?
|
||||
public let originatingto: String?
|
||||
@@ -7292,46 +7286,6 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
public let suppresscommandinterpretation: Bool?
|
||||
public let idempotencykey: String
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
sessionid: String?,
|
||||
message: String,
|
||||
thinking: String?,
|
||||
fastmodevalue: AnyCodable?,
|
||||
fastautoonseconds: Int?,
|
||||
deliver: Bool?,
|
||||
originatingchannel: String?,
|
||||
originatingto: String?,
|
||||
originatingaccountid: String?,
|
||||
originatingthreadid: String?,
|
||||
attachments: [AnyCodable]?,
|
||||
timeoutms: Int?,
|
||||
systeminputprovenance: [String: AnyCodable]?,
|
||||
systemprovenancereceipt: String?,
|
||||
suppresscommandinterpretation: Bool?,
|
||||
idempotencykey: String)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.sessionid = sessionid
|
||||
self.message = message
|
||||
self.thinking = thinking
|
||||
self.fastmodevalue = fastmodevalue
|
||||
self.fastautoonseconds = fastautoonseconds
|
||||
self.deliver = deliver
|
||||
self.originatingchannel = originatingchannel
|
||||
self.originatingto = originatingto
|
||||
self.originatingaccountid = originatingaccountid
|
||||
self.originatingthreadid = originatingthreadid
|
||||
self.attachments = attachments
|
||||
self.timeoutms = timeoutms
|
||||
self.systeminputprovenance = systeminputprovenance
|
||||
self.systemprovenancereceipt = systemprovenancereceipt
|
||||
self.suppresscommandinterpretation = suppresscommandinterpretation
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
@@ -7351,25 +7305,23 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
suppresscommandinterpretation: Bool?,
|
||||
idempotencykey: String)
|
||||
{
|
||||
self.init(
|
||||
sessionkey: sessionkey,
|
||||
agentid: agentid,
|
||||
sessionid: sessionid,
|
||||
message: message,
|
||||
thinking: thinking,
|
||||
fastmodevalue: fastmode.map { AnyCodable($0) },
|
||||
fastautoonseconds: nil,
|
||||
deliver: deliver,
|
||||
originatingchannel: originatingchannel,
|
||||
originatingto: originatingto,
|
||||
originatingaccountid: originatingaccountid,
|
||||
originatingthreadid: originatingthreadid,
|
||||
attachments: attachments,
|
||||
timeoutms: timeoutms,
|
||||
systeminputprovenance: systeminputprovenance,
|
||||
systemprovenancereceipt: systemprovenancereceipt,
|
||||
suppresscommandinterpretation: suppresscommandinterpretation,
|
||||
idempotencykey: idempotencykey)
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.sessionid = sessionid
|
||||
self.message = message
|
||||
self.thinking = thinking
|
||||
self.fastmode = fastmode
|
||||
self.deliver = deliver
|
||||
self.originatingchannel = originatingchannel
|
||||
self.originatingto = originatingto
|
||||
self.originatingaccountid = originatingaccountid
|
||||
self.originatingthreadid = originatingthreadid
|
||||
self.attachments = attachments
|
||||
self.timeoutms = timeoutms
|
||||
self.systeminputprovenance = systeminputprovenance
|
||||
self.systemprovenancereceipt = systemprovenancereceipt
|
||||
self.suppresscommandinterpretation = suppresscommandinterpretation
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
@@ -7378,8 +7330,7 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
case sessionid = "sessionId"
|
||||
case message
|
||||
case thinking
|
||||
case fastmodevalue = "fastMode"
|
||||
case fastautoonseconds = "fastAutoOnSeconds"
|
||||
case fastmode = "fastMode"
|
||||
case deliver
|
||||
case originatingchannel = "originatingChannel"
|
||||
case originatingto = "originatingTo"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
3ac3be8b7e201eb577854806a9806ba90acbfb2616e14b3ffd1169f188620303 config-baseline.json
|
||||
2923c1120c0369aeca6646cd67f7264590c6a1f4e5bc3157a04d7661324c6868 config-baseline.core.json
|
||||
769899651e2769833ae7e9c8fbf402e55f3d5e32da6bfe21a9659cc35d1f07bb config-baseline.channel.json
|
||||
d2e2114f1cd43dc894fe1a4836677b42a2a5af825537d6c4a932da832d58a590 config-baseline.plugin.json
|
||||
ac06b6c20a93a8543ec1bd3748ef4f7bdae5006839dd93b3fff874d0da4244aa config-baseline.json
|
||||
e7965566fdaedef445bcd562141f4f3ea1a499cf8ea5956418af7c98049bf242 config-baseline.core.json
|
||||
2d735389858305509528e74329b6f8c65d311e1471c3b4e91dc17aaab8e63a80 config-baseline.channel.json
|
||||
0039da0cf2ba2845b37db52c4cf3a0f25e367cf3d2d507c5d6f8a5e5bdfdc4d4 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
172fe4e143964c0a20525428ff3e6c7631856a7d51c6ad48959a35c72363a410 plugin-sdk-api-baseline.json
|
||||
a4c18ea9f0b0d2c22183bf8c082e757b7f9852b4c518c8b8cb62a21a9dd766e9 plugin-sdk-api-baseline.jsonl
|
||||
6f442c09ff2fa618f6f68cc866091a713d2c730090380dd726a9845f4d0fd9bd plugin-sdk-api-baseline.json
|
||||
d6b1929a42117759a3d0908fb68866e721ee7f0840279dce905a975b461c5b67 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -183,7 +183,7 @@ Model-selection precedence for isolated jobs is:
|
||||
3. User-selected stored cron session model override
|
||||
4. Agent/default model selection
|
||||
|
||||
Fast mode follows the resolved live selection too. If the selected model config has `params.fastMode`, isolated cron uses that by default. A stored session `fastMode` override still wins over config in either direction. Auto mode uses the selected model's `params.fastAutoOnSeconds` cutoff when present, defaulting to 60 seconds.
|
||||
Fast mode follows the resolved live selection too. If the selected model config has `params.fastMode`, isolated cron uses that by default. A stored session `fastMode` override still wins over config in either direction.
|
||||
|
||||
If an isolated run hits a live model-switch handoff, cron retries with the switched provider/model and persists that live selection for the active run before retrying. When the switch also carries a new auth profile, cron persists that auth profile override for the active run too. Retries are bounded: after the initial attempt plus 2 switch retries, cron aborts instead of looping forever.
|
||||
|
||||
|
||||
@@ -94,28 +94,28 @@ Use this checklist when you already know your old BlueBubbles config and want th
|
||||
|
||||
iMessage and BlueBubbles share a lot of channel-level config. The keys that change are mostly transport (REST server vs local CLI). Behavior keys (`dmPolicy`, `groupPolicy`, `allowFrom`, etc.) keep the same meaning.
|
||||
|
||||
| BlueBubbles | bundled iMessage | Notes |
|
||||
| ---------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `channels.bluebubbles.enabled` | `channels.imessage.enabled` | Same semantics. |
|
||||
| `channels.bluebubbles.serverUrl` | _(removed)_ | No REST server — the plugin spawns `imsg rpc` over stdio. |
|
||||
| `channels.bluebubbles.password` | _(removed)_ | No webhook authentication needed. |
|
||||
| _(implicit)_ | `channels.imessage.cliPath` | Path to `imsg` (default `imsg`); use a wrapper script for SSH. |
|
||||
| _(implicit)_ | `channels.imessage.dbPath` | Optional Messages.app `chat.db` override; auto-detected when omitted. |
|
||||
| _(implicit)_ | `channels.imessage.remoteHost` | `host` or `user@host` — only needed when `cliPath` is an SSH wrapper and you want SCP attachment fetches. |
|
||||
| `channels.bluebubbles.dmPolicy` | `channels.imessage.dmPolicy` | Same values (`pairing` / `allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.allowFrom` | `channels.imessage.allowFrom` | Pairing approvals carry over by handle, not by token. |
|
||||
| `channels.bluebubbles.groupPolicy` | `channels.imessage.groupPolicy` | Same values (`allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.groupAllowFrom` | `channels.imessage.groupAllowFrom` | Same. |
|
||||
| `channels.bluebubbles.groups` | `channels.imessage.groups` | **Copy this verbatim, including any `groups: { "*": { ... } }` wildcard entry.** Per-group `requireMention`, `tools`, `toolsBySender` carry over. With `groupPolicy: "allowlist"`, an empty or missing `groups` block silently drops every group message — see "Group registry footgun" below. |
|
||||
| `channels.bluebubbles.sendReadReceipts` | `channels.imessage.sendReadReceipts` | Default `true`. With the bundled plugin this only fires when the private API probe is up. |
|
||||
| `channels.bluebubbles.includeAttachments` | `channels.imessage.includeAttachments` | Same shape, **same off-by-default**. If you had attachments flowing on BlueBubbles you must re-set this explicitly on the iMessage block — it does not carry over implicitly, and inbound photos/media will be silently dropped with no `Inbound message` log line until you do. |
|
||||
| `channels.bluebubbles.attachmentRoots` | `channels.imessage.attachmentRoots` | Local roots; same wildcard rules. |
|
||||
| _(N/A)_ | `channels.imessage.remoteAttachmentRoots` | Only used when `remoteHost` is set for SCP fetches. |
|
||||
| `channels.bluebubbles.mediaMaxMb` | `channels.imessage.mediaMaxMb` | Default 16 MB on iMessage (BlueBubbles default was 8 MB). Set explicitly if you want to keep the lower cap. |
|
||||
| `channels.bluebubbles.textChunkLimit` | `channels.imessage.textChunkLimit` | Default 4000 on both. |
|
||||
| `channels.bluebubbles.coalesceSameSenderDms` | `channels.imessage.coalesceSameSenderDms` | Same opt-in. DM-only — group chats keep instant per-message dispatch on both channels. Widens the default inbound debounce to 7000 ms when enabled without an explicit `messages.inbound.byChannel.imessage` or global `messages.inbound.debounceMs`. See [iMessage docs § Coalescing split-send DMs](/channels/imessage#coalescing-split-send-dms-command--url-in-one-composition). |
|
||||
| `channels.bluebubbles.enrichGroupParticipantsFromContacts` | _(N/A)_ | iMessage already reads sender display names from `chat.db`. |
|
||||
| `channels.bluebubbles.actions.*` | `channels.imessage.actions.*` | Per-action toggles: `reactions`, `edit`, `unsend`, `reply`, `sendWithEffect`, `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, `sendAttachment`. |
|
||||
| BlueBubbles | bundled iMessage | Notes |
|
||||
| ---------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `channels.bluebubbles.enabled` | `channels.imessage.enabled` | Same semantics. |
|
||||
| `channels.bluebubbles.serverUrl` | _(removed)_ | No REST server — the plugin spawns `imsg rpc` over stdio. |
|
||||
| `channels.bluebubbles.password` | _(removed)_ | No webhook authentication needed. |
|
||||
| _(implicit)_ | `channels.imessage.cliPath` | Path to `imsg` (default `imsg`); use a wrapper script for SSH. |
|
||||
| _(implicit)_ | `channels.imessage.dbPath` | Optional Messages.app `chat.db` override; auto-detected when omitted. |
|
||||
| _(implicit)_ | `channels.imessage.remoteHost` | `host` or `user@host` — only needed when `cliPath` is an SSH wrapper and you want SCP attachment fetches. |
|
||||
| `channels.bluebubbles.dmPolicy` | `channels.imessage.dmPolicy` | Same values (`pairing` / `allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.allowFrom` | `channels.imessage.allowFrom` | Pairing approvals carry over by handle, not by token. |
|
||||
| `channels.bluebubbles.groupPolicy` | `channels.imessage.groupPolicy` | Same values (`allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.groupAllowFrom` | `channels.imessage.groupAllowFrom` | Same. |
|
||||
| `channels.bluebubbles.groups` | `channels.imessage.groups` | **Copy this verbatim, including any `groups: { "*": { ... } }` wildcard entry.** Per-group `requireMention`, `tools`, `toolsBySender` carry over. With `groupPolicy: "allowlist"`, an empty or missing `groups` block silently drops every group message — see "Group registry footgun" below. |
|
||||
| `channels.bluebubbles.sendReadReceipts` | `channels.imessage.sendReadReceipts` | Default `true`. With the bundled plugin this only fires when the private API probe is up. |
|
||||
| `channels.bluebubbles.includeAttachments` | `channels.imessage.includeAttachments` | Same shape, **same off-by-default**. If you had attachments flowing on BlueBubbles you must re-set this explicitly on the iMessage block — it does not carry over implicitly, and inbound photos/media will be silently dropped with no `Inbound message` log line until you do. |
|
||||
| `channels.bluebubbles.attachmentRoots` | `channels.imessage.attachmentRoots` | Local roots; same wildcard rules. |
|
||||
| _(N/A)_ | `channels.imessage.remoteAttachmentRoots` | Only used when `remoteHost` is set for SCP fetches. |
|
||||
| `channels.bluebubbles.mediaMaxMb` | `channels.imessage.mediaMaxMb` | Default 16 MB on iMessage (BlueBubbles default was 8 MB). Set explicitly if you want to keep the lower cap. |
|
||||
| `channels.bluebubbles.textChunkLimit` | `channels.imessage.textChunkLimit` | Default 4000 on both. |
|
||||
| `channels.bluebubbles.coalesceSameSenderDms` | `channels.imessage.coalesceSameSenderDms` | Same opt-in. DM-only — group chats keep instant per-message dispatch on both channels. Widens the default inbound debounce to 2500 ms when enabled without an explicit `messages.inbound.byChannel.imessage`. See [iMessage docs § Coalescing split-send DMs](/channels/imessage#coalescing-split-send-dms-command--url-in-one-composition). |
|
||||
| `channels.bluebubbles.enrichGroupParticipantsFromContacts` | _(N/A)_ | iMessage already reads sender display names from `chat.db`. |
|
||||
| `channels.bluebubbles.actions.*` | `channels.imessage.actions.*` | Per-action toggles: `reactions`, `edit`, `unsend`, `reply`, `sendWithEffect`, `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, `sendAttachment`. |
|
||||
|
||||
Multi-account configs (`channels.bluebubbles.accounts.*`) translate one-to-one to `channels.imessage.accounts.*`.
|
||||
|
||||
|
||||
@@ -579,7 +579,7 @@ When `imsg launch` is running and `openclaw channels status --probe` reports `pr
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Read receipts and typing">
|
||||
When the private API bridge is up, accepted inbound chats are marked read and direct chats show a typing bubble as soon as the turn is accepted, while the agent prepares context and generates. Disable read-marking with:
|
||||
When the private API bridge is up, accepted inbound chats are marked read before dispatch and a typing bubble is shown to the sender while the agent generates. Disable read-marking with:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -681,7 +681,7 @@ The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalesc
|
||||
}
|
||||
```
|
||||
|
||||
With the flag on and no explicit `messages.inbound.byChannel.imessage` or global `messages.inbound.debounceMs`, the debounce window widens to **7000 ms** (the legacy default is 0 ms — no debouncing). The wider window is required because Apple's URL-preview split-send cadence can stretch to several seconds while Messages.app emits the preview row.
|
||||
With the flag on and no explicit `messages.inbound.byChannel.imessage`, the debounce window widens to **2500 ms** (the legacy default is 0 ms — no debouncing). The wider window is required because Apple's split-send cadence of 0.8-2.0 s does not fit in a tighter default.
|
||||
|
||||
To tune the window yourself:
|
||||
|
||||
@@ -690,8 +690,10 @@ The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalesc
|
||||
messages: {
|
||||
inbound: {
|
||||
byChannel: {
|
||||
// 7000 ms covers observed Messages.app URL-preview delays.
|
||||
imessage: 7000,
|
||||
// 2500 ms works for most setups; raise to 4000 ms if your Mac is
|
||||
// slow or under memory pressure (observed gap can stretch past 2 s
|
||||
// then).
|
||||
imessage: 2500,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -713,15 +715,15 @@ The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalesc
|
||||
|
||||
The "Flag on" column shows behavior on an `imsg` build that emits `balloon_bundle_id`. On older `imsg` builds that emit no balloon metadata at all, the rows below marked "Two turns" / "N turns" instead fall back to a legacy merge (one turn): OpenClaw cannot structurally tell a split-send from separate sends, so it preserves the pre-metadata merge. Precise separation activates once the build emits balloon metadata.
|
||||
|
||||
| User composes | `chat.db` produces | Flag off (default) | Flag on + window (imsg emits balloon metadata) |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `Dump https://example.com` (one send) | 2 rows ~1 s apart | Two agent turns: "Dump" alone, then URL | One turn: merged text `Dump https://example.com` |
|
||||
| `Save this 📎image.jpg caption` (attachment + text) | 2 rows without URL balloon metadata | Two turns | Two turns after metadata is observed; one merged turn on old/pre-latch metadata-less sessions |
|
||||
| `/status` (standalone command) | 1 row | Instant dispatch | **Wait up to window, then dispatch** |
|
||||
| URL pasted alone | 1 row | Instant dispatch | Wait up to window, then dispatch |
|
||||
| Text + URL sent as two deliberate separate messages, minutes apart | 2 rows outside window | Two turns | Two turns (window expires between them) |
|
||||
| Rapid flood (>10 small DMs inside window) | N rows without URL balloon metadata | N turns | N turns after metadata is observed; one bounded merged turn on old/pre-latch metadata-less sessions |
|
||||
| Two people typing in a group chat | N rows from M senders | M+ turns (one per sender bucket) | M+ turns — group chats are not coalesced |
|
||||
| User composes | `chat.db` produces | Flag off (default) | Flag on + window (imsg emits balloon metadata) |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- | --------------------------------------- | ------------------------------------------------ |
|
||||
| `Dump https://example.com` (one send) | 2 rows ~1 s apart | Two agent turns: "Dump" alone, then URL | One turn: merged text `Dump https://example.com` |
|
||||
| `Save this 📎image.jpg caption` (attachment + text) | 2 rows without URL balloon metadata | Two turns | Two turns (legacy merge on metadata-less builds) |
|
||||
| `/status` (standalone command) | 1 row | Instant dispatch | **Wait up to window, then dispatch** |
|
||||
| URL pasted alone | 1 row | Instant dispatch | Wait up to window, then dispatch |
|
||||
| Text + URL sent as two deliberate separate messages, minutes apart | 2 rows outside window | Two turns | Two turns (window expires between them) |
|
||||
| Rapid flood (>10 small DMs inside window) | N rows without URL balloon metadata | N turns | N turns (legacy merge on metadata-less builds) |
|
||||
| Two people typing in a group chat | N rows from M senders | M+ turns (one per sender bucket) | M+ turns — group chats are not coalesced |
|
||||
|
||||
## Inbound recovery after a bridge or gateway restart
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
summary: "Slack setup and runtime behavior (Socket Mode, HTTP Request URLs, and relay mode)"
|
||||
summary: "Slack setup and runtime behavior (Socket Mode + HTTP Request URLs)"
|
||||
read_when:
|
||||
- Setting up Slack or debugging Slack socket, HTTP, or relay mode
|
||||
- Setting up Slack or debugging Slack socket/HTTP mode
|
||||
title: "Slack"
|
||||
---
|
||||
|
||||
Production-ready for DMs and channels via Slack app integrations. Default mode is Socket Mode; HTTP Request URLs are also supported. Relay mode is intended for managed deployments where a trusted router owns Slack ingress.
|
||||
Production-ready for DMs and channels via Slack app integrations. Default mode is Socket Mode; HTTP Request URLs are also supported.
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Pairing" icon="link" href="/channels/pairing">
|
||||
@@ -41,37 +41,6 @@ Both transports are production-ready and reach feature parity for messaging, sla
|
||||
**Pick HTTP Request URLs** when running multiple Gateway replicas behind a load balancer, when outbound WSS is blocked but inbound HTTPS is allowed, or when you already terminate Slack webhooks at a reverse proxy.
|
||||
</Note>
|
||||
|
||||
### Relay mode
|
||||
|
||||
Relay mode separates Slack ingress from the OpenClaw gateway. A trusted router owns the
|
||||
single Slack Socket Mode connection, chooses a destination gateway, and forwards a typed
|
||||
event over an authenticated websocket. The gateway continues to use its bot token for
|
||||
outbound Slack Web API calls.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
mode: "relay",
|
||||
botToken: { source: "env", provider: "default", id: "SLACK_BOT_TOKEN" },
|
||||
relay: {
|
||||
url: "wss://router.example.com/gateway/ws",
|
||||
authToken: { source: "env", provider: "default", id: "SLACK_RELAY_AUTH_TOKEN" },
|
||||
gatewayId: "team-gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The relay URL must use `wss://` unless it targets localhost. Treat the bearer token and
|
||||
router route table as part of the Slack authorization boundary: routed events enter the
|
||||
normal Slack message handler as authorized activations. A router-provided `slack_identity`
|
||||
in the websocket `hello` frame can set the default outbound username and icon; an explicit
|
||||
identity supplied by the caller still wins. The relay connection reconnects with the same
|
||||
bounded backoff timing used by Socket Mode and clears the router-provided identity whenever
|
||||
it disconnects.
|
||||
|
||||
## Install
|
||||
|
||||
Install Slack before configuring the channel:
|
||||
@@ -894,8 +863,7 @@ The default manifest enables the Slack App Home **Home** tab and subscribes to `
|
||||
|
||||
- `botToken` + `appToken` are required for Socket Mode.
|
||||
- HTTP mode requires `botToken` + `signingSecret`.
|
||||
- Relay mode requires `botToken` plus `relay.url`, `relay.authToken`, and `relay.gatewayId`; it does not use an app token or signing secret.
|
||||
- `botToken`, `appToken`, `signingSecret`, `relay.authToken`, and `userToken` accept plaintext
|
||||
- `botToken`, `appToken`, `signingSecret`, and `userToken` accept plaintext
|
||||
strings or SecretRef objects.
|
||||
- Config tokens override env fallback.
|
||||
- `SLACK_BOT_TOKEN` / `SLACK_APP_TOKEN` env fallback applies only to the default account.
|
||||
|
||||
@@ -336,8 +336,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
Requirement:
|
||||
|
||||
- `channels.telegram.streaming` is `off | partial | block | progress` (default: `partial`)
|
||||
- short initial answer previews are debounced, then materialized after a bounded delay if the run is still active
|
||||
- `progress` keeps one editable status draft for tool progress, shows the stable status label when answer activity arrives before tool progress, clears it at completion, and sends the final answer as a normal message
|
||||
- `progress` keeps one editable status draft for tool progress, clears it at completion, and sends the final answer as a normal message
|
||||
- `streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true` when preview streaming is active)
|
||||
- `streaming.preview.commandText` controls command/exec detail inside those tool-progress lines: `raw` (default, preserves released behavior) or `status` (tool label only)
|
||||
- `streaming.progress.commentary` (default: `false`) opts into assistant commentary/preamble text in the temporary progress draft
|
||||
|
||||
@@ -177,7 +177,7 @@ Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured
|
||||
|
||||
## Full Release Validation
|
||||
|
||||
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. The canonical package Telegram E2E runs inside Package Acceptance, so a full candidate does not start a duplicate live poller. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only for a focused published-package Telegram rerun. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
|
||||
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. With `rerun_group=all` and `release_profile=full`, it also runs `NPM Telegram Beta E2E` against the `release-package-under-test` artifact from release checks. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only when Telegram must prove a different package. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
|
||||
|
||||
See [Full release validation](/reference/full-release-validation) for the
|
||||
stage matrix, exact workflow job names, profile differences, artifacts, and
|
||||
|
||||
@@ -197,7 +197,7 @@ Isolated cron resolves the active model in this order:
|
||||
|
||||
### Fast mode
|
||||
|
||||
Isolated cron fast mode follows the resolved live model selection. Model config `params.fastMode` applies by default, but a stored session `fastMode` override still wins over config. When the resolved mode is `auto`, the cutoff uses the selected model's `params.fastAutoOnSeconds` value, defaulting to 60 seconds.
|
||||
Isolated cron fast mode follows the resolved live model selection. Model config `params.fastMode` applies by default, but a stored session `fastMode` override still wins over config.
|
||||
|
||||
### Live model switch retries
|
||||
|
||||
|
||||
@@ -165,15 +165,10 @@ When you set `--url`, the CLI does not fall back to config or environment creden
|
||||
|
||||
```bash
|
||||
openclaw gateway health --url ws://127.0.0.1:18789
|
||||
openclaw gateway health --port 18789
|
||||
```
|
||||
|
||||
The HTTP `/healthz` endpoint is a liveness probe: it returns once the server can answer HTTP. The HTTP `/readyz` endpoint is stricter and stays red while startup plugin sidecars, channels, or configured hooks are still settling. Local or authenticated detailed readiness responses include an `eventLoop` diagnostic block with event-loop delay, event-loop utilization, CPU core ratio, and a `degraded` flag.
|
||||
|
||||
<ParamField path="--port <port>" type="number">
|
||||
Target a local loopback Gateway on this port. This overrides `OPENCLAW_GATEWAY_URL` and `OPENCLAW_GATEWAY_PORT` for the health call.
|
||||
</ParamField>
|
||||
|
||||
### `gateway usage-cost`
|
||||
|
||||
Fetch usage-cost summaries from session logs.
|
||||
@@ -345,13 +340,8 @@ If multiple probe targets are reachable, it prints all of them. An SSH tunnel, T
|
||||
```bash
|
||||
openclaw gateway probe
|
||||
openclaw gateway probe --json
|
||||
openclaw gateway probe --port 18789
|
||||
```
|
||||
|
||||
<ParamField path="--port <port>" type="number">
|
||||
Use this port for the local loopback probe target and SSH tunnel remote port. Without `--url`, this selects the local loopback target instead of configured gateway environment URL, environment port, or remote targets.
|
||||
</ParamField>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Interpretation">
|
||||
- `Reachable: yes` means at least one target accepted a WebSocket connect.
|
||||
|
||||
@@ -230,8 +230,8 @@ canonical subscription `github-copilot` provider and is **never** selected by
|
||||
The harness claims its provider, runtime, CLI session key, and auth profile
|
||||
prefix in `extensions/copilot/doctor-contract-api.ts`, which
|
||||
`openclaw doctor` auto-loads. For configuration, auth, transcript mirroring,
|
||||
compaction, the declarative doctor contract, and the broader PI vs Codex vs
|
||||
Copilot SDK decision, see [GitHub Copilot agent runtime](/plugins/copilot).
|
||||
compaction, the doctor probe surface, and the broader PI vs Codex vs Copilot
|
||||
SDK decision, see [GitHub Copilot agent runtime](/plugins/copilot).
|
||||
|
||||
## Compatibility contract
|
||||
|
||||
|
||||
@@ -302,13 +302,13 @@ Live transport runners should import the shared scenario ids, baseline
|
||||
coverage helpers, and scenario-selection helper from
|
||||
`openclaw/plugin-sdk/qa-live-transport-scenarios`.
|
||||
|
||||
| Lane | Canary | Mention gating | Bot-to-bot | Allowlist block | Top-level reply | Quote reply | Restart resume | Thread follow-up | Thread isolation | Reaction observation | Help command | Native command registration |
|
||||
| -------- | ------ | -------------- | ---------- | --------------- | --------------- | ----------- | -------------- | ---------------- | ---------------- | -------------------- | ------------ | --------------------------- |
|
||||
| Matrix | x | x | x | x | x | | x | x | x | x | | |
|
||||
| Telegram | x | x | x | | | | | | | | x | |
|
||||
| Discord | x | x | x | | | | | | | | | x |
|
||||
| Slack | x | x | x | x | x | | x | x | x | | | |
|
||||
| WhatsApp | x | x | | x | x | x | x | | | x | x | |
|
||||
| Lane | Canary | Mention gating | Bot-to-bot | Allowlist block | Top-level reply | Restart resume | Thread follow-up | Thread isolation | Reaction observation | Help command | Native command registration |
|
||||
| -------- | ------ | -------------- | ---------- | --------------- | --------------- | -------------- | ---------------- | ---------------- | -------------------- | ------------ | --------------------------- |
|
||||
| Matrix | x | x | x | x | x | x | x | x | x | | |
|
||||
| Telegram | x | x | x | | | | | | | x | |
|
||||
| Discord | x | x | x | | | | | | | | x |
|
||||
| Slack | x | x | x | x | x | x | x | x | | | |
|
||||
| WhatsApp | x | x | | x | x | x | | | x | x | |
|
||||
|
||||
This keeps `qa-channel` as the broad product-behavior suite while Matrix,
|
||||
Telegram, and other live transports share one explicit transport-contract checklist.
|
||||
@@ -731,9 +731,8 @@ Scenario catalog (`extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.
|
||||
`whatsapp-whoami-command`, `whatsapp-context-command`,
|
||||
`whatsapp-native-new-command`.
|
||||
- Reply and final-output behavior: `whatsapp-tool-only-usage-footer`,
|
||||
`whatsapp-reply-to-message`, `whatsapp-group-reply-to-message`,
|
||||
`whatsapp-reply-context-isolation`, `whatsapp-reply-delivery-shape`,
|
||||
`whatsapp-stream-final-message-accounting`.
|
||||
`whatsapp-reply-to-message`, `whatsapp-reply-context-isolation`,
|
||||
`whatsapp-reply-delivery-shape`, `whatsapp-stream-final-message-accounting`.
|
||||
- Inbound media and structured messages: `whatsapp-inbound-image-caption`,
|
||||
`whatsapp-audio-preflight`, `whatsapp-inbound-structured-messages`,
|
||||
`whatsapp-group-audio-gating`. These send real WhatsApp image, audio,
|
||||
@@ -750,9 +749,9 @@ Scenario catalog (`extensions/qa-lab/src/live-transports/whatsapp/whatsapp-live.
|
||||
`whatsapp-approval-plugin-native`.
|
||||
- Status reactions: `whatsapp-status-reactions`.
|
||||
|
||||
The catalog currently contains 36 scenarios. The `live-frontier` default lane is
|
||||
kept small at 10 scenarios for fast smoke coverage. The `mock-openai` default
|
||||
lane runs 31 deterministic scenarios through the real WhatsApp transport while
|
||||
The catalog currently contains 35 scenarios. The `live-frontier` default lane is
|
||||
kept small at 8 scenarios for fast smoke coverage. The `mock-openai` default
|
||||
lane runs 29 deterministic scenarios through the real WhatsApp transport while
|
||||
mocking only model output. Approval scenarios and a few heavier/blocking checks
|
||||
remain explicit by scenario id.
|
||||
|
||||
|
||||
@@ -160,10 +160,9 @@ Legacy key migration:
|
||||
Telegram:
|
||||
|
||||
- Uses `sendMessage` + `editMessageText` preview updates across DMs and group/topics.
|
||||
- Short initial previews are still debounced for push-notification UX, but Telegram now materializes them after a bounded delay so active runs do not stay visually silent.
|
||||
- Final text edits the active preview in place; long finals reuse that message for the first chunk and send only the remaining chunks.
|
||||
- `block` mode rotates the preview into a new message at `streaming.preview.chunk.maxChars` (default 800, capped at Telegram's 4096 edit limit); other modes grow one preview up to 4096 characters.
|
||||
- `progress` mode keeps tool progress in an editable status draft, materializes the status label when answer streaming is active but no tool line is available yet, clears that draft at completion, and sends the final answer through normal delivery.
|
||||
- `progress` mode keeps tool progress in an editable status draft, clears that draft at completion, and sends the final answer through normal delivery.
|
||||
- If the final edit fails before the completed text is confirmed, OpenClaw uses normal final delivery and cleans up the stale preview.
|
||||
- Preview streaming is skipped when Telegram block streaming is explicitly enabled (to avoid double-streaming).
|
||||
- `/reasoning stream` can write reasoning to a transient preview that is deleted after final delivery.
|
||||
|
||||
@@ -249,10 +249,9 @@ Shared defaults for bounded runtime context surfaces.
|
||||
- `toolResultMaxChars`: advanced live tool-result ceiling used for persisted
|
||||
results and overflow recovery. Leave unset for the model-context auto cap:
|
||||
`16000` chars below 100K tokens, `32000` chars at 100K+ tokens, and `64000`
|
||||
chars at 200K+ tokens. Explicit values up to `1000000` are accepted for
|
||||
long-context models, but the effective cap is still limited to about 30% of
|
||||
the model context window. `openclaw doctor --deep` prints the effective cap,
|
||||
and doctor warns only when an explicit override is stale or has no effect.
|
||||
chars at 200K+ tokens. The effective cap is still limited to about 30% of the
|
||||
model context window. `openclaw doctor --deep` prints the effective cap, and
|
||||
doctor warns only when an explicit override is stale or has no effect.
|
||||
- `postCompactionMaxChars`: AGENTS.md excerpt cap used during post-compaction
|
||||
refresh injection.
|
||||
|
||||
@@ -1099,7 +1098,7 @@ for provider examples and precedence.
|
||||
- `skills`: optional per-agent skill allowlist. If omitted, the agent inherits `agents.defaults.skills` when set; an explicit list replaces defaults instead of merging, and `[]` means no skills.
|
||||
- `thinkingDefault`: optional per-agent default thinking level (`off | minimal | low | medium | high | xhigh | adaptive | max`). Overrides `agents.defaults.thinkingDefault` for this agent when no per-message or session override is set. The selected provider/model profile controls which values are valid; for Google Gemini, `adaptive` keeps provider-owned dynamic thinking (`thinkingLevel` omitted on Gemini 3/3.1, `thinkingBudget: -1` on Gemini 2.5).
|
||||
- `reasoningDefault`: optional per-agent default reasoning visibility (`on | off | stream`). Overrides `agents.defaults.reasoningDefault` for this agent when no per-message or session reasoning override is set.
|
||||
- `fastModeDefault`: optional per-agent default for fast mode (`"auto" | true | false`). Applies when no per-message or session fast-mode override is set.
|
||||
- `fastModeDefault`: optional per-agent default for fast mode (`true | false`). Applies when no per-message or session fast-mode override is set.
|
||||
- `models`: optional per-agent model catalog/runtime overrides keyed by full `provider/model` ids. Use `models["provider/model"].agentRuntime` for per-agent runtime exceptions.
|
||||
- `runtime`: optional per-agent runtime descriptor. Use `type: "acp"` with `runtime.acp` defaults (`agent`, `backend`, `mode`, `cwd`) when the agent should default to ACP harness sessions.
|
||||
- `identity.avatar`: workspace-relative path, `http(s)` URL, or `data:` URI.
|
||||
|
||||
@@ -160,6 +160,7 @@ must be paired with `--lint`; regular doctor and repair runs reject them.
|
||||
- State integrity and permissions checks (sessions, transcripts, state dir).
|
||||
- Config file permission checks (chmod 600) when running locally.
|
||||
- Model auth health: checks OAuth expiry, can refresh expiring tokens, and reports auth-profile cooldown/disabled states.
|
||||
- Extra workspace dir detection (`~/openclaw`).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Gateway, services, and supervisors">
|
||||
@@ -468,14 +469,14 @@ That stages grounded durable candidates into the short-term dreaming store while
|
||||
<Accordion title="10. systemd linger (Linux)">
|
||||
If running as a systemd user service, doctor ensures lingering is enabled so the gateway stays alive after logout.
|
||||
</Accordion>
|
||||
<Accordion title="11. Workspace status (skills, plugins, and TaskFlows)">
|
||||
<Accordion title="11. Workspace status (skills, plugins, and legacy dirs)">
|
||||
Doctor prints a summary of the workspace state for the default agent:
|
||||
|
||||
- **Skills status**: counts eligible, missing-requirements, and allowlist-blocked skills.
|
||||
- **Legacy workspace dirs**: warns when `~/openclaw` or other legacy workspace directories exist alongside the current workspace.
|
||||
- **Plugin status**: counts enabled/disabled/errored plugins; lists plugin IDs for any errors; reports bundle plugin capabilities.
|
||||
- **Plugin compatibility warnings**: flags plugins that have compatibility issues with the current runtime.
|
||||
- **Plugin diagnostics**: surfaces any load-time warnings or errors emitted by the plugin registry.
|
||||
- **TaskFlow recovery**: surfaces suspicious managed TaskFlows that need manual inspection or cancellation.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="11b. Bootstrap file size">
|
||||
|
||||
@@ -445,7 +445,6 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- `sessions.get` returns the full stored session row.
|
||||
- Chat execution still uses `chat.history`, `chat.send`, `chat.abort`, and `chat.inject`. `chat.history` is display-normalized for UI clients: inline directive tags are stripped from visible text, plain-text tool-call XML payloads (including `<tool_call>...</tool_call>`, `<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and truncated tool-call blocks) and leaked ASCII/full-width model control tokens are stripped, pure silent-token assistant rows such as exact `NO_REPLY` / `no_reply` are omitted, and oversized rows can be replaced with placeholders.
|
||||
- `chat.message.get` is the additive bounded full-message reader for a single visible transcript entry. Clients pass `sessionKey`, optional `agentId` when the session selection is agent-scoped, plus a transcript `messageId` previously surfaced through `chat.history`, and the Gateway returns the same display-normalized projection without the lightweight history truncation cap when the stored entry is still available and not oversized.
|
||||
- `chat.send` accepts one-turn `fastMode: "auto"` to use fast mode for model calls started before the auto cutoff, then start later retry, fallback, tool-result, or continuation calls without fast mode. The cutoff defaults to 60 seconds and can be configured per model with `agents.defaults.models["<provider>/<model>"].params.fastAutoOnSeconds`. A `chat.send` caller can pass one-turn `fastAutoOnSeconds` to override the cutoff for that request.
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -174,7 +174,6 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
|
||||
- **Per session:** send `/fast on` while the session is using `openai/gpt-5.5`.
|
||||
- **Per model default:** set `agents.defaults.models["openai/gpt-5.5"].params.fastMode` to `true`.
|
||||
- **Automatic cutoff:** use `/fast auto` or `params.fastMode: "auto"` to start new model calls fast until the auto cutoff, then start later retry, fallback, tool-result, or continuation calls without fast mode. The cutoff defaults to 60 seconds; set `params.fastAutoOnSeconds` on the active model to change it.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -185,8 +184,7 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
models: {
|
||||
"openai/gpt-5.5": {
|
||||
params: {
|
||||
fastMode: "auto",
|
||||
fastAutoOnSeconds: 30,
|
||||
fastMode: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -195,7 +193,7 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
}
|
||||
```
|
||||
|
||||
For OpenAI, fast mode maps to `service_tier = "priority"` on supported native Responses requests. Session `/fast` overrides beat config defaults. Codex app-server turns can only receive the tier at turn start, so `auto` applies on the next OpenClaw-started model turn rather than inside one already-running app-server turn.
|
||||
For OpenAI, fast mode maps to `service_tier = "priority"` on supported native Responses requests. Session `/fast` overrides beat config defaults.
|
||||
|
||||
See [Thinking and fast mode](/tools/thinking) and [OpenAI fast mode](/providers/openai#fast-mode).
|
||||
|
||||
|
||||
@@ -15,18 +15,15 @@ OpenClaw treats **wake words as a single global list** owned by the **Gateway**.
|
||||
|
||||
## Storage (Gateway host)
|
||||
|
||||
Wake words and routing rules are stored in the gateway state database:
|
||||
Wake words are stored on the gateway machine at:
|
||||
|
||||
- `~/.openclaw/state/openclaw.sqlite`
|
||||
- `~/.openclaw/settings/voicewake.json`
|
||||
|
||||
The active tables are:
|
||||
Shape:
|
||||
|
||||
- `voicewake_triggers`
|
||||
- `voicewake_routing_config`
|
||||
- `voicewake_routing_routes`
|
||||
|
||||
Legacy `settings/voicewake.json` and `settings/voicewake-routing.json` files are
|
||||
doctor migration inputs only; runtime reads and writes the SQLite tables.
|
||||
```json
|
||||
{ "triggers": ["openclaw", "claude", "computer"], "updatedAtMs": 1730000000000 }
|
||||
```
|
||||
|
||||
## Protocol
|
||||
|
||||
|
||||
@@ -143,12 +143,39 @@ The native Codex app-server harness supports context engines that require
|
||||
pre-prompt assembly. Generic CLI backends, including `codex-cli`, do not provide
|
||||
that host capability.
|
||||
|
||||
Codex thread bindings live in OpenClaw's SQLite plugin state and use the stable
|
||||
agent-scoped OpenClaw session key, or an opaque conversation-binding id, as
|
||||
their owner. Physical session ids fence delayed cleanup but may rotate without
|
||||
losing the Codex thread. Context-engine compaction adopts the successor id
|
||||
before continuing native Codex compaction. The bounded store rejects a new
|
||||
binding at its safety limit instead of evicting an existing thread's continuity
|
||||
record.
|
||||
Conversation binds create or resume their Codex thread on the first bound
|
||||
message after channel approval; an abandoned approval consumes no thread row.
|
||||
That first message carries the prepared thread directly into its turn.
|
||||
Subsequent messages use a metadata-only resume to subscribe the shared client,
|
||||
then unsubscribe after the turn completes.
|
||||
The runtime does not poll transcript-adjacent binding files. Upgrades from
|
||||
releases that used `*.jsonl.codex-app-server.json` sidecars migrate them during
|
||||
normal startup preflight. `openclaw doctor --fix` can run the same migration
|
||||
manually.
|
||||
Successfully matched sidecars are archived before the new runtime resumes their
|
||||
threads. Migration imports durable thread ownership only; it does not infer
|
||||
Codex context usage from OpenClaw counters or crawl Codex rollout files. For
|
||||
agent-session harness bindings, the next resume attempts to restore a cached
|
||||
native snapshot when Codex has one, and ongoing turns persist the current-context
|
||||
usage reported by app-server notifications, not the cumulative thread lifetime
|
||||
total. Conversation bindings
|
||||
keep metadata-only resumes and leave continuity and compaction with the native
|
||||
Codex thread. Conflicting or ambiguous sidecars stay in place with a warning for
|
||||
operator review.
|
||||
|
||||
For Codex-backed agents, `/compact` starts native Codex app-server compaction on
|
||||
the bound thread. OpenClaw does not wait for completion, impose an OpenClaw
|
||||
timeout, restart the shared app-server, or fall back to a context-engine or
|
||||
public OpenAI summarizer. If the native Codex thread binding is missing or
|
||||
stale, the command fails closed so the operator sees the real runtime boundary
|
||||
instead of silently switching compaction backends.
|
||||
the bound thread. OpenClaw bounds the request-acceptance RPC but does not wait
|
||||
for compaction completion, restart the shared app-server, or fall back to a
|
||||
context-engine or public OpenAI summarizer. If the native Codex thread binding
|
||||
is missing or stale, the command fails closed so the operator sees the real
|
||||
runtime boundary instead of silently switching compaction backends.
|
||||
|
||||
```json5
|
||||
{
|
||||
|
||||
@@ -33,12 +33,15 @@ For the broader model/provider/runtime split, start with
|
||||
- A GitHub Copilot subscription that can drive the Copilot CLI (or a
|
||||
`gitHubToken` env / auth-profile entry for headless / cron runs).
|
||||
- A writable `copilotHome` directory. The harness defaults to
|
||||
`<agentDir>/copilot` when OpenClaw provides an agent directory, otherwise
|
||||
`~/.openclaw/agents/<agentId>/copilot` for full per-agent isolation.
|
||||
`~/.openclaw/agents/<agentId>/copilot` for full per-agent isolation. The
|
||||
platform default (`%APPDATA%\copilot` on Windows, `$XDG_CONFIG_HOME/copilot`
|
||||
or `~/.config/copilot` elsewhere) is used as the doctor probe fallback when
|
||||
no explicit home is set.
|
||||
|
||||
`openclaw doctor` runs the plugin
|
||||
[doctor contract](#doctor) for declarative session-state ownership and future
|
||||
compatibility migrations. It does not run Copilot CLI environment probes.
|
||||
[doctor contract](#doctor-and-probes) for the extension; failures there are
|
||||
the canonical way to confirm the environment is ready before opting an agent
|
||||
in.
|
||||
|
||||
## Plugin install
|
||||
|
||||
@@ -150,6 +153,10 @@ the same directory), or `~/.openclaw/agents/<agentId>/copilot` otherwise.
|
||||
Override with `copilotHome: <path>` on the attempt input when you need a
|
||||
custom location (for example, a shared mount for migration).
|
||||
|
||||
`probeCopilotAuthShape` (see [Doctor and probes](#doctor-and-probes)) is the
|
||||
pure shape check that validates which of the modes above will be used.
|
||||
It does not perform a live SDK handshake.
|
||||
|
||||
## Configuration surface
|
||||
|
||||
The harness reads its config from per-attempt input
|
||||
@@ -232,7 +239,7 @@ asserted in
|
||||
[`extensions/copilot/harness.test.ts`](https://github.com/openclaw/openclaw/blob/main/extensions/copilot/harness.test.ts)
|
||||
under `describe("runSideQuestion")`.
|
||||
|
||||
## Doctor
|
||||
## Doctor and probes
|
||||
|
||||
`extensions/copilot/doctor-contract-api.ts` is auto-loaded by
|
||||
`src/plugins/doctor-contract-registry.ts`. It contributes:
|
||||
@@ -244,6 +251,18 @@ under `describe("runSideQuestion")`.
|
||||
runtime `copilot`; CLI session key `copilot`; auth profile
|
||||
prefix `github-copilot:`.
|
||||
|
||||
`extensions/copilot/src/doctor-probes.ts` exports three imperative probes
|
||||
that hosts (including `openclaw doctor`) can call to verify the environment:
|
||||
|
||||
| Probe | What it checks | Reasons it can fail |
|
||||
| -------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `probeCopilotCliVersion` | `copilot --version` exits 0 with a non-empty version string | `non-zero-exit`, `empty-version`, `spawn-failed`, `spawn-error`, `probe-timeout` |
|
||||
| `probeCopilotHomeWritable` | `mkdir -p copilotHome` + write + rm a marker file | `copilothome-not-writable` (with the underlying fs error in `details.rawError`) |
|
||||
| `probeCopilotAuthShape` | At least one of `useLoggedInUser`, `gitHubToken`, or `profileId`+`profileVersion` | `no-auth-source` |
|
||||
|
||||
Each probe accepts a DI seam (`spawnFn`, `fsApi`) so tests do not spawn the
|
||||
real Copilot CLI or touch the host fs.
|
||||
|
||||
## Limitations
|
||||
|
||||
- The harness only claims the canonical `github-copilot` provider at MVP.
|
||||
|
||||
@@ -146,7 +146,6 @@ observation-only.
|
||||
- `subagent_delivery_target` - compatibility hook for completion delivery when no core session binding can project a route.
|
||||
- `subagent_spawning` - deprecated compatibility hook. Core now prepares `thread: true` subagent bindings through channel session-binding adapters before `subagent_spawned` fires.
|
||||
- `subagent_spawned` includes `resolvedModel` and `resolvedProvider` when OpenClaw has resolved the child session's native model before launch.
|
||||
- `subagent_ended` carries `targetSessionKey` (identity — this matches `subagent_spawned.childSessionKey`), `targetKind` (`"subagent"` or `"acp"`), `reason`, optional `outcome` (`"ok"`, `"error"`, `"timeout"`, `"killed"`, `"reset"`, or `"deleted"`), optional `error`, `runId`, `endedAt`, `accountId`, and `sendFarewell`. It does **not** include `agentId` or `childSessionKey`; use `targetSessionKey` to correlate with the corresponding `subagent_spawned` event.
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ Each entry lists the package, distribution route, and description.
|
||||
|
||||
- **[slack](/plugins/reference/slack)** (`@openclaw/slack`) - npm; ClawHub. OpenClaw Slack channel plugin for channels, DMs, commands, and app events.
|
||||
|
||||
- **[stepfun](/plugins/reference/stepfun)** (`@openclaw/stepfun-provider`) - npm; ClawHub: `clawhub:@openclaw/stepfun-provider`. Adds StepFun, StepFun Plan model provider support to OpenClaw.
|
||||
- **[stepfun](/plugins/reference/stepfun)** (`@openclaw/stepfun-provider`) - npm. Adds StepFun, StepFun Plan model provider support to OpenClaw.
|
||||
|
||||
- **[synology-chat](/plugins/reference/synology-chat)** (`@openclaw/synology-chat`) - npm; ClawHub. Synology Chat channel plugin for OpenClaw channels and direct messages.
|
||||
|
||||
|
||||
@@ -16,4 +16,4 @@ Experimental Canvas control and A2UI rendering surfaces for paired nodes.
|
||||
|
||||
## Surface
|
||||
|
||||
contracts: tools; skills
|
||||
contracts: tools
|
||||
|
||||
@@ -16,7 +16,7 @@ OpenClaw Discord channel plugin for channels, DMs, commands, and app events.
|
||||
|
||||
## Surface
|
||||
|
||||
channels: discord; contracts: transcriptSourceProviders; skills
|
||||
channels: discord; contracts: transcriptSourceProviders
|
||||
|
||||
## Related docs
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ OpenClaw Slack channel plugin for channels, DMs, commands, and app events.
|
||||
|
||||
## Surface
|
||||
|
||||
channels: slack; skills
|
||||
channels: slack
|
||||
|
||||
## Related docs
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Adds StepFun, StepFun Plan model provider support to OpenClaw.
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/stepfun-provider`
|
||||
- Install route: npm; ClawHub: `clawhub:@openclaw/stepfun-provider`
|
||||
- Install route: npm
|
||||
|
||||
## Surface
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls.
|
||||
|
||||
## Surface
|
||||
|
||||
contracts: tools; skills
|
||||
contracts: tools
|
||||
|
||||
## Related docs
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ OpenClaw WhatsApp channel plugin for WhatsApp Web chats.
|
||||
|
||||
## Surface
|
||||
|
||||
channels: whatsapp; skills
|
||||
channels: whatsapp
|
||||
|
||||
## Related docs
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ usage endpoint failed or returned no usable usage data.
|
||||
| `plugin-sdk/reply-history` | Shared short-window reply-history helpers. New message-turn code should use `createChannelHistoryWindow`; lower-level map helpers remain deprecated compatibility exports only |
|
||||
| `plugin-sdk/reply-reference` | `createReplyReferencePlanner` |
|
||||
| `plugin-sdk/reply-chunking` | Narrow text/markdown chunking helpers |
|
||||
| `plugin-sdk/session-store-runtime` | Session workflow helpers (`getSessionEntry`, `listSessionEntries`, `patchSessionEntry`, `upsertSessionEntry`), bounded recent user/assistant transcript text reads by session identity, legacy session store path/session-key helpers, updated-at reads, and transition-only whole-store/file-path compatibility helpers |
|
||||
| `plugin-sdk/session-store-runtime` | Session workflow helpers (`getSessionEntry`, `listSessionEntries`, `patchSessionEntry`, `upsertSessionEntry`), legacy session store path/session-key helpers, updated-at reads, and transition-only whole-store/file-path compatibility helpers |
|
||||
| `plugin-sdk/session-transcript-runtime` | Transcript identity, scoped target/read/write helpers, update publishing, write locks, and transcript memory hit keys |
|
||||
| `plugin-sdk/sqlite-runtime` | Focused SQLite agent-schema, path, and transaction helpers for first-party runtime |
|
||||
| `plugin-sdk/cron-store-runtime` | Cron store path/load/save helpers |
|
||||
|
||||
@@ -759,7 +759,7 @@ Tool name: `voice_call`.
|
||||
| `end_call` | `callId` |
|
||||
| `get_status` | `callId` |
|
||||
|
||||
The voice-call plugin ships a matching agent skill.
|
||||
This repo ships a matching skill doc at `skills/voice-call/SKILL.md`.
|
||||
|
||||
## Gateway RPC
|
||||
|
||||
|
||||
@@ -915,17 +915,17 @@ the Server-side compaction accordion below.
|
||||
<Accordion title="Fast mode">
|
||||
OpenClaw exposes a shared fast-mode toggle for `openai/*`:
|
||||
|
||||
- **Chat/UI:** `/fast status|auto|on|off`
|
||||
- **Chat/UI:** `/fast status|on|off`
|
||||
- **Config:** `agents.defaults.models["<provider>/<model>"].params.fastMode`
|
||||
|
||||
When enabled, OpenClaw maps fast mode to OpenAI priority processing (`service_tier = "priority"`). Existing `service_tier` values are preserved, and fast mode does not rewrite `reasoning` or `text.verbosity`. `fastMode: "auto"` starts new model calls fast until the auto cutoff, then starts later retry, fallback, tool-result, or continuation calls without fast mode. The cutoff defaults to 60 seconds; set `params.fastAutoOnSeconds` on the active model to change it.
|
||||
When enabled, OpenClaw maps fast mode to OpenAI priority processing (`service_tier = "priority"`). Existing `service_tier` values are preserved, and fast mode does not rewrite `reasoning` or `text.verbosity`.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-5.5": { params: { fastMode: "auto", fastAutoOnSeconds: 30 } },
|
||||
"openai/gpt-5.5": { params: { fastMode: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,10 +28,8 @@ The provider includes:
|
||||
| ------------------------------- | --------------------- |
|
||||
| `opencode-go/glm-5` | GLM-5 |
|
||||
| `opencode-go/glm-5.1` | GLM-5.1 |
|
||||
| `opencode-go/glm-5.2` | GLM-5.2 |
|
||||
| `opencode-go/kimi-k2.5` | Kimi K2.5 |
|
||||
| `opencode-go/kimi-k2.6` | Kimi K2.6 (3x limits) |
|
||||
| `opencode-go/kimi-k2.7-code` | Kimi K2.7 Code |
|
||||
| `opencode-go/deepseek-v4-pro` | DeepSeek V4 Pro |
|
||||
| `opencode-go/deepseek-v4-flash` | DeepSeek V4 Flash |
|
||||
| `opencode-go/mimo-v2-omni` | MiMo V2 Omni |
|
||||
@@ -41,8 +39,6 @@ The provider includes:
|
||||
| `opencode-go/qwen3.5-plus` | Qwen3.5 Plus |
|
||||
| `opencode-go/qwen3.6-plus` | Qwen3.6 Plus |
|
||||
|
||||
GLM-5.2 uses a 1M-token context window and supports up to 131K output tokens.
|
||||
|
||||
## Getting started
|
||||
|
||||
<Tabs>
|
||||
|
||||
@@ -126,11 +126,6 @@ The manifest-backed catalog currently includes:
|
||||
GLM models are available as `zai/<model>` (example: `zai/glm-5`).
|
||||
</Tip>
|
||||
|
||||
<Tip>
|
||||
GLM-5.2 supports `off`, `low`, `high`, and `max` thinking levels. OpenClaw maps
|
||||
`low` and `high` to Z.AI high reasoning effort, and `max` to max effort.
|
||||
</Tip>
|
||||
|
||||
<Note>
|
||||
Coding Plan setup defaults to `zai/glm-5.2`; general API setup keeps
|
||||
`zai/glm-5.1`. Endpoint auto-detection falls back to `glm-5.1` or `glm-4.7`
|
||||
|
||||
@@ -228,9 +228,9 @@ release state.
|
||||
`OpenClaw Release Checks` for install smoke, package acceptance, cross-OS
|
||||
package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full
|
||||
runs always include exhaustive live/E2E and Docker release-path soak;
|
||||
`run_release_soak=true` is retained for an explicit beta soak. Package
|
||||
Acceptance provides the canonical package Telegram E2E during candidate
|
||||
validation, avoiding a second concurrent live poller.
|
||||
`run_release_soak=true` is retained for an explicit beta soak. With
|
||||
`release_profile=full` and `rerun_group=all`, it also runs package Telegram
|
||||
E2E against the `release-package-under-test` artifact from release checks.
|
||||
Provide `release_package_spec` after publishing a beta to reuse the shipped
|
||||
npm package across release checks, Package Acceptance, and package Telegram
|
||||
E2E without rebuilding the release tarball. Provide
|
||||
@@ -460,16 +460,20 @@ gh workflow run full-release-validation.yml \
|
||||
```
|
||||
|
||||
The workflow resolves the target ref, dispatches manual `CI` with
|
||||
`target_ref=<release-ref>`, then dispatches `OpenClaw Release Checks`.
|
||||
`OpenClaw Release Checks` fans out install smoke, cross-OS release checks,
|
||||
live/E2E Docker release-path coverage when soak is enabled, Package Acceptance
|
||||
with the canonical Telegram package E2E, QA Lab parity, live Matrix, and live
|
||||
Telegram. A full/all run is only acceptable when the `Full Release Validation`
|
||||
summary shows `normal_ci`, `plugin_prerelease`, and `release_checks` as
|
||||
successful, unless a focused rerun intentionally skipped the separate `Plugin
|
||||
Prerelease` child. Use the standalone `npm-telegram` child only for a focused
|
||||
published-package rerun with `release_package_spec` or
|
||||
`npm_telegram_package_spec`. The final
|
||||
`target_ref=<release-ref>`, dispatches `OpenClaw Release Checks`, prepares a
|
||||
parent `release-package-under-test` artifact for package-facing checks, and
|
||||
dispatches standalone package Telegram E2E when `release_profile=full` with
|
||||
`rerun_group=all` or when `release_package_spec` or
|
||||
`npm_telegram_package_spec` is set. `OpenClaw Release
|
||||
Checks` then fans out install smoke, cross-OS release checks, live/E2E Docker
|
||||
release-path coverage when soak is enabled, Package Acceptance with Telegram
|
||||
package QA, QA Lab parity, live Matrix, and live Telegram. A full/all run is
|
||||
only acceptable when the `Full Release Validation` summary shows `normal_ci`,
|
||||
`plugin_prerelease`, and `release_checks` as successful, unless a focused rerun
|
||||
intentionally skipped the separate `Plugin Prerelease` child. In full/all mode,
|
||||
the `npm_telegram` child must also be successful; outside full/all it is skipped
|
||||
unless a published `release_package_spec` or `npm_telegram_package_spec` was
|
||||
provided. The final
|
||||
verifier summary includes slowest-job tables for each child run, so the release
|
||||
manager can see the current critical path without downloading logs.
|
||||
See [Full release validation](/reference/full-release-validation) for the
|
||||
@@ -554,8 +558,8 @@ runs only the release-only plugin child, `release-checks` runs every release
|
||||
box, and the narrower release groups are `install-smoke`, `cross-os`,
|
||||
`live-e2e`, `package`, `qa`, `qa-parity`, `qa-live`, and `npm-telegram`.
|
||||
Focused `npm-telegram` reruns require `release_package_spec` or
|
||||
`npm_telegram_package_spec`; full/all runs use the canonical package Telegram
|
||||
E2E inside Package Acceptance. Focused
|
||||
`npm_telegram_package_spec`; full/all runs with `release_profile=full` use the
|
||||
release-checks package artifact. Focused
|
||||
cross-OS reruns can add `cross_os_suite_filter=windows/packaged-upgrade` or
|
||||
another OS/suite filter. QA release-check failures block normal release
|
||||
validation, including required OpenClaw dynamic tool drift in the standard tier.
|
||||
|
||||
@@ -53,7 +53,8 @@ that plugin, then runs Codex CLI preflight and same-session OpenAI agent turns.
|
||||
| Vitest and normal CI | **Job:** `Run normal full CI`<br />**Child workflow:** `CI`<br />**Proves:** manual full CI graph against the target ref, including Linux Node lanes, bundled plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, Control UI i18n, and Android via the umbrella.<br />**Rerun:** `rerun_group=ci`. |
|
||||
| Plugin prerelease | **Job:** `Run plugin prerelease validation`<br />**Child workflow:** `Plugin Prerelease`<br />**Proves:** release-only plugin static checks, agentic plugin coverage, full extension batch shards, plugin prerelease Docker lanes, and a non-blocking `plugin-inspector-advisory` artifact for compatibility triage.<br />**Rerun:** `rerun_group=plugin-prerelease`. |
|
||||
| Release checks | **Job:** `Run release/live/Docker/QA validation`<br />**Child workflow:** `OpenClaw Release Checks`<br />**Proves:** install smoke, cross-OS package checks, Package Acceptance, QA Lab parity, live Matrix, and live Telegram. Stable and full profiles also run exhaustive live/E2E suites and Docker release-path chunks; beta can opt in with `run_release_soak=true`.<br />**Rerun:** `rerun_group=release-checks` or a narrower release-checks handle. |
|
||||
| Package Telegram | **Job:** `Run package Telegram E2E`<br />**Child workflow:** `NPM Telegram Beta E2E`<br />**Proves:** a focused published-package Telegram E2E when `release_package_spec` or `npm_telegram_package_spec` is set. Full candidate validation uses the canonical Package Acceptance Telegram E2E instead.<br />**Rerun:** `rerun_group=npm-telegram` with `release_package_spec` or `npm_telegram_package_spec`. |
|
||||
| Package artifact | **Job:** `Prepare release package artifact`<br />**Child workflow:** none<br />**Proves:** creates the parent `release-package-under-test` tarball early enough for package-facing checks that do not need to wait for `OpenClaw Release Checks`.<br />**Rerun:** rerun the umbrella or provide `release_package_spec` for published-package reruns. |
|
||||
| Package Telegram | **Job:** `Run package Telegram E2E`<br />**Child workflow:** `NPM Telegram Beta E2E`<br />**Proves:** parent-artifact-backed Telegram package proof for `rerun_group=all` with `release_profile=full`, or published-package Telegram proof when `release_package_spec` or `npm_telegram_package_spec` is set.<br />**Rerun:** `rerun_group=npm-telegram` with `release_package_spec` or `npm_telegram_package_spec`. |
|
||||
| Umbrella verifier | **Job:** `Verify full validation`<br />**Child workflow:** none<br />**Proves:** re-checks recorded child run conclusions and appends slowest-job tables from child workflows.<br />**Rerun:** rerun only this job after rerunning a failed child to green. |
|
||||
|
||||
For `ref=main` and `rerun_group=all`, a newer umbrella supersedes an older one.
|
||||
@@ -75,7 +76,7 @@ or Docker-facing stages need it.
|
||||
| Cross-OS | **Job:** `cross_os_release_checks`<br />**Backing workflow:** `OpenClaw Cross-OS Release Checks (Reusable)`<br />**Tests:** fresh and upgrade lanes on Linux, Windows, and macOS for the selected provider and mode, using the candidate tarball plus a baseline package.<br />**Rerun:** `rerun_group=cross-os`. |
|
||||
| Repo and live E2E | **Job:** `Run repo/live E2E validation`<br />**Backing workflow:** `OpenClaw Live And E2E Checks (Reusable)`<br />**Tests:** repository E2E, live cache, OpenAI websocket streaming, native live provider and plugin shards, and Docker-backed live model/backend/gateway harnesses selected by `release_profile`.<br />**Runs:** `run_release_soak=true`, `release_profile=full`, or focused `rerun_group=live-e2e`.<br />**Rerun:** `rerun_group=live-e2e`, optionally with `live_suite_filter`. |
|
||||
| Docker release path | **Job:** `Run Docker release-path validation`<br />**Backing workflow:** `OpenClaw Live And E2E Checks (Reusable)`<br />**Tests:** release-path Docker chunks against the shared package artifact.<br />**Runs:** `run_release_soak=true`, `release_profile=full`, or focused `rerun_group=live-e2e`.<br />**Rerun:** `rerun_group=live-e2e`. |
|
||||
| Package Acceptance | **Job:** `Run package acceptance`<br />**Backing workflow:** `Package Acceptance`<br />**Tests:** offline plugin package fixtures, plugin update, the canonical mock-OpenAI Telegram package E2E, and published-upgrade survivor checks against the same tarball. Blocking release checks use the default latest published baseline; soak checks expand to every stable npm release at or after `2026.4.23` plus reported-issue fixtures.<br />**Rerun:** `rerun_group=package`. |
|
||||
| Package Acceptance | **Job:** `Run package acceptance`<br />**Backing workflow:** `Package Acceptance`<br />**Tests:** offline plugin package fixtures, plugin update, mock-OpenAI Telegram package acceptance, and published-upgrade survivor checks against the same tarball. Blocking release checks use the default latest published baseline; soak checks expand to every stable npm release at or after `2026.4.23` plus reported-issue fixtures.<br />**Rerun:** `rerun_group=package`. |
|
||||
| QA parity | **Job:** `Run QA Lab parity lane` and `Run QA Lab parity report`<br />**Backing workflow:** direct jobs<br />**Tests:** candidate and baseline agentic parity packs, then the parity report.<br />**Rerun:** `rerun_group=qa-parity` or `rerun_group=qa`. |
|
||||
| QA live Matrix | **Job:** `Run QA Lab live Matrix lane`<br />**Backing workflow:** direct job<br />**Tests:** fast live Matrix QA profile in the `qa-live-shared` environment.<br />**Rerun:** `rerun_group=qa-live` or `rerun_group=qa`. |
|
||||
| QA live Telegram | **Job:** `Run QA Lab live Telegram lane`<br />**Backing workflow:** direct job<br />**Tests:** live Telegram QA with Convex CI credential leases.<br />**Rerun:** `rerun_group=qa-live` or `rerun_group=qa`. |
|
||||
@@ -106,9 +107,9 @@ commands with package artifact and image reuse inputs when available.
|
||||
It does not remove normal full CI, Plugin Prerelease, install smoke, package
|
||||
acceptance, or QA Lab. Stable and full profiles always run exhaustive repo/live
|
||||
E2E and Docker release-path soak coverage. The beta profile can opt in with
|
||||
`run_release_soak=true`. Package Acceptance supplies the canonical package
|
||||
Telegram E2E for every full candidate, so the umbrella does not duplicate that
|
||||
live poller.
|
||||
`run_release_soak=true`. The full profile also makes the umbrella run package
|
||||
Telegram E2E against the parent release package artifact when `rerun_group=all`,
|
||||
so a full pre-publish candidate does not silently skip that Telegram package lane.
|
||||
|
||||
| Profile | Intended use | Included live/provider coverage |
|
||||
| --------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -188,7 +189,7 @@ workflow first, then rerun the smallest matching handle above.
|
||||
|
||||
Useful artifacts:
|
||||
|
||||
- `release-package-under-test` from `OpenClaw Release Checks`
|
||||
- `release-package-under-test` from the Full Release Validation parent and `OpenClaw Release Checks`
|
||||
- Docker release-path artifacts under `.artifacts/docker-tests/`
|
||||
- Package Acceptance `package-under-test` and Docker acceptance artifacts
|
||||
- Cross-OS release-check artifacts for each OS and suite
|
||||
|
||||
@@ -72,12 +72,10 @@ Scope intent:
|
||||
- `channels.telegram.accounts.*.webhookSecret`
|
||||
- `channels.slack.botToken`
|
||||
- `channels.slack.appToken`
|
||||
- `channels.slack.relay.authToken`
|
||||
- `channels.slack.userToken`
|
||||
- `channels.slack.signingSecret`
|
||||
- `channels.slack.accounts.*.botToken`
|
||||
- `channels.slack.accounts.*.appToken`
|
||||
- `channels.slack.accounts.*.relay.authToken`
|
||||
- `channels.slack.accounts.*.userToken`
|
||||
- `channels.slack.accounts.*.signingSecret`
|
||||
- `channels.sms.authToken`
|
||||
|
||||
@@ -295,13 +295,6 @@
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "channels.slack.accounts.*.relay.authToken",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "channels.slack.accounts.*.relay.authToken",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "channels.slack.accounts.*.signingSecret",
|
||||
"configFile": "openclaw.json",
|
||||
@@ -330,13 +323,6 @@
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "channels.slack.relay.authToken",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "channels.slack.relay.authToken",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "channels.slack.signingSecret",
|
||||
"configFile": "openclaw.json",
|
||||
|
||||
@@ -54,7 +54,7 @@ for bounded runtime excerpts and injected runtime-owned blocks. They are
|
||||
separate from bootstrap limits, startup-context limits, and skills prompt
|
||||
limits.
|
||||
|
||||
`toolResultMaxChars` is an advanced ceiling (up to `1000000` characters). When it is unset, OpenClaw chooses
|
||||
`toolResultMaxChars` is an advanced ceiling. When it is unset, OpenClaw chooses
|
||||
the live tool-result cap from the effective model context window: `16000` chars
|
||||
below 100K tokens, `32000` chars at 100K+ tokens, and `64000` chars at 200K+
|
||||
tokens, still bounded by the runtime context-share guard.
|
||||
|
||||
@@ -198,7 +198,7 @@ plugins.
|
||||
| `/think <level\|default>` | Set the thinking level or clear the session override. Aliases: `/thinking`, `/t` |
|
||||
| `/verbose on\|off\|full` | Toggle verbose output. Alias: `/v` |
|
||||
| `/trace on\|off` | Toggle plugin trace output for the current session |
|
||||
| `/fast [status\|auto\|on\|off\|default]` | Show, set, or clear fast mode |
|
||||
| `/fast [status\|on\|off\|default]` | Show, set, or clear fast mode |
|
||||
| `/reasoning [on\|off\|stream]` | Toggle reasoning visibility. Alias: `/reason` |
|
||||
| `/elevated [on\|off\|ask\|full]` | Toggle elevated mode. Alias: `/elev` |
|
||||
| `/exec host=<auto\|sandbox\|gateway\|node> security=<deny\|allowlist\|full> ask=<off\|on-miss\|always> node=<id>` | Show or set exec defaults |
|
||||
@@ -211,7 +211,7 @@ plugins.
|
||||
<Accordion title="verbose / trace / fast / reasoning safety">
|
||||
- `/verbose` is for debugging — keep it **off** in normal use.
|
||||
- `/trace` reveals only plugin-owned trace/debug lines; normal verbose chatter stays off.
|
||||
- `/fast auto|on|off` persists a session override; use the Sessions UI `inherit` option to clear it.
|
||||
- `/fast on|off` persists a session override; use the Sessions UI `inherit` option to clear it.
|
||||
- `/fast` is provider-specific: OpenAI/Codex map it to `service_tier=priority`; direct Anthropic requests map it to `service_tier=auto` or `standard_only`.
|
||||
- `/reasoning`, `/verbose`, and `/trace` are risky in group settings — they may reveal internal reasoning or plugin diagnostics. Keep them off in group chats.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ title: "Thinking levels"
|
||||
- Stale configured OpenRouter Hunter Alpha refs skip proxy reasoning injection because that retired route could return final answer text through reasoning fields.
|
||||
- Google Gemini maps `/think adaptive` to Gemini's provider-owned dynamic thinking. Gemini 3 requests omit a fixed `thinkingLevel`, while Gemini 2.5 requests send `thinkingBudget: -1`; fixed levels still map to the closest Gemini `thinkingLevel` or budget for that model family.
|
||||
- MiniMax M2.x (`minimax/MiniMax-M2*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from M2.x's non-native Anthropic stream format. MiniMax-M3 (and M3.x) is exempt: M3 emits proper Anthropic thinking blocks and returns empty content when thinking is disabled, so OpenClaw keeps M3 on the provider's omitted/adaptive thinking path.
|
||||
- Z.AI (`zai/*`) is binary (`on`/`off`) for most GLM models. GLM-5.2 is the exception: it exposes `/think off|low|high|max`, maps `low` and `high` to Z.AI `reasoning_effort: "high"`, and maps `max` to `reasoning_effort: "max"`.
|
||||
- Z.AI (`zai/*`) only supports binary thinking (`on`/`off`). Any non-`off` level is treated as `on` (mapped to `low`).
|
||||
- Moonshot Kimi K2.7 Code (`moonshot/kimi-k2.7-code`) always thinks. Its profile exposes only `on`, and OpenClaw omits the outbound `thinking` field as required by Moonshot. Other `moonshot/*` models map `/think off` to `thinking: { type: "disabled" }` and any non-`off` level to `thinking: { type: "enabled" }`. When thinking is enabled, Moonshot only accepts `tool_choice` `auto|none`; OpenClaw normalizes incompatible values to `auto`.
|
||||
|
||||
## Resolution order
|
||||
@@ -60,22 +60,21 @@ title: "Thinking levels"
|
||||
|
||||
## Fast mode (/fast)
|
||||
|
||||
- Levels: `auto|on|off|default`.
|
||||
- Directive-only message toggles a session fast-mode override and replies `Fast mode set to auto.`, `Fast mode enabled.`, or `Fast mode disabled.`. Use `/fast default` to clear the session override and inherit the configured default; aliases include `inherit`, `clear`, `reset`, and `unpin`.
|
||||
- Levels: `on|off|default`.
|
||||
- Directive-only message toggles a session fast-mode override and replies `Fast mode enabled.` / `Fast mode disabled.`. Use `/fast default` to clear the session override and inherit the configured default; aliases include `inherit`, `clear`, `reset`, and `unpin`.
|
||||
- Send `/fast` (or `/fast status`) with no mode to see the current effective fast-mode state.
|
||||
- OpenClaw resolves fast mode in this order:
|
||||
1. Inline/directive-only `/fast auto|on|off` override (`/fast default` clears this layer)
|
||||
1. Inline/directive-only `/fast on|off` override (`/fast default` clears this layer)
|
||||
2. Session override
|
||||
3. Per-agent default (`agents.list[].fastModeDefault`)
|
||||
4. Per-model config: `agents.defaults.models["<provider>/<model>"].params.fastMode`
|
||||
5. Fallback: `off`
|
||||
- `auto` keeps the session/config mode as auto but resolves each new model call independently. Calls that start before the auto cutoff have fast mode enabled; later retry, fallback, tool-result, or continuation calls start with fast mode disabled. The cutoff defaults to 60 seconds; set `agents.defaults.models["<provider>/<model>"].params.fastAutoOnSeconds` on the active model to change it.
|
||||
- For `openai/*`, fast mode maps to OpenAI priority processing by sending `service_tier=priority` on supported Responses requests.
|
||||
- For Codex-backed `openai/*` / `openai-codex/*` models, fast mode sends the same `service_tier=priority` flag on Codex Responses. Native Codex app-server turns receive the tier only on `turn/start` or thread start/resume, so `auto` cannot retier one already-running app-server turn; it applies to the next model turn OpenClaw starts.
|
||||
- For Codex-backed `openai/*` models, fast mode sends the same `service_tier=priority` flag on Codex Responses. OpenClaw keeps one shared `/fast` toggle across both auth paths.
|
||||
- For direct public `anthropic/*` requests, including OAuth-authenticated traffic sent to `api.anthropic.com`, fast mode maps to Anthropic service tiers: `/fast on` sets `service_tier=auto`, `/fast off` sets `service_tier=standard_only`.
|
||||
- For `minimax/*` on the Anthropic-compatible path, `/fast on` (or `params.fastMode: true`) rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`.
|
||||
- Explicit Anthropic `serviceTier` / `service_tier` model params override the fast-mode default when both are set. OpenClaw still skips Anthropic service-tier injection for non-Anthropic proxy base URLs.
|
||||
- `/status` shows `Fast` when fast mode is enabled and `Fast:auto` when the configured mode is auto.
|
||||
- `/status` shows `Fast` only when fast mode is enabled.
|
||||
|
||||
## Verbose directives (/verbose or /v)
|
||||
|
||||
|
||||
4
extensions/acpx/npm-shrinkwrap.json
generated
4
extensions/acpx/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/claude-agent-acp": "0.39.0",
|
||||
"@zed-industries/codex-acp": "0.15.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"description": "OpenClaw ACP runtime backend with plugin-owned session and transport management.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,10 +26,10 @@
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.9"
|
||||
"pluginApi": ">=2026.6.8"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.9",
|
||||
"openclawVersion": "2026.6.8",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./src/runtime-internals/mcp-proxy.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/admin-http-rpc",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"private": true,
|
||||
"description": "OpenClaw admin HTTP RPC endpoint",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/alibaba-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"private": true,
|
||||
"description": "OpenClaw Alibaba Model Studio video provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.100.1",
|
||||
"@aws/bedrock-token-generator": "1.1.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"description": "OpenClaw Amazon Bedrock Mantle provider plugin for OpenAI-compatible model routing.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,10 +24,10 @@
|
||||
"minHostVersion": ">=2026.5.12-beta.1"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.9"
|
||||
"pluginApi": ">=2026.6.8"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.9",
|
||||
"openclawVersion": "2026.6.8",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
4
extensions/amazon-bedrock/npm-shrinkwrap.json
generated
4
extensions/amazon-bedrock/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock": "3.1056.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1056.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"description": "OpenClaw Amazon Bedrock provider plugin with model discovery, embeddings, and guardrail support.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,10 +28,10 @@
|
||||
"minHostVersion": ">=2026.5.12-beta.1"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.9"
|
||||
"pluginApi": ">=2026.6.8"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.9",
|
||||
"openclawVersion": "2026.6.8",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
4
extensions/anthropic-vertex/npm-shrinkwrap.json
generated
4
extensions/anthropic-vertex/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/vertex-sdk": "0.16.1"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"description": "OpenClaw Anthropic Vertex provider plugin for Claude models on Google Vertex AI.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -23,10 +23,10 @@
|
||||
"minHostVersion": ">=2026.5.12-beta.1"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.9"
|
||||
"pluginApi": ">=2026.6.8"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.9",
|
||||
"openclawVersion": "2026.6.8",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
@@ -49,7 +49,7 @@ export function buildAnthropicCliBackend(): CliBackendPlugin {
|
||||
"--allowedTools",
|
||||
"mcp__openclaw__*",
|
||||
"--disallowedTools",
|
||||
"ScheduleWakeup,CronCreate,Bash(run_in_background:true),Monitor",
|
||||
"ScheduleWakeup,CronCreate",
|
||||
],
|
||||
resumeArgs: [
|
||||
"-p",
|
||||
@@ -62,7 +62,7 @@ export function buildAnthropicCliBackend(): CliBackendPlugin {
|
||||
"--allowedTools",
|
||||
"mcp__openclaw__*",
|
||||
"--disallowedTools",
|
||||
"ScheduleWakeup,CronCreate,Bash(run_in_background:true),Monitor",
|
||||
"ScheduleWakeup,CronCreate",
|
||||
"--resume",
|
||||
"{sessionId}",
|
||||
],
|
||||
|
||||
@@ -10,15 +10,6 @@ import {
|
||||
resolveClaudeCliExecutionArgs,
|
||||
} from "./cli-shared.js";
|
||||
|
||||
const CLAUDE_CLI_DISALLOWED_TOOLS =
|
||||
"ScheduleWakeup,CronCreate,Bash(run_in_background:true),Monitor";
|
||||
|
||||
function expectDefaultDisallowedTools(args: readonly string[] | undefined) {
|
||||
const disallowedIndex = args?.indexOf("--disallowedTools") ?? -1;
|
||||
expect(disallowedIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(args?.[disallowedIndex + 1]).toBe(CLAUDE_CLI_DISALLOWED_TOOLS);
|
||||
}
|
||||
|
||||
describe("normalizeClaudePermissionArgs", () => {
|
||||
it("leaves args alone when they omit permission flags", () => {
|
||||
expect(
|
||||
@@ -365,10 +356,8 @@ describe("normalizeClaudeBackendConfig", () => {
|
||||
expect(backend.config.input).toBe("stdin");
|
||||
expect(backend.config.args).toContain("--setting-sources");
|
||||
expect(backend.config.args).toContain("user");
|
||||
expectDefaultDisallowedTools(backend.config.args);
|
||||
expect(backend.config.resumeArgs).toContain("--setting-sources");
|
||||
expect(backend.config.resumeArgs).toContain("user");
|
||||
expectDefaultDisallowedTools(backend.config.resumeArgs);
|
||||
expect(backend.config.clearEnv).toEqual([...CLAUDE_CLI_CLEAR_ENV]);
|
||||
expect(backend.config.clearEnv).toContain("ANTHROPIC_API_TOKEN");
|
||||
expect(backend.config.clearEnv).toContain("ANTHROPIC_BASE_URL");
|
||||
@@ -385,11 +374,4 @@ describe("normalizeClaudeBackendConfig", () => {
|
||||
expect(backend.config.clearEnv).toContain("OTEL_EXPORTER_OTLP_PROTOCOL");
|
||||
expect(backend.config.clearEnv).toContain("OTEL_SDK_DISABLED");
|
||||
});
|
||||
|
||||
it("disables native background Bash and Monitor tools in args and resumeArgs", () => {
|
||||
const backend = buildAnthropicCliBackend();
|
||||
|
||||
expectDefaultDisallowedTools(backend.config.args);
|
||||
expectDefaultDisallowedTools(backend.config.resumeArgs);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
createAnthropicServiceTierWrapper,
|
||||
createAnthropicThinkingPrefillWrapper,
|
||||
resolveAnthropicBetas,
|
||||
resolveAnthropicFastMode,
|
||||
wrapAnthropicProviderStream,
|
||||
} from "./stream-wrappers.js";
|
||||
|
||||
@@ -173,10 +172,6 @@ describe("anthropic stream wrappers", () => {
|
||||
expect(captured.headers?.["anthropic-beta"]).toContain(OAUTH_BETA);
|
||||
expect(captured.headers?.["anthropic-beta"]).not.toContain(CONTEXT_1M_BETA);
|
||||
});
|
||||
|
||||
it("ignores unresolved auto fast mode at the provider boundary", () => {
|
||||
expect(resolveAnthropicFastMode({ fastMode: "auto" })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAnthropicThinkingPrefillWrapper", () => {
|
||||
@@ -287,19 +282,6 @@ describe("Anthropic service_tier payload wrappers", () => {
|
||||
expect(payload?.service_tier).toBe("standard_only");
|
||||
});
|
||||
|
||||
it("fast mode resolves dynamic service_tier for each stream call", () => {
|
||||
let enabled = true;
|
||||
const first = runPayloadWrapper({ apiKey: "sk-ant-api03-test-key" }, (base) =>
|
||||
createAnthropicFastModeWrapper(base, () => enabled),
|
||||
);
|
||||
enabled = false;
|
||||
const second = runPayloadWrapper({ apiKey: "sk-ant-api03-test-key" }, (base) =>
|
||||
createAnthropicFastModeWrapper(base, () => enabled),
|
||||
);
|
||||
expect(first?.service_tier).toBe("auto");
|
||||
expect(second?.service_tier).toBe("standard_only");
|
||||
});
|
||||
|
||||
it("explicit service tier injects service_tier=standard_only for regular API keys", () => {
|
||||
const payload = serviceTierWrapperCases[1].run({
|
||||
apiKey: "sk-ant-api03-test-key",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
composeProviderStreamWrappers,
|
||||
createAnthropicThinkingPrefillPayloadWrapper,
|
||||
resolveAnthropicPayloadPolicy,
|
||||
stripTrailingAnthropicAssistantPrefillWhenThinking,
|
||||
streamWithPayloadPatch,
|
||||
} from "openclaw/plugin-sdk/provider-stream-shared";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
@@ -43,7 +44,6 @@ const OPENCLAW_OAUTH_ANTHROPIC_BETAS = [
|
||||
] as const;
|
||||
|
||||
type AnthropicServiceTier = "auto" | "standard_only";
|
||||
type DynamicFastMode = boolean | (() => boolean | undefined);
|
||||
|
||||
function isAnthropic1MModel(modelId: string): boolean {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(modelId);
|
||||
@@ -157,20 +157,9 @@ export function createAnthropicBetaHeadersWrapper(
|
||||
/** Wrap a stream function with the Anthropic fast-mode service tier. */
|
||||
export function createAnthropicFastModeWrapper(
|
||||
baseStreamFn: StreamFn | undefined,
|
||||
enabled: DynamicFastMode,
|
||||
enabled: boolean,
|
||||
): StreamFn {
|
||||
const underlying = baseStreamFn ?? streamSimple;
|
||||
return (model, context, options) => {
|
||||
const resolved = typeof enabled === "function" ? enabled() : enabled;
|
||||
if (resolved === undefined) {
|
||||
return underlying(model, context, options);
|
||||
}
|
||||
return createAnthropicServiceTierWrapper(underlying, resolveAnthropicFastServiceTier(resolved))(
|
||||
model,
|
||||
context,
|
||||
options,
|
||||
);
|
||||
};
|
||||
return createAnthropicServiceTierWrapper(baseStreamFn, resolveAnthropicFastServiceTier(enabled));
|
||||
}
|
||||
|
||||
/** Wrap a stream function with an explicit Anthropic service tier when allowed. */
|
||||
@@ -215,12 +204,9 @@ export function createAnthropicThinkingPrefillWrapper(
|
||||
export function resolveAnthropicFastMode(
|
||||
extraParams: Record<string, unknown> | undefined,
|
||||
): boolean | undefined {
|
||||
const raw = extraParams?.fastMode ?? extraParams?.fast_mode;
|
||||
const fastMode =
|
||||
typeof raw === "function"
|
||||
? normalizeFastMode((raw as () => unknown)() as string | boolean | null | undefined)
|
||||
: normalizeFastMode(raw as string | boolean | null | undefined);
|
||||
return fastMode === "auto" ? undefined : fastMode;
|
||||
return normalizeFastMode(
|
||||
(extraParams?.fastMode ?? extraParams?.fast_mode) as string | boolean | null | undefined,
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve Anthropic service tier from model extra params. */
|
||||
@@ -246,9 +232,7 @@ export function wrapAnthropicProviderStream(
|
||||
hasConfiguredAnthropicBeta(ctx.extraParams) ||
|
||||
(ctx.extraParams?.context1m === true && isAnthropic1MModel(ctx.modelId));
|
||||
const serviceTier = resolveAnthropicServiceTier(ctx.extraParams);
|
||||
const hasFastModeParam =
|
||||
ctx.extraParams !== undefined &&
|
||||
(Object.hasOwn(ctx.extraParams, "fastMode") || Object.hasOwn(ctx.extraParams, "fast_mode"));
|
||||
const fastMode = resolveAnthropicFastMode(ctx.extraParams);
|
||||
return composeProviderStreamWrappers(
|
||||
ctx.streamFn,
|
||||
needsAnthropicBetaWrapper
|
||||
@@ -257,9 +241,8 @@ export function wrapAnthropicProviderStream(
|
||||
serviceTier
|
||||
? (streamFn) => createAnthropicServiceTierWrapper(streamFn, serviceTier)
|
||||
: undefined,
|
||||
hasFastModeParam
|
||||
? (streamFn) =>
|
||||
createAnthropicFastModeWrapper(streamFn, () => resolveAnthropicFastMode(ctx.extraParams))
|
||||
fastMode !== undefined
|
||||
? (streamFn) => createAnthropicFastModeWrapper(streamFn, fastMode)
|
||||
: undefined,
|
||||
(streamFn) => createAnthropicThinkingPrefillWrapper(streamFn),
|
||||
);
|
||||
@@ -268,5 +251,6 @@ export function wrapAnthropicProviderStream(
|
||||
/** Test-only hooks for Anthropic stream wrapper behavior. */
|
||||
export const testing = {
|
||||
log,
|
||||
stripTrailingAssistantPrefillWhenThinking: stripTrailingAnthropicAssistantPrefillWhenThinking,
|
||||
};
|
||||
export { testing as __testing };
|
||||
|
||||
4
extensions/arcee/npm-shrinkwrap.json
generated
4
extensions/arcee/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/arcee-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/arcee-provider",
|
||||
"version": "2026.6.9"
|
||||
"version": "2026.6.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/arcee-provider",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"description": "OpenClaw Arcee provider plugin.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -21,10 +21,10 @@
|
||||
"minHostVersion": ">=2026.6.8"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.9"
|
||||
"pluginApi": ">=2026.6.8"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.9",
|
||||
"openclawVersion": "2026.6.8",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/azure-speech",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"private": true,
|
||||
"description": "OpenClaw Azure Speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/bonjour",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"description": "OpenClaw Bonjour/mDNS gateway discovery",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
4
extensions/brave/npm-shrinkwrap.json
generated
4
extensions/brave/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.6.9"
|
||||
"version": "2026.6.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"description": "OpenClaw Brave Search provider plugin for web search.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -21,10 +21,10 @@
|
||||
"allowInvalidConfigRecovery": true
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.9"
|
||||
"pluginApi": ">=2026.6.8"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.9"
|
||||
"openclawVersion": "2026.6.8"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/browser-plugin",
|
||||
"version": "2026.6.9",
|
||||
"version": "2026.6.8",
|
||||
"private": true,
|
||||
"description": "OpenClaw browser tool plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -9,23 +9,6 @@ const { registerManagedProxyBrowserCdpBypassMock } = vi.hoisted(() => ({
|
||||
),
|
||||
}));
|
||||
|
||||
function createDeferred<T = void>(): {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
} {
|
||||
let resolve: ((value: T | PromiseLike<T>) => void) | undefined;
|
||||
let reject: ((reason?: unknown) => void) | undefined;
|
||||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise;
|
||||
reject = rejectPromise;
|
||||
});
|
||||
if (!resolve || !reject) {
|
||||
throw new Error("Expected deferred callbacks to be initialized");
|
||||
}
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/ssrf-runtime-internal", () => ({
|
||||
registerManagedProxyBrowserCdpBypass: registerManagedProxyBrowserCdpBypassMock,
|
||||
}));
|
||||
@@ -46,6 +29,19 @@ beforeEach(() => {
|
||||
registerManagedProxyBrowserCdpBypassMock.mockImplementation(() => undefined);
|
||||
});
|
||||
|
||||
function createDeferred<T = void>() {
|
||||
let resolve: ((value: T | PromiseLike<T>) => void) | undefined;
|
||||
let reject: ((reason?: unknown) => void) | undefined;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
if (!resolve || !reject) {
|
||||
throw new Error("Expected deferred callbacks to be initialized");
|
||||
}
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
async function withIsolatedNoProxyEnv(fn: () => Promise<void>) {
|
||||
const origNoProxy = process.env.NO_PROXY;
|
||||
const origNoProxyLower = process.env.no_proxy;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user