Compare commits
192 Commits
codex/mark
...
codex/comm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85e6e3edbb | ||
|
|
1e3b2b24fb | ||
|
|
b0e8418862 | ||
|
|
e842363667 | ||
|
|
b72baa7727 | ||
|
|
74990d4fed | ||
|
|
684e827346 | ||
|
|
fc9e3b7b77 | ||
|
|
d1c653406e | ||
|
|
e24035d9a4 | ||
|
|
58f60ea247 | ||
|
|
1ac45d0889 | ||
|
|
0b0ae3117d | ||
|
|
decc930303 | ||
|
|
60a5e8e720 | ||
|
|
4f3f7150fa | ||
|
|
a5b2b42721 | ||
|
|
025da47025 | ||
|
|
4e5366aa91 | ||
|
|
7b8a805ac6 | ||
|
|
1abf74258e | ||
|
|
6b5ab53b71 | ||
|
|
816ac0ee1e | ||
|
|
e20e9adda2 | ||
|
|
4b1ce400ee | ||
|
|
5ff221ba30 | ||
|
|
c26b660403 | ||
|
|
d6dfa06981 | ||
|
|
8d19ee6e0d | ||
|
|
cd2e5bc6cf | ||
|
|
2aba860546 | ||
|
|
fd114b9998 | ||
|
|
a4c74e2ec1 | ||
|
|
c13ed2868a | ||
|
|
9d88eddae8 | ||
|
|
681491f6fe | ||
|
|
0617de9dda | ||
|
|
f1b1a12fbc | ||
|
|
48a9b32494 | ||
|
|
be16e6f7ac | ||
|
|
8d5a385a21 | ||
|
|
2057cb4c1c | ||
|
|
fbcc290459 | ||
|
|
ac7fa63036 | ||
|
|
cce72ebf27 | ||
|
|
42f6d93ed5 | ||
|
|
31be6bc311 | ||
|
|
65a190530c | ||
|
|
7c4fb1bd2c | ||
|
|
7d5d62511f | ||
|
|
cc6a6f5682 | ||
|
|
7a8d307bdc | ||
|
|
b7d363cadf | ||
|
|
68b4dd1816 | ||
|
|
0e16e72091 | ||
|
|
9ead0ae921 | ||
|
|
3128ec9858 | ||
|
|
1ec291c682 | ||
|
|
9d9a6140a3 | ||
|
|
674bd6fc93 | ||
|
|
b2a55a282a | ||
|
|
3cf4c1ad69 | ||
|
|
fa9ce6ea0e | ||
|
|
0f1f1a1fd7 | ||
|
|
d944aaa9ec | ||
|
|
baade28397 | ||
|
|
883c0f1254 | ||
|
|
793ab78ebb | ||
|
|
57ea5aff81 | ||
|
|
f1d65b3cd6 | ||
|
|
e6b951a6a6 | ||
|
|
55e9194a4c | ||
|
|
8929838159 | ||
|
|
a355c8897d | ||
|
|
b06dc17537 | ||
|
|
7967a3582c | ||
|
|
2e6016fdec | ||
|
|
8a1a8ea8a3 | ||
|
|
4608f7dcf9 | ||
|
|
49ac93bda6 | ||
|
|
f6653b9b35 | ||
|
|
2f92fddef0 | ||
|
|
489efc8f5e | ||
|
|
459abfc26b | ||
|
|
340cc2c1e4 | ||
|
|
be8cb5d4ea | ||
|
|
222ade9fa6 | ||
|
|
6667b9734a | ||
|
|
ebbb2e8f01 | ||
|
|
dea3e835c5 | ||
|
|
722af385d2 | ||
|
|
dacd18a8aa | ||
|
|
8a9acd2940 | ||
|
|
bd8353dbaa | ||
|
|
3baf78dd0a | ||
|
|
1ed7692d2f | ||
|
|
12798eb789 | ||
|
|
02192bd27f | ||
|
|
086274fd7e | ||
|
|
ed07a7a2de | ||
|
|
829fb5dcb3 | ||
|
|
4c6285e8ff | ||
|
|
7c52969d49 | ||
|
|
42d3acfc99 | ||
|
|
32f98d7fe8 | ||
|
|
4bd7421182 | ||
|
|
d91d8ff060 | ||
|
|
af44fb9b6c | ||
|
|
45e0545e82 | ||
|
|
2d17cb295d | ||
|
|
e8120a72e1 | ||
|
|
0904f3e553 | ||
|
|
2770aa5f4c | ||
|
|
285401ced8 | ||
|
|
64697fbe24 | ||
|
|
e9aae26b22 | ||
|
|
cb12a9af94 | ||
|
|
65d7fa2420 | ||
|
|
bd4a7f4119 | ||
|
|
14f61d0637 | ||
|
|
0f3a63b12e | ||
|
|
a14eacf372 | ||
|
|
646df2da83 | ||
|
|
211321ce5c | ||
|
|
a34e822cd4 | ||
|
|
8c180c9153 | ||
|
|
990f0baff9 | ||
|
|
bd8baeb323 | ||
|
|
0771bbbd20 | ||
|
|
74cf5c7e7d | ||
|
|
0cfd6b0504 | ||
|
|
4e45010203 | ||
|
|
afdf9aaea0 | ||
|
|
72ed2121f8 | ||
|
|
2405bbcbaf | ||
|
|
403190572b | ||
|
|
67983a00c8 | ||
|
|
61aa499b53 | ||
|
|
420450b5cb | ||
|
|
f8491b0fcf | ||
|
|
98e943ebdd | ||
|
|
f8d5f162a1 | ||
|
|
a2fdd5bc70 | ||
|
|
2af2111ae0 | ||
|
|
c9d35c7172 | ||
|
|
50b69e16dc | ||
|
|
fe97c6000c | ||
|
|
a99cbf29bd | ||
|
|
05ea36a81f | ||
|
|
eb58c88598 | ||
|
|
5a67c5c556 | ||
|
|
5a55135146 | ||
|
|
193988bc5b | ||
|
|
a20f57bf2e | ||
|
|
66f797b22c | ||
|
|
65a805ac28 | ||
|
|
b18bab0bcc | ||
|
|
9ac30b587e | ||
|
|
82de264710 | ||
|
|
7f7f0775ed | ||
|
|
30819ed3da | ||
|
|
1c3095e029 | ||
|
|
62cfc613f1 | ||
|
|
64a946ac21 | ||
|
|
96187089d4 | ||
|
|
965e680603 | ||
|
|
1cf39a2d6f | ||
|
|
92b3d52e8a | ||
|
|
8ba6dfeaf6 | ||
|
|
bddcf4448c | ||
|
|
c8a67768e3 | ||
|
|
26e61b2087 | ||
|
|
ee48028028 | ||
|
|
3c324590ae | ||
|
|
ba88b7a178 | ||
|
|
d767e296e2 | ||
|
|
83cd3cbe2a | ||
|
|
16807824cc | ||
|
|
e3d24faecd | ||
|
|
469bec97ef | ||
|
|
101db565ca | ||
|
|
ef26e8dfce | ||
|
|
25c19e013a | ||
|
|
f2eea90dac | ||
|
|
3113fe95ea | ||
|
|
4e1f8b8ac7 | ||
|
|
0b8f6b81e6 | ||
|
|
ab1042d115 | ||
|
|
9153aab037 | ||
|
|
285a792aa8 | ||
|
|
a8bc1716dd | ||
|
|
373ef81e83 |
@@ -16,6 +16,10 @@ Use this with `$release-openclaw-maintainer` and `$openclaw-testing` when a rele
|
||||
- Watch one parent run plus compact child summaries. Avoid broad `gh run view` polling loops; REST quota is easy to burn.
|
||||
- Fetch logs only for failed or currently-blocking jobs. If quota is low, stop polling and wait for reset.
|
||||
- Treat live-provider flakes separately from code failures: prove key validity, provider HTTP status, retry evidence, and exact failing lane before editing code.
|
||||
- Full Release Validation parent monitors fail fast: once a required child job
|
||||
fails, the parent cancels the remaining child matrix and prints the failed
|
||||
job summary. Inspect that first red job instead of waiting for unrelated
|
||||
matrix tails.
|
||||
|
||||
## Preflight
|
||||
|
||||
@@ -73,6 +77,9 @@ gh workflow run full-release-validation.yml \
|
||||
```
|
||||
|
||||
Use `release_profile=stable` unless the operator explicitly asks for the broad advisory provider/media matrix. Use narrow `rerun_group` after focused fixes.
|
||||
Publish with `openclaw-release-publish.yml` using `release_profile=from-validation`
|
||||
unless a maintainer intentionally wants to cross-check a specific profile; the
|
||||
publish workflow reads the effective profile from the full-validation manifest.
|
||||
|
||||
## Watch
|
||||
|
||||
|
||||
@@ -49,17 +49,21 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
|
||||
the next beta number until the matching npm package has actually published.
|
||||
If a published beta needs a fix, commit the fix on the release branch and
|
||||
increment to the next `-beta.N`.
|
||||
- For a beta release train, run the fast local preflight first, publish the
|
||||
beta to npm `beta`, then run the expensive published-package roster focused
|
||||
on install/update/Docker/Parallels/NPM Telegram. If anything fails, fix it on
|
||||
the release branch, commit/push/pull, increment beta number, and repeat. Run
|
||||
the full expensive roster at least once before stable/latest promotion; for
|
||||
later beta attempts, rerun only lanes whose evidence changed unless the fix
|
||||
touches broad release, install/update, plugin, Docker, Parallels, or live QA
|
||||
behavior. After each beta is published, scan current `main` once for critical
|
||||
fixes that landed after the release branch cut and backport only important
|
||||
low-risk fixes. Operators may authorize up to 4 autonomous beta attempts;
|
||||
after 4 failed beta attempts, stop and report.
|
||||
- For a beta release train, keep Full Release Validation as a pre-publish gate
|
||||
unless the operator explicitly waives it. Run the fast local preflight, npm
|
||||
preflight, full release validation, and performance in parallel where safe.
|
||||
If anything fails before npm publish, fix it on the release branch,
|
||||
forward-port the fix to `main`, move the unpublished beta tag/prerelease to
|
||||
the fixed commit, and rerun the affected pre-publish gates. If anything fails
|
||||
after npm publish, fix it, forward-port to `main`, increment beta number, and
|
||||
repeat. After each beta publish, run the published-package roster focused on
|
||||
install/update/Docker/Parallels/NPM Telegram. For later beta attempts, rerun
|
||||
only lanes whose evidence changed unless the fix touches broad release,
|
||||
install/update, plugin, Docker, Parallels, or live QA behavior. After each
|
||||
beta is live, scan current `main` once for critical fixes that landed after
|
||||
the release branch cut and backport only important low-risk fixes. Operators
|
||||
may authorize up to 4 autonomous beta attempts; after 4 failed beta attempts,
|
||||
stop and report.
|
||||
- As soon as the release candidate SHA exists, dispatch `OpenClaw Performance`
|
||||
with `target_ref=<release-sha>` in parallel with the other release work. Do
|
||||
not wait for full release validation to start the performance signal.
|
||||
@@ -468,8 +472,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
- The npm workflow and the private mac publish workflow accept
|
||||
`preflight_only=true` to run validation/build/package steps without uploading
|
||||
public release assets.
|
||||
- Real npm publish requires a prior successful npm preflight run id so the
|
||||
publish job promotes the prepared tarball instead of rebuilding it.
|
||||
- Real npm publish requires a prior successful npm preflight run id and the
|
||||
successful Full Release Validation run id for the same tag/SHA so the publish
|
||||
job promotes the prepared tarball instead of rebuilding it and attaches the
|
||||
correct release evidence.
|
||||
- Real private mac publish requires a prior successful private mac preflight
|
||||
run id so the publish job promotes the prepared artifacts instead of
|
||||
rebuilding or renotarizing them again.
|
||||
@@ -499,11 +505,12 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
instead of uploading public GitHub release assets.
|
||||
- Private smoke-test runs upload ad-hoc, non-notarized build artifacts as
|
||||
workflow artifacts and intentionally skip stable `appcast.xml` generation.
|
||||
- For stable releases, npm preflight, public mac validation, private mac
|
||||
validation, and private mac preflight must all pass before any real publish
|
||||
run starts. For beta releases, npm preflight plus the selected Docker,
|
||||
install/update, Parallels, and release-check lanes are sufficient unless mac
|
||||
beta validation was explicitly requested.
|
||||
- For stable releases, npm preflight, Full Release Validation, public mac
|
||||
validation, private mac validation, and private mac preflight must all pass
|
||||
before any real publish run starts. For beta releases, npm preflight and Full
|
||||
Release Validation must pass before npm publish unless the operator explicitly
|
||||
waives the full gate; mac beta validation is still only required when
|
||||
requested.
|
||||
- Real publish runs may be dispatched from `main` or from a
|
||||
`release/YYYY.M.D` branch. For release-branch runs, the tag must be contained
|
||||
in that release branch, and the real publish must reuse a successful preflight
|
||||
|
||||
35
.github/workflows/ci.yml
vendored
@@ -605,7 +605,19 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-all-v3-
|
||||
|
||||
- name: Restore dist build cache
|
||||
id: dist_build_cache
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: |
|
||||
dist/
|
||||
dist-runtime/
|
||||
extensions/*/src/host/**/.bundle.hash
|
||||
extensions/*/src/host/**/*.bundle.js
|
||||
key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_revision }}
|
||||
|
||||
- name: Build dist
|
||||
if: steps.dist_build_cache.outputs.cache-hit != 'true'
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build:ci-artifacts
|
||||
@@ -614,14 +626,6 @@ jobs:
|
||||
if: needs.preflight.outputs.run_control_ui_i18n == 'true'
|
||||
run: pnpm ui:i18n:check
|
||||
|
||||
- name: Cache dist build
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
dist/
|
||||
dist-runtime/
|
||||
key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_revision }}
|
||||
|
||||
- name: Pack built runtime artifacts
|
||||
run: tar --posix -cf dist-runtime-build.tar.zst --use-compress-program zstdmt dist dist-runtime
|
||||
|
||||
@@ -751,6 +755,18 @@ jobs:
|
||||
done
|
||||
exit "$failures"
|
||||
|
||||
- name: Save dist build cache
|
||||
if: steps.dist_build_cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v5
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: |
|
||||
dist/
|
||||
dist-runtime/
|
||||
extensions/*/src/host/**/.bundle.hash
|
||||
extensions/*/src/host/**/*.bundle.js
|
||||
key: ${{ steps.dist_build_cache.outputs.cache-primary-key }}
|
||||
|
||||
- name: Upload gateway watch regression artifacts
|
||||
if: always() && needs.preflight.outputs.run_check_additional == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -1151,7 +1167,8 @@ jobs:
|
||||
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
|
||||
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
|
||||
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
|
||||
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "900000"
|
||||
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000"
|
||||
OPENCLAW_VITEST_NO_OUTPUT_RETRY: "1"
|
||||
OPENCLAW_TEST_PROJECTS_PARALLEL: "2"
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
6
.github/workflows/crabbox-hydrate.yml
vendored
@@ -32,11 +32,11 @@ permissions:
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
PNPM_CONFIG_CHILD_CONCURRENCY: "1"
|
||||
PNPM_CONFIG_MODULES_DIR: "/tmp/openclaw-pnpm-node-modules"
|
||||
PNPM_CONFIG_MODULES_DIR: "/var/tmp/openclaw-pnpm-node-modules"
|
||||
PNPM_CONFIG_NETWORK_CONCURRENCY: "1"
|
||||
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
|
||||
PNPM_CONFIG_STORE_DIR: "/var/tmp/openclaw-pnpm-store"
|
||||
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
|
||||
PNPM_CONFIG_VIRTUAL_STORE_DIR: "/tmp/openclaw-pnpm-virtual-store"
|
||||
PNPM_CONFIG_VIRTUAL_STORE_DIR: "/var/tmp/openclaw-pnpm-virtual-store"
|
||||
|
||||
jobs:
|
||||
hydrate:
|
||||
|
||||
122
.github/workflows/full-release-validation.yml
vendored
@@ -229,7 +229,7 @@ jobs:
|
||||
needs: [resolve_target]
|
||||
if: inputs.rerun_group == 'all'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
@@ -245,54 +245,11 @@ jobs:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
timeout --kill-after=30s 35m docker build \
|
||||
timeout --kill-after=30s 15m docker build \
|
||||
--target runtime-assets \
|
||||
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
|
||||
.
|
||||
|
||||
- name: Build and smoke test final Docker runtime image
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image_ref="openclaw-release-runtime-smoke:${TARGET_SHA}"
|
||||
timeout --kill-after=30s 35m docker build \
|
||||
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
|
||||
-t "${image_ref}" \
|
||||
.
|
||||
docker run --rm --entrypoint /bin/sh "${image_ref}" -lc '
|
||||
set -eu
|
||||
test -f /app/src/agents/templates/HEARTBEAT.md
|
||||
temp_root="$(mktemp -d)"
|
||||
trap "rm -rf \"${temp_root}\"" EXIT
|
||||
mkdir -p "${temp_root}/home" "${temp_root}/cwd"
|
||||
cd "${temp_root}/cwd"
|
||||
set +e
|
||||
HOME="${temp_root}/home" \
|
||||
USERPROFILE="${temp_root}/home" \
|
||||
OPENCLAW_HOME="${temp_root}/home" \
|
||||
OPENCLAW_NO_ONBOARD=1 \
|
||||
OPENCLAW_SUPPRESS_NOTES=1 \
|
||||
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 \
|
||||
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK=1 \
|
||||
AWS_EC2_METADATA_DISABLED=true \
|
||||
AWS_SHARED_CREDENTIALS_FILE="${temp_root}/home/.aws/credentials" \
|
||||
AWS_CONFIG_FILE="${temp_root}/home/.aws/config" \
|
||||
node /app/openclaw.mjs agent --message "workspace bootstrap smoke" --session-id "workspace-bootstrap-smoke" --local --timeout 1 --json \
|
||||
>"${temp_root}/out.log" 2>&1
|
||||
status="$?"
|
||||
set -e
|
||||
if grep -F "Missing workspace template:" "${temp_root}/out.log"; then
|
||||
cat "${temp_root}/out.log"
|
||||
exit 1
|
||||
fi
|
||||
test -f "${temp_root}/home/.openclaw/workspace/HEARTBEAT.md"
|
||||
if [ "${status}" -ne 0 ]; then
|
||||
cat "${temp_root}/out.log"
|
||||
fi
|
||||
'
|
||||
|
||||
normal_ci:
|
||||
name: Run normal full CI
|
||||
needs: [resolve_target, docker_runtime_assets_preflight]
|
||||
@@ -380,6 +337,21 @@ jobs:
|
||||
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
|
||||
}
|
||||
|
||||
fail_fast_failed_jobs() {
|
||||
local failed_jobs_json
|
||||
failed_jobs_json="$(
|
||||
fetch_child_jobs |
|
||||
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
|
||||
)"
|
||||
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
|
||||
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
|
||||
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
|
||||
cancel_child
|
||||
trap - EXIT INT TERM
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cancel_child() {
|
||||
if [[ -n "${run_id:-}" ]]; then
|
||||
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
|
||||
@@ -395,6 +367,9 @@ jobs:
|
||||
break
|
||||
fi
|
||||
poll_count=$((poll_count + 1))
|
||||
if (( poll_count % 2 == 0 )); then
|
||||
fail_fast_failed_jobs
|
||||
fi
|
||||
if (( poll_count % 10 == 0 )); then
|
||||
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
|
||||
@@ -510,6 +485,21 @@ jobs:
|
||||
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
|
||||
}
|
||||
|
||||
fail_fast_failed_jobs() {
|
||||
local failed_jobs_json
|
||||
failed_jobs_json="$(
|
||||
fetch_child_jobs |
|
||||
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
|
||||
)"
|
||||
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
|
||||
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
|
||||
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
|
||||
cancel_child
|
||||
trap - EXIT INT TERM
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cancel_child() {
|
||||
if [[ -n "${run_id:-}" ]]; then
|
||||
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
|
||||
@@ -525,6 +515,9 @@ jobs:
|
||||
break
|
||||
fi
|
||||
poll_count=$((poll_count + 1))
|
||||
if (( poll_count % 2 == 0 )); then
|
||||
fail_fast_failed_jobs
|
||||
fi
|
||||
if (( poll_count % 10 == 0 )); then
|
||||
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
|
||||
@@ -690,6 +683,24 @@ jobs:
|
||||
[[ "$saw_advisory" == "1" && "$failed" == "0" ]]
|
||||
}
|
||||
|
||||
fail_fast_failed_jobs() {
|
||||
local failed_jobs_json
|
||||
if [[ "$workflow" == "openclaw-release-checks.yml" && "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
|
||||
return 0
|
||||
fi
|
||||
failed_jobs_json="$(
|
||||
fetch_child_jobs |
|
||||
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
|
||||
)"
|
||||
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
|
||||
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
|
||||
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
|
||||
cancel_child
|
||||
trap - EXIT INT TERM
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cancel_child() {
|
||||
if [[ -n "${run_id:-}" ]]; then
|
||||
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
|
||||
@@ -705,6 +716,9 @@ jobs:
|
||||
break
|
||||
fi
|
||||
poll_count=$((poll_count + 1))
|
||||
if (( poll_count % 2 == 0 )); then
|
||||
fail_fast_failed_jobs
|
||||
fi
|
||||
if (( poll_count % 10 == 0 )); then
|
||||
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
|
||||
@@ -962,6 +976,21 @@ jobs:
|
||||
}
|
||||
trap cancel_child EXIT INT TERM
|
||||
|
||||
fail_fast_failed_jobs() {
|
||||
local failed_jobs_json
|
||||
failed_jobs_json="$(
|
||||
gh_with_retry run view "$run_id" --json jobs \
|
||||
--jq '[.jobs[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
|
||||
)"
|
||||
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
|
||||
echo "::error::npm-telegram-beta-e2e.yml has failed child jobs before the workflow completed; cancelling the remaining run."
|
||||
jq '.[] | {name, conclusion, url}' <<< "$failed_jobs_json"
|
||||
cancel_child
|
||||
trap - EXIT INT TERM
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
poll_count=0
|
||||
while true; do
|
||||
status="$(gh_with_retry run view "$run_id" --json status --jq '.status')"
|
||||
@@ -969,6 +998,9 @@ jobs:
|
||||
break
|
||||
fi
|
||||
poll_count=$((poll_count + 1))
|
||||
if (( poll_count % 2 == 0 )); then
|
||||
fail_fast_failed_jobs
|
||||
fi
|
||||
if (( poll_count % 10 == 0 )); then
|
||||
echo "Still waiting on npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||
gh_with_retry run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
|
||||
|
||||
25
.github/workflows/openclaw-release-publish.yml
vendored
@@ -46,11 +46,12 @@ on:
|
||||
default: true
|
||||
type: boolean
|
||||
release_profile:
|
||||
description: Release coverage profile used for release evidence summaries
|
||||
description: Release coverage profile used for release evidence summaries; default reads it from the validation manifest
|
||||
required: false
|
||||
default: beta
|
||||
default: from-validation
|
||||
type: choice
|
||||
options:
|
||||
- from-validation
|
||||
- beta
|
||||
- stable
|
||||
- full
|
||||
@@ -135,9 +136,9 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
case "$RELEASE_PROFILE" in
|
||||
beta|stable|full) ;;
|
||||
from-validation|beta|stable|full) ;;
|
||||
*)
|
||||
echo "release_profile must be one of: beta, stable, full" >&2
|
||||
echo "release_profile must be one of: from-validation, beta, stable, full" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -259,6 +260,7 @@ jobs:
|
||||
echo "sha=$release_sha" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate full release validation manifest
|
||||
id: full_manifest
|
||||
if: ${{ inputs.publish_openclaw_npm }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -289,7 +291,7 @@ jobs:
|
||||
echo "Full release validation target SHA mismatch: expected $EXPECTED_SHA, got $target_sha" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$release_profile" != "$EXPECTED_RELEASE_PROFILE" ]]; then
|
||||
if [[ "$EXPECTED_RELEASE_PROFILE" != "from-validation" && "$release_profile" != "$EXPECTED_RELEASE_PROFILE" ]]; then
|
||||
echo "Full release validation profile mismatch: expected $EXPECTED_RELEASE_PROFILE, got $release_profile" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -297,6 +299,7 @@ jobs:
|
||||
echo "Full release validation must run rerun_group=all before npm publish; got $rerun_group" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "release_profile=$release_profile" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate release tag is reachable from a trusted release branch
|
||||
env:
|
||||
@@ -332,7 +335,7 @@ jobs:
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
TARGET_SHA: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
|
||||
RELEASE_PROFILE: ${{ inputs.release_profile }}
|
||||
RELEASE_PROFILE: ${{ steps.full_manifest.outputs.release_profile || inputs.release_profile }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
run: |
|
||||
{
|
||||
@@ -501,7 +504,7 @@ jobs:
|
||||
wait_for_run() {
|
||||
local workflow="$1"
|
||||
local run_id="$2"
|
||||
local status conclusion url updated_at created_at duration_seconds duration_label last_state
|
||||
local status conclusion url updated_at created_at duration_seconds duration_label last_state failed_json
|
||||
|
||||
last_state=""
|
||||
while true; do
|
||||
@@ -510,6 +513,14 @@ jobs:
|
||||
if [[ "$status" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
failed_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json jobs \
|
||||
--jq '[.jobs[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]' || true)"
|
||||
if [[ -n "${failed_json}" ]] && jq -e 'length > 0' <<< "$failed_json" >/dev/null; then
|
||||
echo "${workflow} has failed jobs before the workflow completed: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" >&2
|
||||
jq '.[] | {name, conclusion, url}' <<< "$failed_json" >&2 || true
|
||||
print_failed_run_summary "${run_id}"
|
||||
return 1
|
||||
fi
|
||||
url="$(printf '%s' "$run_json" | jq -r '.url')"
|
||||
updated_at="$(printf '%s' "$run_json" | jq -r '.updatedAt')"
|
||||
state="${status}:${updated_at}"
|
||||
|
||||
@@ -818,6 +818,7 @@ jobs:
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_SLACK_CAPTURE_CONTENT: "1"
|
||||
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
|
||||
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.slack_scenario || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.6.1
|
||||
## 2026.6.2
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Codex: surface Skill Workshop guidance in Codex app-server prompts when `skill_workshop` is available. Thanks @shakkernerd.
|
||||
- Agents/auth: write auth profiles atomically, add force re-login recovery, preserve workspaces during state-only uninstall, and compact before oversized turns so recovery paths avoid partial state.
|
||||
- Skills: skip disabled skill env overrides from stale persisted snapshots so disabled skill `apiKey` SecretRefs cannot abort embedded or channel turns. (#79072, #79173) Thanks @zeus1959.
|
||||
- Skill Workshop: render the Control UI tab from filtered navigation state and keep filtered fallback routing stable.
|
||||
- CLI: avoid live catalog validation during `openclaw agents add`, so adding a secondary agent no longer depends on provider catalog availability. (#76284, #88314) Thanks @zhangguiping-xydt.
|
||||
- CLI: keep `plugins list --json` on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.
|
||||
- CLI/desktop: bridge WSL clipboard operations through the shell and recognize manual-update launchd jobs. (#88764)
|
||||
@@ -83,6 +84,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels: preserve long Feishu streaming replies, send visible fallbacks when accepted Feishu turns produce no final reply, tolerate iMessage self-chat timestamp skew, preserve colon-prefixed slash commands in mention parsing, decode Nostr `npub` allowlists correctly, and suppress raw provider errors during channel delivery. (#87896)
|
||||
- Config/status/doctor: skip unresolved shell references in state-dir dotenv files, resolve gateway auth secrets during deep status audits, respect explicit PI runtime policy, report runtime tool-schema errors, and keep post-upgrade JSON stable. (#88288)
|
||||
- Gateway/session state: list commands from the Gateway plugin registry, harden MCP loopback tool schemas, hide phantom agent-store rows from `sessions.list`, make task persistence failures explicit, and carry session UUIDs on interactive dispatch events.
|
||||
- Gateway/plugins: narrow plugin lookup memoization to the stable plugin/runtime inputs, avoiding repeated lookup work without mixing disabled or filtered plugin state.
|
||||
- OpenAI/TTS: handle speed directives for OpenAI TTS voices. (#74089)
|
||||
- CI/Crabbox: keep default runner capacity on the Azure credit-backed on-demand D4 lane with the Azure SSH port and a Git-independent full check job, so broad validation avoids low-priority spot quota stalls, hydrate port mismatches, non-Git hydrated workspaces, and stale AWS region hints.
|
||||
- CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.
|
||||
@@ -657,6 +659,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/sessions: allow shared-secret bearer callers to read and stream session history without an explicit scope header. (#81815) Thanks @medns.
|
||||
- Agents/embedded runner: classify HTML auth provider responses as `auth_html` and return a re-authentication hint instead of the CDN-blocked copy that `upstream_html` returns. Cloudflare Access login pages, nginx basic-auth challenges, and gateway login walls all produce HTML auth bodies that were previously misdiagnosed as transient CDN blocks. (#79900) Thanks @martingarramon.
|
||||
- TUI/streaming watchdog: dismiss the `This response is taking longer than expected` notice as soon as a chat event for the same run arrives, so the message no longer sits next to the recovered response when the run was only briefly silent. Refs #67052, #69081 (closed), prior attempt #69026. Thanks @jpruit20 and @romneyda.
|
||||
- Agents/auth profiles: replace the bare `No available auth profile for <provider> (all in cooldown or unavailable)` TUI error with plain-language copy that explains what happened in user terms (sign-in expired, provider asking us to slow down, billing issue on the account, etc.) and suggests the matching `openclaw models auth login --provider <provider>` recovery command for sign-in and billing causes, while falling back to the underlying provider error for cases without a clear recovery path. Thanks @romneyda.
|
||||
- Agents/Pi: tolerate OpenClaw-owned transcript writes while embedded prompts are released for model I/O, keeping long-running Feishu, Slack, Telegram, and cron turns from failing with false session-takeover errors. Fixes #84059. (#84250) Thanks @tianxiaochannel-oss88.
|
||||
|
||||
## 2026.5.20
|
||||
|
||||
@@ -65,8 +65,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026053101
|
||||
versionName = "2026.6.1"
|
||||
versionCode = 2026060201
|
||||
versionName = "2026.6.2"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.6.2 - 2026-06-02
|
||||
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
## 2026.6.1 - 2026-06-01
|
||||
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.6.1
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.1
|
||||
OPENCLAW_IOS_VERSION = 2026.6.2
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.2
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
- Added hosted push relay defaults, realtime Talk playback, and safer WebSocket ping handling for mobile sessions.
|
||||
- Updated App Store screenshots to cover Gateway pairing, Command, Chat, Talk, Agent, and Settings flows.
|
||||
- Highlighted realtime Talk relay, Gateway connection status, node capabilities, push wake, and privacy controls.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.6.1"
|
||||
"version": "2026.6.2"
|
||||
}
|
||||
|
||||
@@ -514,12 +514,16 @@ extension GatewayConnection {
|
||||
var params: [String: AnyCodable] = [
|
||||
"message": AnyCodable(trimmed),
|
||||
"sessionKey": AnyCodable(sessionKey),
|
||||
"thinking": AnyCodable(invocation.thinking ?? "default"),
|
||||
"deliver": AnyCodable(invocation.deliver),
|
||||
"to": AnyCodable(invocation.to ?? ""),
|
||||
"channel": AnyCodable(invocation.channel.rawValue),
|
||||
"idempotencyKey": AnyCodable(invocation.idempotencyKey),
|
||||
]
|
||||
if let thinking = invocation.thinking?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!thinking.isEmpty
|
||||
{
|
||||
params["thinking"] = AnyCodable(thinking)
|
||||
}
|
||||
if let timeout = invocation.timeoutSeconds {
|
||||
params["timeout"] = AnyCodable(timeout)
|
||||
}
|
||||
@@ -664,7 +668,7 @@ extension GatewayConnection {
|
||||
func chatSend(
|
||||
sessionKey: String,
|
||||
message: String,
|
||||
thinking: String,
|
||||
thinking: String?,
|
||||
idempotencyKey: String,
|
||||
attachments: [OpenClawChatAttachmentPayload],
|
||||
timeoutMs: Int = 30000) async throws -> OpenClawChatSendResponse
|
||||
@@ -673,10 +677,14 @@ extension GatewayConnection {
|
||||
var params: [String: AnyCodable] = [
|
||||
"sessionKey": AnyCodable(resolvedKey),
|
||||
"message": AnyCodable(message),
|
||||
"thinking": AnyCodable(thinking),
|
||||
"idempotencyKey": AnyCodable(idempotencyKey),
|
||||
"timeoutMs": AnyCodable(timeoutMs),
|
||||
]
|
||||
if let thinking = thinking?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!thinking.isEmpty
|
||||
{
|
||||
params["thinking"] = AnyCodable(thinking)
|
||||
}
|
||||
|
||||
if !attachments.isEmpty {
|
||||
let encoded = attachments.map { att in
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.6.1</string>
|
||||
<string>2026.6.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026053100</string>
|
||||
<string>2026060200</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -387,7 +387,7 @@ actor TalkModeRuntime {
|
||||
let response = try await GatewayConnection.shared.chatSend(
|
||||
sessionKey: sessionKey,
|
||||
message: prompt,
|
||||
thinking: "low",
|
||||
thinking: nil,
|
||||
idempotencyKey: runId,
|
||||
attachments: [])
|
||||
guard self.isCurrent(gen) else { return }
|
||||
|
||||
@@ -34,7 +34,7 @@ enum VoiceWakeForwarder {
|
||||
|
||||
struct ForwardOptions {
|
||||
var sessionKey: String = "main"
|
||||
var thinking: String = "low"
|
||||
var thinking: String?
|
||||
var deliver: Bool = true
|
||||
var to: String?
|
||||
var channel: GatewayAgentChannel = .webchat
|
||||
@@ -97,7 +97,6 @@ enum VoiceWakeForwarder {
|
||||
|
||||
return ForwardOptions(
|
||||
sessionKey: sessionKey,
|
||||
thinking: "low",
|
||||
deliver: true,
|
||||
to: to,
|
||||
channel: channel,
|
||||
|
||||
@@ -173,9 +173,57 @@ private func makeTestGatewayConnection() -> (GatewayConnection, FakeWebSocketSes
|
||||
|
||||
let json = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
|
||||
let params = json?["params"] as? [String: Any]
|
||||
#expect(params?["thinking"] == nil)
|
||||
#expect(params?["voiceWakeTrigger"] as? String == "")
|
||||
}
|
||||
|
||||
@Test func `chat send omits thinking when inheriting session default`() async throws {
|
||||
let recorder = WebSocketMessageRecorder()
|
||||
let session = GatewayTestWebSocketSession(taskFactory: {
|
||||
GatewayTestWebSocketTask(sendHook: { task, message, sendIndex in
|
||||
recorder.append(message)
|
||||
guard sendIndex > 0,
|
||||
let data = Self.messageData(message),
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let id = json["id"] as? String
|
||||
else { return }
|
||||
task.emitReceiveSuccess(.data(Self.chatSendOkResponseData(id: id)))
|
||||
})
|
||||
})
|
||||
let connection = GatewayConnection(
|
||||
configProvider: {
|
||||
(url: URL(string: "ws://127.0.0.1:1")!, token: nil, password: nil)
|
||||
},
|
||||
sessionBox: WebSocketSessionBox(session: session))
|
||||
|
||||
_ = try await connection.chatSend(
|
||||
sessionKey: "main",
|
||||
message: "hello",
|
||||
thinking: nil,
|
||||
idempotencyKey: "chat-1",
|
||||
attachments: [])
|
||||
await connection.shutdown()
|
||||
|
||||
guard let chatMessage = recorder.snapshot().reversed().first(where: { message in
|
||||
guard let data = Self.messageData(message),
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
else { return false }
|
||||
return json["method"] as? String == "chat.send"
|
||||
}) else {
|
||||
Issue.record("expected chat.send websocket payload")
|
||||
return
|
||||
}
|
||||
|
||||
guard let payloadData = Self.messageData(chatMessage) else {
|
||||
Issue.record("unexpected chat.send websocket message type")
|
||||
return
|
||||
}
|
||||
|
||||
let json = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
|
||||
let params = json?["params"] as? [String: Any]
|
||||
#expect(params?["thinking"] == nil)
|
||||
}
|
||||
|
||||
private static func messageData(_ message: URLSessionWebSocketTask.Message) -> Data? {
|
||||
switch message {
|
||||
case let .string(text):
|
||||
@@ -186,4 +234,15 @@ private func makeTestGatewayConnection() -> (GatewayConnection, FakeWebSocketSes
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func chatSendOkResponseData(id: String) -> Data {
|
||||
Data("""
|
||||
{
|
||||
"type": "res",
|
||||
"id": "\(id)",
|
||||
"ok": true,
|
||||
"payload": { "runId": "chat-1", "status": "ok" }
|
||||
}
|
||||
""".utf8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import Testing
|
||||
@Test func `forward options defaults`() {
|
||||
let opts = VoiceWakeForwarder.ForwardOptions()
|
||||
#expect(opts.sessionKey == "main")
|
||||
#expect(opts.thinking == "low")
|
||||
#expect(opts.thinking == nil)
|
||||
#expect(opts.deliver == true)
|
||||
#expect(opts.to == nil)
|
||||
#expect(opts.channel == .webchat)
|
||||
@@ -38,6 +38,7 @@ import Testing
|
||||
#expect(opts.channel == .telegram)
|
||||
#expect(opts.to == "telegram:6812765697")
|
||||
#expect(opts.voiceWakeTrigger == "open claw")
|
||||
#expect(opts.thinking == nil)
|
||||
#expect(opts.channel.shouldDeliver(opts.deliver) == true)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
bdcf661ec680f79819096950295bdb04805aac9639477058d8855f294f6d8034 plugin-sdk-api-baseline.json
|
||||
6b8c92cc5a9277f90973370102fa31efb23ffd93008c3ed961d38e4a8a3073b0 plugin-sdk-api-baseline.jsonl
|
||||
63d49032a9b4dc4874a0ca17be73ecc97a2df5d1f47b4e72db34868423370558 plugin-sdk-api-baseline.json
|
||||
af79f7d711afa0a8563782b8f5cdd7e46b9aea245f5e7ebc464327a8969ed65e plugin-sdk-api-baseline.jsonl
|
||||
|
||||
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 251 KiB |
BIN
docs/assets/showcase/caldav-calendar.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 251 KiB |
BIN
docs/assets/showcase/homeassistant.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
docs/assets/showcase/openrouter-transcribe.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
docs/assets/showcase/r2-upload.png
Normal file
|
After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 244 KiB |
@@ -93,6 +93,7 @@ openclaw onboard --non-interactive \
|
||||
|
||||
`--custom-api-key` is optional in non-interactive mode. If omitted, onboarding checks `CUSTOM_API_KEY`.
|
||||
OpenClaw marks common vision model IDs as image-capable automatically. Pass `--custom-image-input` for unknown custom vision IDs, or `--custom-text-input` to force text-only metadata.
|
||||
Use `--custom-compatibility openai-responses` for OpenAI-compatible endpoints that support `/v1/responses` but not `/v1/chat/completions`.
|
||||
|
||||
LM Studio also supports a provider-specific key flag in non-interactive mode:
|
||||
|
||||
|
||||
@@ -101,6 +101,8 @@ Automated UK school meal booking via ParentPay. Uses mouse coordinates for relia
|
||||
**@julianengel** • `files` `r2` `presigned-urls`
|
||||
|
||||
Upload to Cloudflare R2/S3 and generate secure presigned download links. Useful for remote OpenClaw instances.
|
||||
|
||||
<img src="/assets/showcase/r2-upload.png" alt="R2 upload skill on ClawHub" />
|
||||
</Card>
|
||||
|
||||
<Card title="iOS app via Telegram" icon="mobile">
|
||||
@@ -269,6 +271,8 @@ Vapi voice assistant to OpenClaw HTTP bridge. Near real-time phone calls with yo
|
||||
**@obviyus** • `transcription` `multilingual` `skill`
|
||||
|
||||
Multi-lingual audio transcription via OpenRouter (Gemini, and more). Available on ClawHub.
|
||||
|
||||
<img src="/assets/showcase/openrouter-transcribe.png" alt="OpenRouter transcription skill on ClawHub" />
|
||||
</Card>
|
||||
|
||||
</CardGroup>
|
||||
@@ -289,6 +293,8 @@ OpenClaw gateway running on Home Assistant OS with SSH tunnel support and persis
|
||||
**ClawHub** • `homeassistant` `skill` `automation`
|
||||
|
||||
Control and automate Home Assistant devices via natural language.
|
||||
|
||||
<img src="/assets/showcase/homeassistant.png" alt="Home Assistant skill on ClawHub" />
|
||||
</Card>
|
||||
|
||||
<Card title="Nix packaging" icon="snowflake" href="https://github.com/openclaw/nix-openclaw">
|
||||
@@ -301,6 +307,8 @@ Batteries-included nixified OpenClaw configuration for reproducible deployments.
|
||||
**ClawHub** • `calendar` `caldav` `skill`
|
||||
|
||||
Calendar skill using khal and vdirsyncer. Self-hosted calendar integration.
|
||||
|
||||
<img src="/assets/showcase/caldav-calendar.png" alt="CalDAV calendar skill on ClawHub" />
|
||||
</Card>
|
||||
|
||||
</CardGroup>
|
||||
|
||||
@@ -219,7 +219,7 @@ What you set:
|
||||
- `--custom-model-id`
|
||||
- `--custom-api-key` (optional; falls back to `CUSTOM_API_KEY`)
|
||||
- `--custom-provider-id` (optional)
|
||||
- `--custom-compatibility <openai|anthropic>` (optional; default `openai`)
|
||||
- `--custom-compatibility <openai|openai-responses|anthropic>` (optional; default `openai`)
|
||||
- `--custom-image-input` / `--custom-text-input` (optional; override inferred model input capability)
|
||||
|
||||
</Accordion>
|
||||
|
||||
4
extensions/acpx/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"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.1",
|
||||
"version": "2026.6.2",
|
||||
"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.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1",
|
||||
"openclawVersion": "2026.6.2",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./src/runtime-internals/mcp-proxy.mjs",
|
||||
|
||||
@@ -215,6 +215,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
agent: "codex",
|
||||
mode: "persistent",
|
||||
model: "gpt-5.4",
|
||||
sessionOptions: { model: "gpt-5.4" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -619,7 +620,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not normalize model startup for non-Codex ACP agents", async () => {
|
||||
it("passes model startup through sessionOptions for non-Codex ACP agents", async () => {
|
||||
const baseStore: TestSessionStore = {
|
||||
load: vi.fn(async () => undefined),
|
||||
save: vi.fn(async () => {}),
|
||||
@@ -648,6 +649,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
agent: "main",
|
||||
mode: "persistent",
|
||||
model: "openai/gpt-5.5",
|
||||
sessionOptions: { model: "openai/gpt-5.5" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -694,6 +696,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
agent: "codex",
|
||||
mode: "persistent",
|
||||
model: "gpt-5.5",
|
||||
sessionOptions: { model: "gpt-5.5" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -728,6 +731,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
mode: "persistent",
|
||||
model: "gpt-5.4/xhigh",
|
||||
thinking: "x-high",
|
||||
sessionOptions: { model: "gpt-5.4/xhigh" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type AcpRuntimeStatus,
|
||||
type AcpRuntimeTurn,
|
||||
type AcpRuntimeTurnResult,
|
||||
type SessionAgentOptions,
|
||||
} from "acpx/runtime";
|
||||
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { redactSensitiveText } from "openclaw/plugin-sdk/security-runtime";
|
||||
@@ -49,6 +50,8 @@ type AcpxRuntimeTestOptions = Record<string, unknown> & {
|
||||
openclawProcessCleanup?: AcpxProcessCleanupDeps;
|
||||
};
|
||||
type OpenClawRuntimeTurnInput = Parameters<NonNullable<AcpRuntime["startTurn"]>>[0];
|
||||
type OpenClawRuntimeEnsureInput = Parameters<AcpRuntime["ensureSession"]>[0];
|
||||
type AcpxDelegateEnsureInput = Parameters<BaseAcpxRuntime["ensureSession"]>[0];
|
||||
|
||||
type ResetAwareSessionStore = AcpSessionStore & {
|
||||
markFresh: (sessionKey: string) => void;
|
||||
@@ -547,6 +550,16 @@ function codexAcpSessionModelId(override: CodexAcpModelOverride): string {
|
||||
: override.model;
|
||||
}
|
||||
|
||||
function withAcpxSessionOptions(input: OpenClawRuntimeEnsureInput): AcpxDelegateEnsureInput {
|
||||
const existingOptions = (input as { sessionOptions?: SessionAgentOptions }).sessionOptions;
|
||||
const model = input.model?.trim() || existingOptions?.model;
|
||||
const sessionOptions = model ? { ...existingOptions, model } : existingOptions;
|
||||
return {
|
||||
...input,
|
||||
...(sessionOptions ? { sessionOptions } : {}),
|
||||
} as AcpxDelegateEnsureInput;
|
||||
}
|
||||
|
||||
function quoteShellArg(value: string): string {
|
||||
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
|
||||
return value;
|
||||
@@ -942,7 +955,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
this.withCodexWrapperDiagnostics({
|
||||
command: stableLaunchCommand,
|
||||
fallbackCode: "ACP_SESSION_INIT_FAILED",
|
||||
run: () => delegate.ensureSession(input),
|
||||
run: () => delegate.ensureSession(withAcpxSessionOptions(input)),
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -962,7 +975,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
this.withCodexWrapperDiagnostics({
|
||||
command: stableLaunchCommand,
|
||||
fallbackCode: "ACP_SESSION_INIT_FAILED",
|
||||
run: () => delegate.ensureSession(normalizedInput),
|
||||
run: () => delegate.ensureSession(withAcpxSessionOptions(normalizedInput)),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/admin-http-rpc",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw admin HTTP RPC endpoint",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/alibaba-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"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.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"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.1",
|
||||
"version": "2026.6.2",
|
||||
"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.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1",
|
||||
"openclawVersion": "2026.6.2",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
4
extensions/amazon-bedrock/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"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.1",
|
||||
"version": "2026.6.2",
|
||||
"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.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1",
|
||||
"openclawVersion": "2026.6.2",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
4
extensions/anthropic-vertex/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/vertex-sdk": "0.16.1"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"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.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1",
|
||||
"openclawVersion": "2026.6.2",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/arcee-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Arcee provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/azure-speech",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Azure Speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/bonjour",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw Bonjour/mDNS gateway discovery",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
4
extensions/brave/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.6.1"
|
||||
"version": "2026.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw Brave Search provider plugin for web search.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -21,10 +21,10 @@
|
||||
"allowInvalidConfigRecovery": true
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1"
|
||||
"openclawVersion": "2026.6.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/browser-plugin",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw browser tool plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/byteplus-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw BytePlus provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/canvas-plugin",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Canvas plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/cerebras-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Cerebras provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/chutes-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Chutes.ai provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/clickclack",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw ClickClack channel plugin",
|
||||
"type": "module",
|
||||
@@ -18,7 +18,7 @@
|
||||
"openclaw": "2026.5.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.1"
|
||||
"openclaw": ">=2026.6.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/cloudflare-ai-gateway-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Cloudflare AI Gateway provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/codex-supervisor",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Codex app-server fleet supervision plugin.",
|
||||
"type": "module",
|
||||
|
||||
4
extensions/codex/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/codex",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/codex",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@openai/codex": "0.135.0",
|
||||
"typebox": "1.1.39",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/codex",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw Codex app-server harness and model provider plugin with a Codex-managed GPT catalog.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,10 +26,10 @@
|
||||
"minHostVersion": ">=2026.5.1-beta.1"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1"
|
||||
"openclawVersion": "2026.6.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -266,6 +266,7 @@ export async function startCodexAttemptThread(params: {
|
||||
mcpServersFingerprintEvaluated: params.bundleMcpThreadConfig.evaluated,
|
||||
environmentSelection: startupEnvironmentSelection,
|
||||
contextEngineProjection: params.contextEngineProjection,
|
||||
signal: params.signal,
|
||||
pluginThreadConfig: pluginThreadConfigRequired
|
||||
? {
|
||||
enabled: true,
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
import {
|
||||
filterCodexDynamicTools,
|
||||
resolveCodexDynamicToolsLoading,
|
||||
resolveCodexDynamicToolsLoadingForModel,
|
||||
shouldUseDirectCodexDynamicToolsForModel,
|
||||
} from "./dynamic-tool-profile.js";
|
||||
import { createCodexDynamicToolBridge } from "./dynamic-tools.js";
|
||||
import { createCodexTestModel } from "./test-support.js";
|
||||
@@ -179,6 +181,22 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
expect(resolveCodexDynamicToolsLoading({}, privateQaCodexEnv)).toBe("direct");
|
||||
});
|
||||
|
||||
it("uses direct dynamic tools for OpenAI nano models without tool_search support", () => {
|
||||
const tools = [createRuntimeDynamicTool("message"), createRuntimeDynamicTool("web_search")];
|
||||
const toolBridge = createCodexDynamicToolBridge({
|
||||
tools,
|
||||
signal: new AbortController().signal,
|
||||
loading: resolveCodexDynamicToolsLoadingForModel({}, "openai/gpt-5.4-nano"),
|
||||
});
|
||||
|
||||
expect(shouldUseDirectCodexDynamicToolsForModel("gpt-5.4-nano")).toBe(true);
|
||||
expect(resolveCodexDynamicToolsLoadingForModel({}, "gpt-5.4-nano")).toBe("direct");
|
||||
expect(resolveCodexDynamicToolsLoadingForModel({}, "gpt-5.5")).toBe("searchable");
|
||||
const webSearch = toolBridge.specs.find((tool) => tool.name === "web_search");
|
||||
expect(webSearch).not.toHaveProperty("deferLoading");
|
||||
expect(webSearch).not.toHaveProperty("namespace");
|
||||
});
|
||||
|
||||
it("quarantines unreadable tool entries before Codex-specific filtering", async () => {
|
||||
const messageTool = createRuntimeDynamicTool("message");
|
||||
const sourceTools = new Proxy([messageTool] as RuntimeDynamicToolForTest[], {
|
||||
|
||||
@@ -47,6 +47,33 @@ export function resolveCodexDynamicToolsLoading(
|
||||
: (config.codexDynamicToolsLoading ?? "searchable");
|
||||
}
|
||||
|
||||
function normalizeCodexModelId(modelId: string | undefined): string {
|
||||
const normalized = modelId?.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
return normalized.includes("/") ? normalized.split("/").at(-1)! : normalized;
|
||||
}
|
||||
|
||||
export function shouldUseDirectCodexDynamicToolsForModel(modelId: string | undefined): boolean {
|
||||
return shouldDisableCodexToolSearchForModel(modelId);
|
||||
}
|
||||
|
||||
export function shouldDisableCodexToolSearchForModel(modelId: string | undefined): boolean {
|
||||
return normalizeCodexModelId(modelId) === "gpt-5.4-nano";
|
||||
}
|
||||
|
||||
export function resolveCodexDynamicToolsLoadingForModel(
|
||||
config: Pick<CodexPluginConfig, "codexDynamicToolsLoading">,
|
||||
modelId: string | undefined,
|
||||
env: CodexDynamicToolProfileEnv = process.env,
|
||||
): CodexDynamicToolsLoading {
|
||||
const loading = resolveCodexDynamicToolsLoading(config, env);
|
||||
return loading === "searchable" && shouldUseDirectCodexDynamicToolsForModel(modelId)
|
||||
? "direct"
|
||||
: loading;
|
||||
}
|
||||
|
||||
export function filterCodexDynamicTools<T extends { name: string }>(
|
||||
tools: T[],
|
||||
config: Pick<CodexPluginConfig, "codexDynamicToolsExclude">,
|
||||
|
||||
@@ -1652,6 +1652,81 @@ describe("CodexAppServerEventProjector", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed when a native tool call finishes without a matching result", async () => {
|
||||
const trajectoryRecorder = {
|
||||
filePath: "trajectory.jsonl",
|
||||
recordEvent: vi.fn(),
|
||||
flush: vi.fn(async () => undefined),
|
||||
};
|
||||
const projector = await createProjector(await createParams(), { trajectoryRecorder });
|
||||
|
||||
await projector.handleNotification(
|
||||
forCurrentTurn("item/started", {
|
||||
item: {
|
||||
type: "commandExecution",
|
||||
id: "cmd-denied",
|
||||
command: "node scripts/report.js --publish",
|
||||
cwd: "/workspace",
|
||||
processId: null,
|
||||
source: "agent",
|
||||
status: "inProgress",
|
||||
commandActions: [],
|
||||
aggregatedOutput: null,
|
||||
exitCode: null,
|
||||
durationMs: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
await projector.handleNotification(
|
||||
turnCompleted([
|
||||
{
|
||||
type: "agentMessage",
|
||||
id: "msg-denied",
|
||||
text: "The requested publish command was denied before execution.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const result = projector.buildResult(buildEmptyToolTelemetry());
|
||||
|
||||
expect(String(result.promptError)).toContain("without a matching tool.result");
|
||||
expect(result.promptErrorSource).toBe("prompt");
|
||||
expect(result.messagesSnapshot.map((message) => message.role)).toEqual([
|
||||
"user",
|
||||
"assistant",
|
||||
"toolResult",
|
||||
"assistant",
|
||||
]);
|
||||
const toolResultMessage = requireRecord(result.messagesSnapshot[2], "tool result message");
|
||||
expect(toolResultMessage.toolCallId).toBe("cmd-denied");
|
||||
expect(toolResultMessage.toolName).toBe("bash");
|
||||
expect(toolResultMessage.isError).toBe(true);
|
||||
const toolResultContent = requireArray(toolResultMessage.content, "tool result content");
|
||||
expect(JSON.stringify(toolResultContent)).toContain("matching tool.result");
|
||||
expect(trajectoryRecorder.recordEvent).toHaveBeenCalledWith("tool.call", {
|
||||
threadId: THREAD_ID,
|
||||
turnId: TURN_ID,
|
||||
itemId: "cmd-denied",
|
||||
toolCallId: "cmd-denied",
|
||||
name: "bash",
|
||||
arguments: {
|
||||
command: "node scripts/report.js --publish",
|
||||
cwd: "/workspace",
|
||||
},
|
||||
});
|
||||
expect(trajectoryRecorder.recordEvent).toHaveBeenCalledWith("tool.result", {
|
||||
threadId: THREAD_ID,
|
||||
turnId: TURN_ID,
|
||||
itemId: "cmd-denied",
|
||||
toolCallId: "cmd-denied",
|
||||
name: "bash",
|
||||
status: "failed",
|
||||
isError: true,
|
||||
result: { status: "failed", reason: "missing_tool_result" },
|
||||
output: expect.stringContaining("without a matching tool.result"),
|
||||
});
|
||||
});
|
||||
|
||||
it("uses streamed command output when final command snapshots omit aggregated output", async () => {
|
||||
const onAgentEvent = vi.fn();
|
||||
const trajectoryRecorder = {
|
||||
|
||||
@@ -109,6 +109,8 @@ const CODEX_PROMPT_TOTAL_INPUT_KEYS = [
|
||||
|
||||
const MAX_TOOL_OUTPUT_DELTA_MESSAGES_PER_ITEM = 20;
|
||||
const TOOL_TRANSCRIPT_OUTPUT_MAX_CHARS = 12_000;
|
||||
const MISSING_TOOL_RESULT_ERROR =
|
||||
"OpenClaw recorded a native Codex tool.call without a matching tool.result before the turn completed.";
|
||||
const GENERATED_IMAGE_MEDIA_SUBDIR = "tool-image-generation";
|
||||
const BYTES_PER_MB = 1024 * 1024;
|
||||
// Match OpenClaw's default image media cap for generated image tool outputs.
|
||||
@@ -172,6 +174,10 @@ export class CodexAppServerEventProjector {
|
||||
private readonly toolTranscriptMessages: AgentMessage[] = [];
|
||||
private readonly toolTranscriptCallIds = new Set<string>();
|
||||
private readonly toolTranscriptResultIds = new Set<string>();
|
||||
private readonly toolTranscriptNamesById = new Map<string, string>();
|
||||
private readonly toolTrajectoryCallIds = new Set<string>();
|
||||
private readonly toolTrajectoryResultIds = new Set<string>();
|
||||
private readonly toolTrajectoryNamesById = new Map<string, string>();
|
||||
private readonly transcriptToolProgressCallIds = new Set<string>();
|
||||
private lastNativeToolError: EmbeddedRunAttemptResult["lastToolError"];
|
||||
private readonly nativeGeneratedMediaUrls = new Set<string>();
|
||||
@@ -185,6 +191,7 @@ export class CodexAppServerEventProjector {
|
||||
private completedTurn: CodexTurn | undefined;
|
||||
private promptError: unknown;
|
||||
private promptErrorSource: EmbeddedRunAttemptResult["promptErrorSource"] = null;
|
||||
private synthesizedMissingToolResultError: string | null = null;
|
||||
private aborted = false;
|
||||
private tokenUsage: ReturnType<typeof normalizeUsage>;
|
||||
private guardianReviewCount = 0;
|
||||
@@ -285,6 +292,12 @@ export class CodexAppServerEventProjector {
|
||||
this.reasoningItemOrder,
|
||||
).join("\n\n");
|
||||
const planText = collectTextValues(this.planTextByItem).join("\n\n");
|
||||
this.synthesizeMissingToolResults({
|
||||
failClosed:
|
||||
!this.completedTurn ||
|
||||
this.completedTurn.status !== "completed" ||
|
||||
assistantTexts.length > 0,
|
||||
});
|
||||
const lastAssistant =
|
||||
assistantTexts.length > 0
|
||||
? this.createAssistantMessage(assistantTexts.join("\n\n"))
|
||||
@@ -328,6 +341,7 @@ export class CodexAppServerEventProjector {
|
||||
const turnFailed = this.completedTurn?.status === "failed";
|
||||
const promptError =
|
||||
this.promptError ??
|
||||
this.synthesizedMissingToolResultError ??
|
||||
(turnFailed ? (this.completedTurn?.error?.message ?? "codex app-server turn failed") : null);
|
||||
const agentHarnessResultClassification = classifyAgentHarnessTerminalOutcome({
|
||||
assistantTexts,
|
||||
@@ -1125,6 +1139,8 @@ export class CodexAppServerEventProjector {
|
||||
status: ReturnType<typeof itemStatus>;
|
||||
}): void {
|
||||
if (params.phase === "start") {
|
||||
this.toolTrajectoryCallIds.add(params.item.id);
|
||||
this.toolTrajectoryNamesById.set(params.item.id, params.name);
|
||||
this.options.trajectoryRecorder?.recordEvent("tool.call", {
|
||||
threadId: this.threadId,
|
||||
turnId: this.turnId,
|
||||
@@ -1135,6 +1151,7 @@ export class CodexAppServerEventProjector {
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.toolTrajectoryResultIds.add(params.item.id);
|
||||
const toolResult = itemToolResult(params.item).result;
|
||||
const output = itemOutputText(params.item, this.toolResultOutputTextByItem);
|
||||
this.options.trajectoryRecorder?.recordEvent("tool.result", {
|
||||
@@ -1396,6 +1413,7 @@ export class CodexAppServerEventProjector {
|
||||
return;
|
||||
}
|
||||
this.toolTranscriptCallIds.add(params.id);
|
||||
this.toolTranscriptNamesById.set(params.id, params.name);
|
||||
this.toolTranscriptArgumentsById.set(params.id, params.arguments);
|
||||
if (!shouldEmitTranscriptToolProgress(params.name, params.arguments)) {
|
||||
this.transcriptToolProgressSuppressedIds.add(params.id);
|
||||
@@ -1425,6 +1443,61 @@ export class CodexAppServerEventProjector {
|
||||
);
|
||||
}
|
||||
|
||||
private synthesizeMissingToolResults(params: { failClosed: boolean }): void {
|
||||
if (!params.failClosed) {
|
||||
return;
|
||||
}
|
||||
const missingTranscriptIds = [...this.toolTranscriptCallIds].filter(
|
||||
(id) => !this.toolTranscriptResultIds.has(id),
|
||||
);
|
||||
const missingTrajectoryIds = [...this.toolTrajectoryCallIds].filter(
|
||||
(id) => !this.toolTrajectoryResultIds.has(id),
|
||||
);
|
||||
if (missingTranscriptIds.length === 0 && missingTrajectoryIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const id of missingTranscriptIds) {
|
||||
const name = this.toolTranscriptNamesById.get(id) ?? this.toolTrajectoryNamesById.get(id);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
this.recordToolTranscriptResult({
|
||||
id,
|
||||
name,
|
||||
text: formatMissingToolResultError({ id, name }),
|
||||
isError: true,
|
||||
});
|
||||
}
|
||||
|
||||
for (const id of missingTrajectoryIds) {
|
||||
const name = this.toolTrajectoryNamesById.get(id) ?? this.toolTranscriptNamesById.get(id);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
this.toolTrajectoryResultIds.add(id);
|
||||
const text = formatMissingToolResultError({ id, name });
|
||||
this.options.trajectoryRecorder?.recordEvent("tool.result", {
|
||||
threadId: this.threadId,
|
||||
turnId: this.turnId,
|
||||
itemId: id,
|
||||
toolCallId: id,
|
||||
name,
|
||||
status: "failed",
|
||||
isError: true,
|
||||
result: { status: "failed", reason: "missing_tool_result" },
|
||||
output: text,
|
||||
});
|
||||
}
|
||||
|
||||
const missingCount = new Set([...missingTranscriptIds, ...missingTrajectoryIds]).size;
|
||||
this.synthesizedMissingToolResultError =
|
||||
missingCount === 1
|
||||
? MISSING_TOOL_RESULT_ERROR
|
||||
: `${MISSING_TOOL_RESULT_ERROR} missingToolResultCount=${missingCount}`;
|
||||
this.promptErrorSource = this.promptErrorSource ?? "prompt";
|
||||
}
|
||||
|
||||
private emitTranscriptToolCallProgress(params: ToolTranscriptCallInput): void {
|
||||
if (!shouldEmitTranscriptToolProgress(params.name, params.arguments)) {
|
||||
return;
|
||||
@@ -1954,6 +2027,10 @@ function itemStatus(item: CodexThreadItem): "completed" | "failed" | "running" |
|
||||
return "completed";
|
||||
}
|
||||
|
||||
function formatMissingToolResultError(params: { id: string; name: string }): string {
|
||||
return `${MISSING_TOOL_RESULT_ERROR} toolCallId=${params.id}; toolName=${params.name}`;
|
||||
}
|
||||
|
||||
function isNonSuccessItemStatus(status: ReturnType<typeof itemStatus>): boolean {
|
||||
return status === "failed" || status === "blocked";
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ import {
|
||||
} from "./dynamic-tool-execution.js";
|
||||
import {
|
||||
filterCodexDynamicTools,
|
||||
resolveCodexDynamicToolsLoading,
|
||||
resolveCodexDynamicToolsLoadingForModel,
|
||||
} from "./dynamic-tool-profile.js";
|
||||
import { createCodexDynamicToolBridge } from "./dynamic-tools.js";
|
||||
import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js";
|
||||
@@ -595,7 +595,7 @@ export async function runCodexAppServerAttempt(
|
||||
tools,
|
||||
registeredTools,
|
||||
signal: runAbortController.signal,
|
||||
loading: resolveCodexDynamicToolsLoading(pluginConfig),
|
||||
loading: resolveCodexDynamicToolsLoadingForModel(pluginConfig, params.modelId),
|
||||
directToolNames: shouldForceMessageTool(params) ? ["message"] : [],
|
||||
hookContext: {
|
||||
agentId: sessionAgentId,
|
||||
@@ -2640,7 +2640,7 @@ export const testing = {
|
||||
buildDynamicTools,
|
||||
filterCodexDynamicToolsForAllowlist,
|
||||
includeForcedCodexDynamicToolAllow,
|
||||
resolveCodexDynamicToolsLoading,
|
||||
resolveCodexDynamicToolsLoadingForModel,
|
||||
resolveCodexAppServerHookChannelId,
|
||||
buildCodexAppServerPromptTimeoutOutcome,
|
||||
resolveOpenClawCodingToolsSessionKeys,
|
||||
|
||||
@@ -40,6 +40,7 @@ export type CodexAppServerThreadBinding = {
|
||||
sandbox?: CodexAppServerSandboxMode;
|
||||
serviceTier?: CodexServiceTier;
|
||||
dynamicToolsFingerprint?: string;
|
||||
dynamicToolsContainDeferred?: boolean;
|
||||
userMcpServersFingerprint?: string;
|
||||
mcpServersFingerprint?: string;
|
||||
nativeHookRelayGeneration?: string;
|
||||
@@ -111,6 +112,10 @@ export async function readCodexAppServerBinding(
|
||||
typeof parsed.dynamicToolsFingerprint === "string"
|
||||
? parsed.dynamicToolsFingerprint
|
||||
: undefined,
|
||||
dynamicToolsContainDeferred:
|
||||
typeof parsed.dynamicToolsContainDeferred === "boolean"
|
||||
? parsed.dynamicToolsContainDeferred
|
||||
: undefined,
|
||||
userMcpServersFingerprint:
|
||||
typeof parsed.userMcpServersFingerprint === "string"
|
||||
? parsed.userMcpServersFingerprint
|
||||
@@ -170,6 +175,7 @@ export async function writeCodexAppServerBinding(
|
||||
sandbox: binding.sandbox,
|
||||
serviceTier: binding.serviceTier,
|
||||
dynamicToolsFingerprint: binding.dynamicToolsFingerprint,
|
||||
dynamicToolsContainDeferred: binding.dynamicToolsContainDeferred,
|
||||
userMcpServersFingerprint: binding.userMcpServersFingerprint,
|
||||
mcpServersFingerprint: binding.mcpServersFingerprint,
|
||||
nativeHookRelayGeneration: binding.nativeHookRelayGeneration,
|
||||
|
||||
@@ -63,6 +63,16 @@ function createNamedDynamicTool(
|
||||
};
|
||||
}
|
||||
|
||||
function createDeferredNamedDynamicTool(
|
||||
name: string,
|
||||
): Parameters<typeof startOrResumeThread>[0]["dynamicTools"][number] {
|
||||
return {
|
||||
...createNamedDynamicTool(name),
|
||||
namespace: "openclaw",
|
||||
deferLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
function createPluginAppConfigPatch() {
|
||||
return {
|
||||
apps: {
|
||||
@@ -169,6 +179,42 @@ function createTwoCalendarAppPolicyContext() {
|
||||
setupRunAttemptTestHooks();
|
||||
|
||||
describe("Codex app-server thread lifecycle bindings", () => {
|
||||
it("does not write a binding when thread start resolves after abort", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
const appServer = createThreadLifecycleAppServerOptions();
|
||||
const abortController = new AbortController();
|
||||
let resolveStart: ((value: ReturnType<typeof threadStartResult>) => void) | undefined;
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "thread/start") {
|
||||
return await new Promise<ReturnType<typeof threadStartResult>>((resolve) => {
|
||||
resolveStart = resolve;
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected method: ${method}`);
|
||||
});
|
||||
|
||||
const run = startOrResumeThread({
|
||||
client: { request } as never,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [],
|
||||
appServer,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
expect(request).toHaveBeenCalledWith("thread/start", expect.any(Object), {
|
||||
signal: abortController.signal,
|
||||
}),
|
||||
);
|
||||
abortController.abort("test_abort");
|
||||
resolveStart?.(threadStartResult("thread-after-abort"));
|
||||
|
||||
await expect(run).rejects.toThrow("test_abort");
|
||||
await expect(readCodexAppServerBinding(sessionFile)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("resumes a bound Codex thread when only dynamic tool descriptions change", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
@@ -207,6 +253,42 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/resume"]);
|
||||
});
|
||||
|
||||
it("starts a fresh Codex thread when dynamic tools switch from deferred to direct", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
const appServer = createThreadLifecycleAppServerOptions();
|
||||
let starts = 0;
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "thread/start") {
|
||||
starts += 1;
|
||||
return threadStartResult(`thread-${starts}`);
|
||||
}
|
||||
if (method === "thread/resume") {
|
||||
return threadStartResult("thread-existing");
|
||||
}
|
||||
throw new Error(`unexpected method: ${method}`);
|
||||
});
|
||||
|
||||
await startOrResumeThread({
|
||||
client: { request } as never,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createDeferredNamedDynamicTool("web_search")],
|
||||
appServer,
|
||||
});
|
||||
const binding = await startOrResumeThread({
|
||||
client: { request } as never,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createNamedDynamicTool("web_search")],
|
||||
appServer,
|
||||
});
|
||||
|
||||
expect(binding.threadId).toBe("thread-2");
|
||||
expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start", "thread/start"]);
|
||||
});
|
||||
|
||||
it("resumes a bound Codex thread when dynamic tools are reordered", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
@@ -453,7 +535,7 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
client: { request } as never,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createMessageDynamicTool("Send and manage messages.")],
|
||||
dynamicTools: [createDeferredNamedDynamicTool("message")],
|
||||
appServer,
|
||||
});
|
||||
const fingerprint = (await readCodexAppServerBinding(sessionFile))?.dynamicToolsFingerprint;
|
||||
@@ -468,12 +550,13 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
client: { request } as never,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createMessageDynamicTool("Send and manage messages.")],
|
||||
dynamicTools: [createDeferredNamedDynamicTool("message")],
|
||||
appServer,
|
||||
});
|
||||
|
||||
const binding = await readCodexAppServerBinding(sessionFile);
|
||||
expect(binding?.dynamicToolsFingerprint).toBe(fingerprint);
|
||||
expect(binding?.dynamicToolsContainDeferred).toBe(true);
|
||||
expect(binding?.threadId).toBe("thread-1");
|
||||
expect(request.mock.calls.map(([method]) => method)).toEqual([
|
||||
"thread/start",
|
||||
|
||||
@@ -21,6 +21,7 @@ function createAttemptParams(params: {
|
||||
bootstrapContextMode?: "full" | "lightweight";
|
||||
bootstrapContextRunKind?: "default" | "heartbeat" | "cron";
|
||||
images?: EmbeddedRunAttemptParams["images"];
|
||||
modelId?: string;
|
||||
}): EmbeddedRunAttemptParams {
|
||||
const authProfileProviders =
|
||||
params.authProfileProviders ??
|
||||
@@ -30,7 +31,7 @@ function createAttemptParams(params: {
|
||||
const authProfileType = params.authProfileType ?? "oauth";
|
||||
return {
|
||||
provider: params.provider,
|
||||
modelId: "gpt-5.4",
|
||||
modelId: params.modelId ?? "gpt-5.4",
|
||||
prompt: "test prompt",
|
||||
authProfileId: params.authProfileId,
|
||||
...(params.bootstrapContextMode ? { bootstrapContextMode: params.bootstrapContextMode } : {}),
|
||||
@@ -151,7 +152,7 @@ describe("Codex app-server native code mode config", () => {
|
||||
expect(instructions).not.toContain("Deferred searchable OpenClaw dynamic tools available");
|
||||
});
|
||||
|
||||
it("keeps durable dynamic tool fingerprints independent from presentation mode", () => {
|
||||
it("keeps durable dynamic tool fingerprints scoped to loading mode", () => {
|
||||
const inputSchema = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
@@ -177,7 +178,7 @@ describe("Codex app-server native code mode config", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
expect(searchableFingerprint).toBe(directFingerprint);
|
||||
expect(searchableFingerprint).not.toBe(directFingerprint);
|
||||
});
|
||||
|
||||
it("keeps OpenClaw skill catalogs out of developer instructions", () => {
|
||||
@@ -214,6 +215,25 @@ describe("Codex app-server native code mode config", () => {
|
||||
expect(request.personality).toBe("none");
|
||||
});
|
||||
|
||||
it("disables Codex tool-search features for nano models", () => {
|
||||
const request = buildThreadStartParams(
|
||||
createAttemptParams({ provider: "openai", modelId: "gpt-5.4-nano" }),
|
||||
{
|
||||
cwd: "/repo",
|
||||
dynamicTools: [],
|
||||
appServer: createAppServerOptions() as never,
|
||||
developerInstructions: "test instructions",
|
||||
},
|
||||
);
|
||||
|
||||
expect(request.config).toEqual({
|
||||
"features.code_mode": true,
|
||||
"features.code_mode_only": false,
|
||||
"features.apply_patch_streaming_events": true,
|
||||
"features.multi_agent": false,
|
||||
});
|
||||
});
|
||||
|
||||
it("removes Codex model personality on thread/resume", () => {
|
||||
const request = buildThreadResumeParams(createAttemptParams({ provider: "openai" }), {
|
||||
threadId: "thread-1",
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
resolveCodexContextEngineProjectionMaxChars,
|
||||
resolveCodexContextEngineProjectionReserveTokens,
|
||||
} from "./context-engine-projection.js";
|
||||
import { shouldDisableCodexToolSearchForModel } from "./dynamic-tool-profile.js";
|
||||
import { invalidInlineImageText, sanitizeInlineImageDataUrl } from "./image-payload-sanitizer.js";
|
||||
import {
|
||||
isCodexPluginThreadBindingStale,
|
||||
@@ -114,6 +115,10 @@ const CODEX_LIGHTWEIGHT_CONTEXT_THREAD_CONFIG: JsonObject = {
|
||||
project_doc_max_bytes: 0,
|
||||
};
|
||||
|
||||
const CODEX_TOOL_SEARCH_UNSUPPORTED_THREAD_CONFIG: JsonObject = {
|
||||
"features.multi_agent": false,
|
||||
};
|
||||
|
||||
type CodexThreadLifecycleTimingSpan = {
|
||||
name: string;
|
||||
durationMs: number;
|
||||
@@ -243,6 +248,7 @@ export async function startOrResumeThread(params: {
|
||||
environmentSelection?: CodexTurnEnvironmentParams[];
|
||||
pluginThreadConfig?: CodexPluginThreadConfigProvider;
|
||||
contextEngineProjection?: CodexContextEngineThreadBootstrapProjection;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CodexAppServerThreadLifecycleBinding> {
|
||||
// Thread lifecycle spans are useful when profiling startup churn, but normal
|
||||
// turns should not pay Date.now/span-array overhead while resuming threads.
|
||||
@@ -252,6 +258,9 @@ export async function startOrResumeThread(params: {
|
||||
const dynamicToolsFingerprint = lifecycleTiming.measureSync("fingerprint_dynamic_tools", () =>
|
||||
fingerprintDynamicTools(params.dynamicTools),
|
||||
);
|
||||
const dynamicToolsContainDeferred = params.dynamicTools.some(
|
||||
(tool) => tool.deferLoading === true,
|
||||
);
|
||||
const contextEngineBinding = lifecycleTiming.measureSync("context_engine_binding", () =>
|
||||
buildContextEngineBinding(params.params, params.contextEngineProjection),
|
||||
);
|
||||
@@ -275,6 +284,22 @@ export async function startOrResumeThread(params: {
|
||||
let preserveExistingBinding = false;
|
||||
let rotatedContextEngineBinding = false;
|
||||
let prebuiltPluginThreadConfig: CodexPluginThreadConfig | undefined;
|
||||
const throwIfAborted = () => {
|
||||
if (!params.signal?.aborted) {
|
||||
return;
|
||||
}
|
||||
const reason = params.signal.reason;
|
||||
if (reason instanceof Error) {
|
||||
throw reason;
|
||||
}
|
||||
const error = new Error(
|
||||
typeof reason === "string" && reason.length > 0
|
||||
? reason
|
||||
: "codex app-server thread lifecycle aborted",
|
||||
);
|
||||
error.name = "AbortError";
|
||||
throw error;
|
||||
};
|
||||
if (binding?.threadId && params.nativeCodeModeEnabled === false) {
|
||||
embeddedAgentLog.debug(
|
||||
"codex app-server native tool surface disabled for turn; starting transient thread",
|
||||
@@ -387,6 +412,23 @@ export async function startOrResumeThread(params: {
|
||||
await clearCodexAppServerBinding(params.params.sessionFile);
|
||||
binding = undefined;
|
||||
}
|
||||
if (binding?.threadId) {
|
||||
if (
|
||||
binding.dynamicToolsFingerprint &&
|
||||
params.dynamicTools.length > 0 &&
|
||||
binding.dynamicToolsContainDeferred !== dynamicToolsContainDeferred &&
|
||||
(binding.dynamicToolsContainDeferred !== undefined || !dynamicToolsContainDeferred)
|
||||
) {
|
||||
embeddedAgentLog.debug(
|
||||
"codex app-server dynamic tool loading changed; starting a new thread",
|
||||
{
|
||||
threadId: binding.threadId,
|
||||
},
|
||||
);
|
||||
await clearCodexAppServerBinding(params.params.sessionFile);
|
||||
binding = undefined;
|
||||
}
|
||||
}
|
||||
if (binding?.threadId) {
|
||||
// `/codex resume <thread>` writes a binding before the next turn can know
|
||||
// the dynamic tool catalog, so only invalidate fingerprints we actually have.
|
||||
@@ -446,9 +488,10 @@ export async function startOrResumeThread(params: {
|
||||
);
|
||||
const response = assertCodexThreadResumeResponse(
|
||||
await lifecycleTiming.measure("thread_resume_request", () =>
|
||||
params.client.request("thread/resume", resumeParams),
|
||||
params.client.request("thread/resume", resumeParams, { signal: params.signal }),
|
||||
),
|
||||
);
|
||||
throwIfAborted();
|
||||
const boundAuthProfileId = authProfileId;
|
||||
const fallbackModelProvider = resolveCodexAppServerModelProvider({
|
||||
provider: params.params.provider,
|
||||
@@ -471,6 +514,7 @@ export async function startOrResumeThread(params: {
|
||||
model: params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? fallbackModelProvider,
|
||||
dynamicToolsFingerprint,
|
||||
dynamicToolsContainDeferred,
|
||||
userMcpServersFingerprint,
|
||||
mcpServersFingerprint: nextMcpServersFingerprint,
|
||||
nativeHookRelayGeneration:
|
||||
@@ -515,6 +559,7 @@ export async function startOrResumeThread(params: {
|
||||
model: params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? fallbackModelProvider,
|
||||
dynamicToolsFingerprint,
|
||||
dynamicToolsContainDeferred,
|
||||
userMcpServersFingerprint,
|
||||
mcpServersFingerprint: nextMcpServersFingerprint,
|
||||
nativeHookRelayGeneration:
|
||||
@@ -570,7 +615,7 @@ export async function startOrResumeThread(params: {
|
||||
);
|
||||
const threadStartResponse = await lifecycleTiming.measure("thread_start_request", async () => {
|
||||
try {
|
||||
return await params.client.request("thread/start", startParams);
|
||||
return await params.client.request("thread/start", startParams, { signal: params.signal });
|
||||
} catch (error) {
|
||||
if (error instanceof CodexAppServerRpcError) {
|
||||
throw new CodexThreadStartRequestError(error);
|
||||
@@ -579,6 +624,7 @@ export async function startOrResumeThread(params: {
|
||||
}
|
||||
});
|
||||
const response = assertCodexThreadStartResponse(threadStartResponse);
|
||||
throwIfAborted();
|
||||
const modelProvider = resolveCodexAppServerModelProvider({
|
||||
provider: params.params.provider,
|
||||
authProfileId: params.params.authProfileId,
|
||||
@@ -600,6 +646,7 @@ export async function startOrResumeThread(params: {
|
||||
model: response.model ?? params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? modelProvider,
|
||||
dynamicToolsFingerprint,
|
||||
dynamicToolsContainDeferred,
|
||||
userMcpServersFingerprint,
|
||||
mcpServersFingerprint: nextMcpServersFingerprint,
|
||||
nativeHookRelayGeneration: finalConfigPatch.nativeHookRelayGeneration,
|
||||
@@ -645,6 +692,7 @@ export async function startOrResumeThread(params: {
|
||||
model: response.model ?? params.params.modelId,
|
||||
modelProvider: response.modelProvider ?? modelProvider,
|
||||
dynamicToolsFingerprint,
|
||||
dynamicToolsContainDeferred,
|
||||
userMcpServersFingerprint,
|
||||
mcpServersFingerprint: nextMcpServersFingerprint,
|
||||
nativeHookRelayGeneration: finalConfigPatch.nativeHookRelayGeneration,
|
||||
@@ -905,7 +953,14 @@ function buildCodexRuntimeThreadConfigForRun(
|
||||
config: JsonObject | undefined,
|
||||
options: { nativeCodeModeEnabled?: boolean; nativeCodeModeOnlyEnabled?: boolean } = {},
|
||||
): JsonObject {
|
||||
const runtimeConfig = buildCodexRuntimeThreadConfig(config, options);
|
||||
const baseConfig = buildCodexRuntimeThreadConfig(config, options);
|
||||
const runtimeConfig =
|
||||
mergeCodexThreadConfigs(
|
||||
baseConfig,
|
||||
shouldDisableCodexToolSearchForModel(params.modelId)
|
||||
? CODEX_TOOL_SEARCH_UNSUPPORTED_THREAD_CONFIG
|
||||
: undefined,
|
||||
) ?? baseConfig;
|
||||
if (params.bootstrapContextMode !== "lightweight") {
|
||||
return runtimeConfig;
|
||||
}
|
||||
@@ -1095,9 +1150,7 @@ function fingerprintDynamicToolSpec(tool: JsonValue): JsonValue {
|
||||
for (const [key, child] of Object.entries(tool).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
// Tool-search presentation can change per turn without changing the
|
||||
// durable app-server execution contract for an existing thread.
|
||||
if (key === "description" || key === "deferLoading" || key === "namespace") {
|
||||
if (key === "description") {
|
||||
continue;
|
||||
}
|
||||
stable[key] = stabilizeJsonValue(child);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/comfy-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw ComfyUI provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/copilot-proxy",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Copilot Proxy provider plugin",
|
||||
"type": "module",
|
||||
|
||||
4
extensions/copilot/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/copilot",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/copilot",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "1.0.0-beta.9"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "copilot",
|
||||
"name": "GitHub Copilot agent runtime",
|
||||
"description": "Registers the GitHub Copilot agent runtime.",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"activation": {
|
||||
"onStartup": false,
|
||||
"onAgentHarnesses": ["copilot"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/copilot",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw GitHub Copilot agent runtime plugin (registers a `github-copilot` AgentHarness backed by @github/copilot-sdk over JSON-RPC to the GitHub Copilot CLI)",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,10 +25,10 @@
|
||||
"minHostVersion": ">=2026.5.28"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1",
|
||||
"openclawVersion": "2026.6.2",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/deepgram-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Deepgram media-understanding provider",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/deepinfra-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw DeepInfra provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/deepseek-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw DeepSeek provider plugin",
|
||||
"type": "module",
|
||||
|
||||
4
extensions/diagnostics-otel/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-otel",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/diagnostics-otel",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "1.9.1",
|
||||
"@opentelemetry/api-logs": "0.218.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-otel",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw diagnostics OpenTelemetry exporter for metrics and traces.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -34,10 +34,10 @@
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1"
|
||||
"openclawVersion": "2026.6.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-prometheus",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/diagnostics-prometheus",
|
||||
"version": "2026.6.1"
|
||||
"version": "2026.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-prometheus",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw diagnostics Prometheus exporter for runtime metrics.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -21,10 +21,10 @@
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1"
|
||||
"openclawVersion": "2026.6.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/diffs-language-pack",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/diffs-language-pack",
|
||||
"version": "2026.6.1"
|
||||
"version": "2026.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diffs-language-pack",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw diffs viewer syntax highlighting language pack",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -22,13 +22,13 @@
|
||||
"minHostVersion": ">=2026.5.27"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"assetScripts": {
|
||||
"build": "node ../../scripts/build-diffs-viewer-runtime.mjs full"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1",
|
||||
"openclawVersion": "2026.6.2",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./assets/viewer-runtime.js",
|
||||
|
||||
4
extensions/diffs/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/diffs",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/diffs",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@pierre/diffs": "1.2.4",
|
||||
"@pierre/theme": "1.0.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diffs",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw read-only diff viewer plugin and file renderer for agents.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -29,13 +29,13 @@
|
||||
"minHostVersion": ">=2026.4.30"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"assetScripts": {
|
||||
"build": "node ../../scripts/build-diffs-viewer-runtime.mjs curated"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1",
|
||||
"openclawVersion": "2026.6.2",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./assets/viewer-runtime.js",
|
||||
|
||||
14
extensions/discord/npm-shrinkwrap.json
generated
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"name": "@openclaw/discord",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/discord",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@discordjs/voice": "0.19.2",
|
||||
"discord-api-types": "0.38.48",
|
||||
"libopus-wasm": "0.1.0",
|
||||
"libopus-wasm": "0.2.0",
|
||||
"typebox": "1.1.39",
|
||||
"undici": "8.3.0",
|
||||
"ws": "8.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.1"
|
||||
"openclaw": ">=2026.6.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -352,9 +352,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/libopus-wasm": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/libopus-wasm/-/libopus-wasm-0.1.0.tgz",
|
||||
"integrity": "sha512-/aurGcAVgy0GcBEUzFaX9pm9qv7zYcy8W5hBXFiK+cyqOXAX4lOS6rlFogkY9CcSIajhjnuXyixsbmziSHCDMQ==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/libopus-wasm/-/libopus-wasm-0.2.0.tgz",
|
||||
"integrity": "sha512-x/2Gu1/C6L3IICY09zyfp984AWiOYjn53u4WfdY3yh+3KTzMN8Xkm77q3lenWMVIk5SnSzjGEkQT+VQMFHLBHQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/discord",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw Discord channel plugin for channels, DMs, commands, and app events.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"@discordjs/voice": "0.19.2",
|
||||
"discord-api-types": "0.38.48",
|
||||
"libopus-wasm": "0.1.0",
|
||||
"libopus-wasm": "0.2.0",
|
||||
"typebox": "1.1.39",
|
||||
"undici": "8.3.0",
|
||||
"ws": "8.21.0"
|
||||
@@ -20,7 +20,7 @@
|
||||
"openclaw": "2026.5.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.1"
|
||||
"openclaw": ">=2026.6.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -67,10 +67,10 @@
|
||||
"allowInvalidConfigRecovery": true
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1"
|
||||
"openclawVersion": "2026.6.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1774,6 +1774,10 @@ export class DiscordVoiceManager {
|
||||
logVoiceVerbose(`receive stream ended: ${analysis.message}`);
|
||||
return;
|
||||
}
|
||||
if (analysis.isDecodeCorruption && !analysis.countsAsDecryptFailure) {
|
||||
logVoiceVerbose(`receive decode skipped: ${analysis.message}`);
|
||||
return;
|
||||
}
|
||||
logger.warn(`discord voice: receive error: ${analysis.message}`);
|
||||
if (analysis.shouldAttemptPassthrough) {
|
||||
this.enableDaveReceivePassthrough(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { OpusError, OpusErrorCode } from "libopus-wasm";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
analyzeVoiceReceiveError,
|
||||
@@ -15,6 +16,7 @@ describe("voice receive recovery", () => {
|
||||
).toEqual({
|
||||
message: "Failed to decrypt: DecryptionFailed(UnencryptedWhenPassthroughDisabled)",
|
||||
isAbortLike: false,
|
||||
isDecodeCorruption: false,
|
||||
shouldAttemptPassthrough: true,
|
||||
countsAsDecryptFailure: true,
|
||||
});
|
||||
@@ -24,15 +26,60 @@ describe("voice receive recovery", () => {
|
||||
expect(analyzeVoiceReceiveError(new Error("memory access out of bounds"))).toEqual({
|
||||
message: "memory access out of bounds",
|
||||
isAbortLike: false,
|
||||
isDecodeCorruption: false,
|
||||
shouldAttemptPassthrough: false,
|
||||
countsAsDecryptFailure: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("treats corrupt Opus packets as non-recoverable decode noise", () => {
|
||||
expect(
|
||||
analyzeVoiceReceiveError(
|
||||
new OpusError(OpusErrorCode.InvalidPacket, "not inspected", "decode"),
|
||||
),
|
||||
).toEqual({
|
||||
message: "not inspected",
|
||||
isAbortLike: false,
|
||||
isDecodeCorruption: true,
|
||||
shouldAttemptPassthrough: false,
|
||||
countsAsDecryptFailure: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("treats structurally equivalent Opus errors as decode corruption", () => {
|
||||
const analysis = analyzeVoiceReceiveError({
|
||||
name: "OpusError",
|
||||
message: "libopus decode failed (-4): corrupted stream",
|
||||
code: OpusErrorCode.InvalidPacket,
|
||||
codeName: "InvalidPacket",
|
||||
operation: "decode",
|
||||
});
|
||||
|
||||
expect(analysis).toMatchObject({
|
||||
isAbortLike: false,
|
||||
isDecodeCorruption: true,
|
||||
shouldAttemptPassthrough: false,
|
||||
countsAsDecryptFailure: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not classify corrupt Opus packet text without the Opus error contract", () => {
|
||||
expect(
|
||||
analyzeVoiceReceiveError(new Error("libopus decode failed (-4): corrupted stream")),
|
||||
).toEqual({
|
||||
message: "libopus decode failed (-4): corrupted stream",
|
||||
isAbortLike: false,
|
||||
isDecodeCorruption: false,
|
||||
shouldAttemptPassthrough: false,
|
||||
countsAsDecryptFailure: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("treats premature stream close as an expected receive end", () => {
|
||||
expect(analyzeVoiceReceiveError(new Error("Premature close"))).toEqual({
|
||||
message: "Premature close",
|
||||
isAbortLike: true,
|
||||
isDecodeCorruption: false,
|
||||
shouldAttemptPassthrough: false,
|
||||
countsAsDecryptFailure: false,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { OpusErrorCode, isOpusError } from "libopus-wasm";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
|
||||
const DECRYPT_FAILURE_WINDOW_MS = 30_000;
|
||||
@@ -18,6 +19,7 @@ export type VoiceReceiveRecoveryState = {
|
||||
type VoiceReceiveErrorAnalysis = {
|
||||
message: string;
|
||||
isAbortLike: boolean;
|
||||
isDecodeCorruption: boolean;
|
||||
shouldAttemptPassthrough: boolean;
|
||||
countsAsDecryptFailure: boolean;
|
||||
};
|
||||
@@ -80,13 +82,23 @@ function isAbortLikeReceiveError(err: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isOpusDecodeInvalidPacketError(err: unknown): boolean {
|
||||
return (
|
||||
isOpusError(err) &&
|
||||
err.code === OpusErrorCode.InvalidPacket &&
|
||||
(err.operation === "decode" || err.operation === "decodeFloat")
|
||||
);
|
||||
}
|
||||
|
||||
export function analyzeVoiceReceiveError(err: unknown): VoiceReceiveErrorAnalysis {
|
||||
const message = formatErrorMessage(err);
|
||||
const normalizedMessage = message.toLowerCase();
|
||||
const shouldAttemptPassthrough = message.includes(DAVE_PASSTHROUGH_DISABLED_MARKER);
|
||||
const isWasmMemoryAccessFailure = message.toLowerCase().includes(WASM_MEMORY_ACCESS_MARKER);
|
||||
const isWasmMemoryAccessFailure = normalizedMessage.includes(WASM_MEMORY_ACCESS_MARKER);
|
||||
return {
|
||||
message,
|
||||
isAbortLike: isAbortLikeReceiveError(err),
|
||||
isDecodeCorruption: isOpusDecodeInvalidPacketError(err),
|
||||
shouldAttemptPassthrough,
|
||||
countsAsDecryptFailure:
|
||||
message.includes(DECRYPT_FAILURE_MARKER) ||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/document-extract-plugin",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw local document extraction plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/duckduckgo-plugin",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw DuckDuckGo plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/elevenlabs-speech",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw ElevenLabs speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/exa-plugin",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Exa plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/fal-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw fal provider plugin",
|
||||
"type": "module",
|
||||
|
||||
6
extensions/feishu/npm-shrinkwrap.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "@openclaw/feishu",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/feishu",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"dependencies": {
|
||||
"@larksuiteoapi/node-sdk": "1.66.0",
|
||||
"typebox": "1.1.39",
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.1"
|
||||
"openclaw": ">=2026.6.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/feishu",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw Feishu/Lark channel plugin for chats and workplace tools (community maintained by @m1heng).",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -17,7 +17,7 @@
|
||||
"openclaw": "2026.5.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.1"
|
||||
"openclaw": ">=2026.6.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -51,10 +51,10 @@
|
||||
"minHostVersion": ">=2026.5.29"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.1"
|
||||
"pluginApi": ">=2026.6.2"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.1"
|
||||
"openclawVersion": "2026.6.2"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/file-transfer",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"description": "OpenClaw file transfer plugin (file_fetch, dir_list, dir_fetch, file_write)",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/firecrawl-plugin",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Firecrawl plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/fireworks-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw Fireworks provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/github-copilot-provider",
|
||||
"version": "2026.6.1",
|
||||
"version": "2026.6.2",
|
||||
"private": true,
|
||||
"description": "OpenClaw GitHub Copilot provider plugin",
|
||||
"type": "module",
|
||||
|
||||