mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-15 02:28:52 +08:00
Compare commits
58 Commits
codex/secu
...
v2026.6.5-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1514dc740a | ||
|
|
53fcfb5061 | ||
|
|
5f42bfb4d9 | ||
|
|
db711701d2 | ||
|
|
cd50c563c2 | ||
|
|
d71d3d541c | ||
|
|
a2f95d5b38 | ||
|
|
0717cd51bb | ||
|
|
10f903de0d | ||
|
|
522c653fee | ||
|
|
878dda47cd | ||
|
|
0f888b842d | ||
|
|
84e2286b87 | ||
|
|
55e4c0feda | ||
|
|
0075304b2c | ||
|
|
9075912386 | ||
|
|
ff0b21e950 | ||
|
|
b85a13d97d | ||
|
|
965230a417 | ||
|
|
537301c978 | ||
|
|
2e85b58581 | ||
|
|
eeb9b60d0c | ||
|
|
8689417638 | ||
|
|
fb7d2fe128 | ||
|
|
9691ddcab1 | ||
|
|
597bd1684c | ||
|
|
cda413b97a | ||
|
|
e81b643699 | ||
|
|
332df4c4db | ||
|
|
7c5f00d5d3 | ||
|
|
02f3890a1a | ||
|
|
40fa8ed829 | ||
|
|
976e4bfa25 | ||
|
|
678c9cecf8 | ||
|
|
b628d3d534 | ||
|
|
1e28f4845a | ||
|
|
57f5bf25b8 | ||
|
|
c98f49b474 | ||
|
|
0990d98ede | ||
|
|
b15bc581cb | ||
|
|
08048d5f23 | ||
|
|
ad169bbecd | ||
|
|
eea34ca044 | ||
|
|
bd81d3e712 | ||
|
|
579c2b7d34 | ||
|
|
56b63f6ec8 | ||
|
|
07fa4c7095 | ||
|
|
676f41beae | ||
|
|
32f35c58cf | ||
|
|
c62a997446 | ||
|
|
82748a4c97 | ||
|
|
5123af94bd | ||
|
|
2d08823e24 | ||
|
|
8ce8095cd1 | ||
|
|
76b4ef2170 | ||
|
|
53e3bbd7b6 | ||
|
|
2254e4d085 | ||
|
|
b813153e3f |
57
.github/workflows/docker-release.yml
vendored
57
.github/workflows/docker-release.yml
vendored
@@ -88,6 +88,25 @@ jobs:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Pre-pull BuildKit image
|
||||
shell: bash
|
||||
env:
|
||||
BUILDKIT_IMAGE: moby/buildkit:buildx-stable-1
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3 4; do
|
||||
if docker pull "${BUILDKIT_IMAGE}"; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${attempt}" == "4" ]]; then
|
||||
echo "::error::Failed to pull ${BUILDKIT_IMAGE} after ${attempt} attempts"
|
||||
exit 1
|
||||
fi
|
||||
sleep_seconds=$((attempt * 10))
|
||||
echo "BuildKit image pull failed; retrying in ${sleep_seconds}s (${attempt}/4)."
|
||||
sleep "${sleep_seconds}"
|
||||
done
|
||||
|
||||
- name: Set up Docker Builder
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
||||
|
||||
@@ -279,6 +298,25 @@ jobs:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Pre-pull BuildKit image
|
||||
shell: bash
|
||||
env:
|
||||
BUILDKIT_IMAGE: moby/buildkit:buildx-stable-1
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3 4; do
|
||||
if docker pull "${BUILDKIT_IMAGE}"; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${attempt}" == "4" ]]; then
|
||||
echo "::error::Failed to pull ${BUILDKIT_IMAGE} after ${attempt} attempts"
|
||||
exit 1
|
||||
fi
|
||||
sleep_seconds=$((attempt * 10))
|
||||
echo "BuildKit image pull failed; retrying in ${sleep_seconds}s (${attempt}/4)."
|
||||
sleep "${sleep_seconds}"
|
||||
done
|
||||
|
||||
- name: Set up Docker Builder
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
||||
|
||||
@@ -561,6 +599,25 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Pre-pull BuildKit image
|
||||
shell: bash
|
||||
env:
|
||||
BUILDKIT_IMAGE: moby/buildkit:buildx-stable-1
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in 1 2 3 4; do
|
||||
if docker pull "${BUILDKIT_IMAGE}"; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${attempt}" == "4" ]]; then
|
||||
echo "::error::Failed to pull ${BUILDKIT_IMAGE} after ${attempt} attempts"
|
||||
exit 1
|
||||
fi
|
||||
sleep_seconds=$((attempt * 10))
|
||||
echo "BuildKit image pull failed; retrying in ${sleep_seconds}s (${attempt}/4)."
|
||||
sleep "${sleep_seconds}"
|
||||
done
|
||||
|
||||
- name: Set up Docker Builder
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
||||
|
||||
|
||||
1
.github/workflows/install-smoke.yml
vendored
1
.github/workflows/install-smoke.yml
vendored
@@ -296,6 +296,7 @@ jobs:
|
||||
|
||||
- name: Run QR package install smoke
|
||||
env:
|
||||
OPENCLAW_DOCKER_E2E_CPUS: "4"
|
||||
OPENCLAW_QR_SMOKE_FORCE_INSTALL: "1"
|
||||
run: bash scripts/e2e/qr-import-docker.sh
|
||||
|
||||
|
||||
@@ -1503,31 +1503,66 @@ jobs:
|
||||
|
||||
- name: Build and push bare Docker E2E image
|
||||
if: steps.plan.outputs.needs_bare_image == '1' && steps.image_exists.outputs.bare_exists != '1'
|
||||
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
|
||||
with:
|
||||
context: .
|
||||
file: ./scripts/e2e/Dockerfile
|
||||
target: bare
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.image.outputs.bare_image }}
|
||||
sbom: true
|
||||
provenance: mode=max
|
||||
push: true
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE_REF: ${{ steps.image.outputs.bare_image }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
build_cmd=(
|
||||
docker buildx build
|
||||
--file ./scripts/e2e/Dockerfile
|
||||
--target bare
|
||||
--platform linux/amd64
|
||||
--tag "$IMAGE_REF"
|
||||
--sbom=true
|
||||
--provenance=mode=max
|
||||
--push
|
||||
.
|
||||
)
|
||||
for attempt in 1 2 3 4; do
|
||||
if "${build_cmd[@]}"; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$attempt" == "4" ]]; then
|
||||
echo "::error::Failed to build Docker E2E bare image after ${attempt} attempts"
|
||||
exit 1
|
||||
fi
|
||||
sleep_seconds=$((attempt * 20))
|
||||
echo "Docker E2E bare image build failed; retrying in ${sleep_seconds}s (${attempt}/4)."
|
||||
sleep "$sleep_seconds"
|
||||
done
|
||||
|
||||
- name: Build and push functional Docker E2E image
|
||||
if: steps.plan.outputs.needs_functional_image == '1' && steps.image_exists.outputs.functional_exists != '1'
|
||||
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
|
||||
with:
|
||||
context: .
|
||||
file: ./scripts/e2e/Dockerfile
|
||||
target: functional
|
||||
build-contexts: |
|
||||
openclaw_package=.artifacts/docker-e2e-package
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.image.outputs.functional_image }}
|
||||
sbom: true
|
||||
provenance: mode=max
|
||||
push: true
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE_REF: ${{ steps.image.outputs.functional_image }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
build_cmd=(
|
||||
docker buildx build
|
||||
--file ./scripts/e2e/Dockerfile
|
||||
--target functional
|
||||
--build-context openclaw_package=.artifacts/docker-e2e-package
|
||||
--platform linux/amd64
|
||||
--tag "$IMAGE_REF"
|
||||
--sbom=true
|
||||
--provenance=mode=max
|
||||
--push
|
||||
.
|
||||
)
|
||||
for attempt in 1 2 3 4; do
|
||||
if "${build_cmd[@]}"; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$attempt" == "4" ]]; then
|
||||
echo "::error::Failed to build Docker E2E functional image after ${attempt} attempts"
|
||||
exit 1
|
||||
fi
|
||||
sleep_seconds=$((attempt * 20))
|
||||
echo "Docker E2E functional image build failed; retrying in ${sleep_seconds}s (${attempt}/4)."
|
||||
sleep "$sleep_seconds"
|
||||
done
|
||||
|
||||
prepare_live_test_image:
|
||||
needs: validate_selected_ref
|
||||
|
||||
12
.github/workflows/plugin-clawhub-release.yml
vendored
12
.github/workflows/plugin-clawhub-release.yml
vendored
@@ -56,12 +56,6 @@ jobs:
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "false"
|
||||
|
||||
- name: Resolve checked-out ref
|
||||
id: ref
|
||||
env:
|
||||
@@ -107,6 +101,12 @@ jobs:
|
||||
echo "Plugin ClawHub publishes must target a commit reachable from main, release/*, or the matching Tideclaw alpha branch." >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "false"
|
||||
|
||||
- name: Validate publishable plugin metadata
|
||||
env:
|
||||
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Google Vertex ADC users get static catalog rows and runtime model resolution again, while single-provider cooldown recovery and memory adapter status checks are more reliable. (#90506, #90609, #90717, #90816) Thanks @849261680.
|
||||
- Matrix can preflight voice notes before mention gating, preserve thread reads/replies through Matrix relations pagination, and carry QA coverage for voice and thread flows. (#78016, #90415)
|
||||
- Auth and plugin install state is more durable: auth profiles now live in SQLite, official npm plugin install records keep their trusted pins, and prerelease fallback integrity checks avoid carrying stale integrity forward. (#89102, #88585)
|
||||
- Agent, tool, and provider loops are stricter around MCP lease timestamps, prompt-cache tool names, local tool catalogs, unreadable dynamic tools, owner-only HTTP tools, and provider catalog metadata, reducing hidden retries and unsafe exposure. (#91124, #91233, #90022, #90261)
|
||||
- macOS node mode no longer silently self-reconnects away from a healthy direct Gateway session, reducing unexpected companion app session churn. (#90668, #90815) Thanks @vrurg.
|
||||
- Upgrade and service paths are safer: cron legacy JSON stores migrate during doctor preflight, service env placeholders no longer mask state-dir secrets, WhatsApp startup waits are bounded, and disabled WhatsApp accounts tear down on config reload. (#90072, #90208, #90277, #90488, #90486, #87951, #87965) Thanks @MonkeyLeeT, @sallyom, @mcaxtr, and @MukundaKatta.
|
||||
|
||||
@@ -21,11 +22,14 @@ Docs: https://docs.openclaw.ai
|
||||
- Search/providers: add the Parallel bundled web-search plugin, live provider tests, registration contracts, onboarding/docs wiring, and guarded `api.parallel.ai/v1/search` support. (#85158) Thanks @NormallyGaussian.
|
||||
- Matrix/channels: add voice-message preflight and thread-aware read/reply behavior, including Matrix QA scenario wiring and docs for voice-message behavior. (#78016, #90415)
|
||||
- Skills/ClawHub: install ClawHub skills backed by GitHub repositories through the resolved install API, download the pinned GitHub commit, keep install-policy checks, and report install telemetry after success. (#90478) Thanks @Patrick-Erichsen.
|
||||
- Skills/ClawHub: avoid one filesystem watcher per skill file during refresh, keeping large skill trees from exhausting watcher limits.
|
||||
- Google Chat/channels: add native approval card actions and click handling so Google Chat approvals use platform-native cards instead of generic message flow.
|
||||
- Mobile: Android provider/model screens now surface expiring, unavailable, unresolved, and attention states more clearly, while iOS settings and Talk tabs keep diagnostics, gateway rows, attachment labels, and unavailable Talk controls reachable.
|
||||
- Mobile: Android provider/model screens now surface expiring, unavailable, unresolved, and attention states more clearly, Android adds theme mode selection, and iOS settings and Talk tabs keep diagnostics, gateway rows, attachment labels, fallback copy, and unavailable Talk controls reachable. (#90752, #91201)
|
||||
- Memory: QMD search can use the new rerank toggle, and memory adapter status uses the resolved default model identity when checking plain status. (#61834)
|
||||
- Docs/tooling: add Parallel search docs, refresh weather-skill guidance toward `web_fetch`, clarify legacy `openai-codex` auth, document release/test helper scripts, and tighten changed-test routing docs for CI/debugging work. (#90028, #90250) Thanks @fuller-stack-dev.
|
||||
- Release/process: switch release trains to `YYYY.M.PATCH` monthly patch numbering, keep pre-transition tags compatible, and pin the June 2026 floor at `2026.6.5` after the published beta.
|
||||
- Release/process: defer the session-metadata SQLite migration from the `2026.6.5` beta train so this release keeps the existing JSON-backed session metadata path while the migration risk is worked on `main`.
|
||||
- Release metadata: align OpenClaw, publishable plugin manifests, generated shrinkwraps, app version metadata, iOS release notes, Matrix plugin changelog, and generated release baselines with the `2026.6.5` beta train.
|
||||
- Platform maintenance: refresh Android, Swift/macOS, Docker, CodeQL, Buildx, Docker build/push, and Codex Action dependencies for this release train. (#74980, #81757, #86481, #86483, #90601)
|
||||
|
||||
### Fixes
|
||||
@@ -33,19 +37,39 @@ Docs: https://docs.openclaw.ai
|
||||
- Channel content boundaries: QQBot now strips reasoning/thinking tags before sending, preserving final answers while hiding internal model narration from users. (#89913, #90132) Thanks @openperf.
|
||||
- Agents/MCP/providers: coerce non-text/image MCP tool-result blocks before they reach provider converters, preserving valid images and turning richer MCP content into text instead of malformed image blocks. (#90710, #90728) Thanks @RanSHammer and @849261680.
|
||||
- Anthropic/Codex/ACP/agent recovery: defer Anthropic stream start events until `message_start`, strip stale compaction thinking signatures before Anthropic replay, detect unsigned thinking-only stalls, refresh prompt fences after compaction writes, reject empty completion handoffs, preserve parent streaming-off overrides/shared progress commentary, forward heartbeat metadata to context-engine hooks, and cover Codex session/thread migration edge cases. (#90667, #90697, #90163, #90108, #89874, #89505, #90632, #89302, #90729, #90317, #90319) Thanks @openperf, @100yenadmin, and @ooiuuii.
|
||||
- Agents/Codex/tools: MCP lease release no longer refreshes `lastUsedAt`, prompt-cache tool names are guarded, lean local tool catalogs stay compact, unreadable dynamic tools are quarantined, orphan tool errors still surface, native subagent completion results survive app-server monitoring, and background-session name derivation avoids regex backtracking risk. (#91124, #90612, #90022, #91235, #91233)
|
||||
- Provider/model resolution: preserve Google Vertex ADC auth markers in generated catalogs, re-probe a single-provider primary after cooldown, share Codex model visibility, fail closed for unknown model auth, preserve Codex alias availability, keep unresolved profile refs unknown, and avoid resolving auth while listing models. (#90506, #90609, #90717, #90702) Thanks @849261680.
|
||||
- Provider/model resolution: live provider model catalogs keep helper coverage, Ollama catalog metadata is preserved, Google provider prefixes are stripped from Gemini paths, Foundry Responses reasoning replay ids survive, MiniMax M3 thinking stays enabled, Vertex multi-region calls use the right regional host, and OpenRouter streamed generation cost is reconciled. (#91125)
|
||||
- Gateway/macOS/mobile: avoid duplicate Gateway probe warnings by identity, rate-limit node pairing requests while preserving paired-node reconnects, keep macOS node mode on a healthy direct Gateway session, keep iOS diagnostics and gateway rows reachable, and avoid Linux ARM Gradle resource tasks during Android builds. (#85791, #90147, #90668, #90815) Thanks @giodl73-repo and @vrurg.
|
||||
- Gateway/security/config: owner-only HTTP tools are gated, sandbox skills remain readable in writable sandboxes, legacy agent registry and Codex model metadata migrate safely, and stalled MCP response bodies time out instead of tying up Gateway workers. (#90261)
|
||||
- Gateway/config: `config.patch` now preserves explicit array replacement semantics for arrays without merge keys, so replacement patches do not accidentally merge stale entries. (#91551)
|
||||
- SDK: event pump failures now surface to clients instead of being swallowed behind a quiet iterator shutdown.
|
||||
- Agents/transcripts: inline image payload redaction now catches data URLs and repaired transcript images before they can leak raw image bytes into stored or exported transcripts. (#91529)
|
||||
- Plugins/Gateway: legacy flat Control UI descriptors from shipped JavaScript plugins now normalize `name` and missing surface fields into session descriptors, restoring Kitchen Sink RPC descriptor proof for package-backed plugin validation.
|
||||
- TUI/chat/Workboard/auto-reply: optimistic user messages stay stable across stale history reloads, runId reassignment, and abort windows instead of disappearing, jumping, or lingering as ghost rows; Workboard stale lifecycle bulk updates no longer overwrite newer status/provenance; message-tool sends now count as delivery. (#86205, #89600, #88592, #90123) Thanks @RomneyDa.
|
||||
- Cron/update/service env: doctor config preflight now migrates legacy cron JSON stores into SQLite before runtime reads, service env planning skips unresolved placeholders that would mask state-dir `.env` values, and session transcript rewrites keep registry markers/discriminants consistent. (#90072, #90208, #90277, #90488) Thanks @MonkeyLeeT and @sallyom.
|
||||
- Cron/update/service env: doctor config preflight now migrates legacy cron JSON stores into SQLite before runtime reads, isolated agent turn payload messages preserve timeout context, service env planning skips unresolved placeholders that would mask state-dir `.env` values, and session transcript rewrites keep registry markers/discriminants consistent. (#90072, #90208, #91230, #90277, #90488) Thanks @MonkeyLeeT and @sallyom.
|
||||
- State/storage: Matrix sync and crypto sidecars, memory-wiki import/source-sync state, sandbox registry state, ACPX process state, device-pair notify state, Zalo hosted media, and plugin SDK dedupe state now use SQLite-owned storage instead of ad hoc runtime files. (#91100, #91108, #91056)
|
||||
- Security/config/tooling: guard MCP HTTP redirects, protect global agent config defaults, and keep release/test/tooling proof failures bounded and explicit. (#89732, #90145)
|
||||
- Channels: WhatsApp restarts when per-account config changes, bounds background startup waits, closes failed sockets, and preserves reconnect behavior; Mattermost slash commands keep their state on `globalThis`; Feishu streaming cards preserve full merged content; voice-call tracks Twilio streams after connect; ClickClack reply tools respect `toolsAllow`. (#87951, #87965, #90486, #68113, #90534, #90181, #90607, #89500) Thanks @MukundaKatta, @mcaxtr, @infoanton, @mushuiyu886, and @sahibzada-allahyar.
|
||||
- Channels: WhatsApp restarts when per-account config changes, bounds background startup waits, closes failed sockets, and preserves reconnect behavior; Mattermost slash commands keep their state on `globalThis`; Feishu streaming cards preserve full merged content; iMessage private-API failures and send timeouts explain themselves while split-send coalescing honors balloon metadata; voice-call tracks Twilio streams after connect; ClickClack reply tools respect `toolsAllow`; Discord runtime adapters stay resolvable; and outbound delivery retries survive budget deferrals. (#87951, #87965, #90486, #68113, #90534, #90181, #90607, #89500, #91041, #90858, #91119, #91241) Thanks @MukundaKatta, @mcaxtr, @infoanton, @mushuiyu886, and @sahibzada-allahyar.
|
||||
- Feishu: retry transient send rate-limit errors (HTTP 429, per-chat code 230020, tenant-level code 11232) with linear backoff, including SDK responses that fulfill with rate-limit bodies instead of throwing, and route streaming-card sends through the retry wrapper. (#89659) Thanks @ladygege.
|
||||
- WhatsApp: captured replies after restart now route through the successor controller instead of the stale pre-restart controller. (#85823)
|
||||
- Release/CI/E2E: main CI guard drift, PR merge diff scoping, live Docker credential staging, base-image qualification, installer Docker classification, Playwright dependency install recovery, API-key auth for Codex live Docker lanes, Parallels option terminators, and JSON-mode progress handling are tighter so release proof fails cleaner. (#90532, #90287, #90058) Thanks @RomneyDa, @hxy91819, and @mrunalp.
|
||||
- Release/CI/E2E: installed-package root dist verification now allows the current package's JavaScript file count while keeping dependency, per-file-size, and scan-bound checks active.
|
||||
- Release/CI/E2E: Chutes OAuth model-discovery proof now accepts standard `Headers` requests, and QR package install smoke caps Docker CPU requests to the hosted runner capacity so beta validation fails on real package regressions.
|
||||
- Release/CI/E2E: Docker E2E and live Docker harness runs now apply default memory, CPU, and process ceilings while preserving explicit per-lane overrides.
|
||||
- Release/CI/E2E: Docker E2E CPU limits now cap to the runner capacity, keeping package Telegram acceptance on hosted 8-vCPU runners focused on package regressions instead of impossible Docker resource requests.
|
||||
- Release/CI/E2E: task maintenance release checks now reset pinned config around isolated temp state dirs, keeping normal CI focused on the active session-store fixture instead of stale process snapshots.
|
||||
- Release/CI/E2E: plugin lifecycle matrix resource sampling now fails phases that exceed RSS, wall-clock, or CPU ceilings instead of only logging the measurements.
|
||||
- Release/CI/E2E: Codex npm plugin live assertions now cap transcript discovery and diagnostic log reads so failure proof stays bounded.
|
||||
- Release/CI/E2E: browser snapshot, release-scenario, release-user-journey, Telegram desktop/RTT/package, web-search, Parallels update, plugin update, doctor switch, and upgrade-survivor diagnostics now stream or bound log/artifact reads so failed proof stays inspectable without unbounded output.
|
||||
- Release/CI/E2E: ClawHub publish jobs prepare dependencies after checking out the target ref, and Docker store seed package discovery now targets the intended production packages. (#91547)
|
||||
- Release/CI/E2E: QA Lab capability-flip release validation now marks intentional `tools.deny` restores as array replacements, so beta validation fails only on real capability regressions.
|
||||
- Tests/state isolation: QA Lab valid-tool-call metrics now require runtime tool-call evidence when runtime parity data is available instead of counting tool-backed scenario pass status alone.
|
||||
- Tests/state isolation: QA Lab runtime parity now fails planned-only tool-call rows without matching tool results instead of treating matching mock plans as real tool evidence.
|
||||
- Tests/state isolation: QA Lab runtime parity now treats matching controlled tool errors as equivalent and falls back to transcript tool results when mock debug rows miss async image-generation starts.
|
||||
- Tests/state isolation: QA suites now fail closed on skipped summaries, missing runtime tool proof, planned-only rows, loose release limits, missing live/provider artifacts, failed agent reply markers, and package Telegram summary failures.
|
||||
- Tests/state isolation: provider, media, auth, cron, task, session, sandbox, Gateway, and Codex timeout fixtures now scope more home/state/env data per test, reducing cross-test leakage and making release validation failures less noisy. (#90027, #89974)
|
||||
- Sessions: the beta SQLite downgrade rescue now skips extra pre-reads for active non-empty JSON session stores, preserving cache race detection while still restoring missing or empty beta session files.
|
||||
|
||||
## 2026.6.2
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026060201
|
||||
versionName = "2026.6.2"
|
||||
versionCode = 2026060501
|
||||
versionName = "2026.6.5"
|
||||
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.5 - 2026-06-05
|
||||
|
||||
Maintenance update for the current OpenClaw beta release.
|
||||
|
||||
## 2026.6.2 - 2026-06-02
|
||||
|
||||
OpenClaw is now available on iPhone.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.6.2
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.2
|
||||
OPENCLAW_IOS_VERSION = 2026.6.5
|
||||
OPENCLAW_MARKETING_VERSION = 2026.6.5
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -329,6 +329,13 @@ struct AgentConfigLite: Decodable {
|
||||
struct ConfigPatchParams: Encodable {
|
||||
let raw: String
|
||||
let baseHash: String
|
||||
let replacePaths: [String]?
|
||||
|
||||
init(raw: String, baseHash: String, replacePaths: [String]? = nil) {
|
||||
self.raw = raw
|
||||
self.baseHash = baseHash
|
||||
self.replacePaths = replacePaths
|
||||
}
|
||||
}
|
||||
|
||||
enum SkillMutationError: LocalizedError {
|
||||
|
||||
@@ -621,7 +621,10 @@ extension AgentProTab {
|
||||
}
|
||||
|
||||
let raw = try Self.agentSkillsPatchRaw(agentId: self.activeAgentID, skills: skills)
|
||||
let params = ConfigPatchParams(raw: raw, baseHash: baseHash)
|
||||
let params = ConfigPatchParams(
|
||||
raw: raw,
|
||||
baseHash: baseHash,
|
||||
replacePaths: ["agents.list[].skills"])
|
||||
let data = try JSONEncoder().encode(params)
|
||||
guard let json = String(data: data, encoding: .utf8) else {
|
||||
throw SkillMutationError.invalidPatchPayload
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
OpenClaw is now available on iPhone.
|
||||
|
||||
Connect to your OpenClaw Gateway to chat with your assistant, use realtime Talk mode, review approvals, share content from iOS, and bring device capabilities like camera, location, screen, and notifications into your private automation workflows.
|
||||
Maintenance update for the current OpenClaw beta release.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.6.2"
|
||||
"version": "2026.6.5"
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.6.2</string>
|
||||
<string>2026.6.5</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026060200</string>
|
||||
<string>2026060500</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -2769,6 +2769,7 @@ public struct ConfigPatchParams: Codable, Sendable {
|
||||
public let deliverycontext: [String: AnyCodable]?
|
||||
public let note: String?
|
||||
public let restartdelayms: Int?
|
||||
public let replacepaths: [String]?
|
||||
|
||||
public init(
|
||||
raw: String,
|
||||
@@ -2776,7 +2777,8 @@ public struct ConfigPatchParams: Codable, Sendable {
|
||||
sessionkey: String?,
|
||||
deliverycontext: [String: AnyCodable]?,
|
||||
note: String?,
|
||||
restartdelayms: Int?)
|
||||
restartdelayms: Int?,
|
||||
replacepaths: [String]?)
|
||||
{
|
||||
self.raw = raw
|
||||
self.basehash = basehash
|
||||
@@ -2784,6 +2786,7 @@ public struct ConfigPatchParams: Codable, Sendable {
|
||||
self.deliverycontext = deliverycontext
|
||||
self.note = note
|
||||
self.restartdelayms = restartdelayms
|
||||
self.replacepaths = replacepaths
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
@@ -2793,6 +2796,7 @@ public struct ConfigPatchParams: Codable, Sendable {
|
||||
case deliverycontext = "deliveryContext"
|
||||
case note
|
||||
case restartdelayms = "restartDelayMs"
|
||||
case replacepaths = "replacePaths"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
a5a97a8b484acd13e68604037c8d8f448699700103c6ea2186f5914ad35a0623 config-baseline.json
|
||||
b0d668dbd794d2f54738152a4bcfd2a306c7954901e78d4dfbde7545a8301ce5 config-baseline.core.json
|
||||
0637c9bdcb9517f56049dd786563366877458d35df575328a6b80a890c8bc915 config-baseline.channel.json
|
||||
37b56008790612b8293930b6a29d74490e98daa90f954fca9d133fcc28645c4c config-baseline.json
|
||||
75b64c2ea081369ba4306493313a8a4cd48b784145f92fed995e6b77a5df350d config-baseline.core.json
|
||||
17d64c9799dfa239a49493413f1100bdd9237e9b67aaeae331a4604dbc227023 config-baseline.channel.json
|
||||
f9d1f50bfa8403891e76cd99dc1357cdece4a71e8ae18a39b190c2a14e6f97b0 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
8be695e0892078773d78dae5f4c5a20ee57f30c5df227be6a89cc316fc4b4e10 plugin-sdk-api-baseline.json
|
||||
45944cb6fa30a094c4f104d19eec7afc5822be05c88bbd2aa4a8b93a7cba9de8 plugin-sdk-api-baseline.jsonl
|
||||
c4fab2657cefb89e0a5e48cb041c5013fce1635127c74e0008be28d03ce33a45 plugin-sdk-api-baseline.json
|
||||
6f660af3062f00c8c5ac3f4566acaffccc19a168a56538aef9d3192b7539085f plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -601,7 +601,8 @@ For tooling that writes config over the gateway API, prefer this flow:
|
||||
summaries)
|
||||
- `config.get` to fetch the current snapshot plus `hash`
|
||||
- `config.patch` for partial updates (JSON merge patch: objects merge, `null`
|
||||
deletes, arrays replace)
|
||||
deletes, arrays replace when explicitly confirmed with `replacePaths` if
|
||||
entries would be removed)
|
||||
- `config.apply` only when you intend to replace the entire config
|
||||
- `update.run` for explicit self-update plus restart; include `continuationMessage` when the post-restart session should run one follow-up turn
|
||||
- `update.status` to inspect the latest update restart sentinel and verify the running version after a restart
|
||||
@@ -633,6 +634,14 @@ Both `config.apply` and `config.patch` accept `raw`, `baseHash`, `sessionKey`,
|
||||
`note`, and `restartDelayMs`. `baseHash` is required for both methods when a
|
||||
config already exists.
|
||||
|
||||
`config.patch` also accepts `replacePaths`, an array of config paths whose array
|
||||
replacement is intentional. If a patch would replace or delete an existing array
|
||||
with fewer entries, the Gateway rejects the write unless that exact path appears
|
||||
in `replacePaths`; nested arrays under array entries use `[]`, such as
|
||||
`agents.list[].skills`. This prevents truncated `config.get` snapshots from
|
||||
silently clobbering routing or allowlist arrays. Use `config.apply` when you
|
||||
intend to replace the full config.
|
||||
|
||||
## Environment variables
|
||||
|
||||
OpenClaw reads env vars from the parent process plus:
|
||||
|
||||
@@ -405,7 +405,9 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- `secrets.resolve` resolves command-target secret assignments for a specific command/target set.
|
||||
- `config.get` returns the current config snapshot and hash.
|
||||
- `config.set` writes a validated config payload.
|
||||
- `config.patch` merges a partial config update.
|
||||
- `config.patch` merges a partial config update. Destructive array
|
||||
replacement requires the affected path in `replacePaths`; nested arrays
|
||||
under array entries use `[]` paths such as `agents.list[].skills`.
|
||||
- `config.apply` validates + replaces the full config payload.
|
||||
- `config.schema` returns the live config schema payload used by Control UI and CLI tooling: schema, `uiHints`, version, and generation metadata, including plugin + channel schema metadata when the runtime can load it. The schema includes field `title` / `description` metadata derived from the same labels and help text used by the UI, including nested object, wildcard, array-item, and `anyOf` / `oneOf` / `allOf` composition branches when matching field documentation exists.
|
||||
- `config.schema.lookup` returns a path-scoped lookup payload for one config path: normalized path, a shallow schema node, matched hint + `hintPath`, optional `reloadKind`, and immediate child summaries for UI/CLI drill-down. `reloadKind` is one of `restart`, `hot`, or `none` and mirrors the Gateway config reload planner for the requested path. Lookup schema nodes keep the user-facing docs and common validation fields (`title`, `description`, `type`, `enum`, `const`, `format`, `pattern`, numeric/string/array/object bounds, and flags like `additionalProperties`, `deprecated`, `readOnly`, `writeOnly`). Child summaries expose `key`, normalized `path`, `type`, `required`, `hasChildren`, optional `reloadKind`, plus the matched `hint` / `hintPath`.
|
||||
|
||||
@@ -246,7 +246,7 @@ usage endpoint failed or returned no usable usage data.
|
||||
| `plugin-sdk/reply-history` | Shared short-window reply-history helpers. New message-turn code should use `createChannelHistoryWindow`; lower-level map helpers remain deprecated compatibility exports only |
|
||||
| `plugin-sdk/reply-reference` | `createReplyReferencePlanner` |
|
||||
| `plugin-sdk/reply-chunking` | Narrow text/markdown chunking helpers |
|
||||
| `plugin-sdk/session-store-runtime` | Session workflow helpers (`getSessionEntry`, `listSessionEntries`, `patchSessionEntry`, `upsertSessionEntry`), target discovery, legacy session store path/session-key helpers, updated-at reads, and deprecated whole-store mutation helpers |
|
||||
| `plugin-sdk/session-store-runtime` | Session workflow helpers (`getSessionEntry`, `listSessionEntries`, `patchSessionEntry`, `upsertSessionEntry`), legacy session store path/session-key helpers, updated-at reads, and deprecated whole-store mutation helpers |
|
||||
| `plugin-sdk/cron-store-runtime` | Cron store path/load/save helpers |
|
||||
| `plugin-sdk/state-paths` | State/OAuth dir path helpers |
|
||||
| `plugin-sdk/plugin-state-runtime` | Plugin sidecar SQLite keyed-state types |
|
||||
|
||||
4
extensions/acpx/npm-shrinkwrap.json
generated
4
extensions/acpx/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/acpx",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2",
|
||||
"openclawVersion": "2026.6.5-beta.6",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./src/runtime-internals/mcp-proxy.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/admin-http-rpc",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw admin HTTP RPC endpoint",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/alibaba-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2",
|
||||
"openclawVersion": "2026.6.5-beta.6",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
4
extensions/amazon-bedrock/npm-shrinkwrap.json
generated
4
extensions/amazon-bedrock/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/amazon-bedrock-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2",
|
||||
"openclawVersion": "2026.6.5-beta.6",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
4
extensions/anthropic-vertex/npm-shrinkwrap.json
generated
4
extensions/anthropic-vertex/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/vertex-sdk": "0.16.1"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-vertex-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2",
|
||||
"openclawVersion": "2026.6.5-beta.6",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/anthropic-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Anthropic provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/arcee-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Arcee provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/azure-speech",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Azure Speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/bonjour",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"description": "OpenClaw Bonjour/mDNS gateway discovery",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
4
extensions/brave/npm-shrinkwrap.json
generated
4
extensions/brave/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.6.2"
|
||||
"version": "2026.6.5-beta.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"description": "OpenClaw Brave Search provider plugin for web search.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -21,10 +21,10 @@
|
||||
"allowInvalidConfigRecovery": true
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2"
|
||||
"openclawVersion": "2026.6.5-beta.6"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/browser-plugin",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw browser tool plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/byteplus-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw BytePlus provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/canvas-plugin",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Canvas plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/cerebras-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Cerebras provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Chutes tests cover implicit provider plugin behavior.
|
||||
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { resolveOAuthApiKeyMarker } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import plugin from "./index.js";
|
||||
import { CHUTES_BASE_URL } from "./models.js";
|
||||
import { CHUTES_BASE_URL, clearChutesModelCacheForTests } from "./models.js";
|
||||
|
||||
const CHUTES_OAUTH_MARKER = resolveOAuthApiKeyMarker("chutes");
|
||||
|
||||
@@ -35,6 +35,17 @@ async function runChutesCatalogProvider(params: { apiKey: string; discoveryApiKe
|
||||
return result.provider;
|
||||
}
|
||||
|
||||
function readAuthorizationHeader(init?: { headers?: HeadersInit }): string {
|
||||
const headers = init?.headers;
|
||||
if (headers instanceof Headers) {
|
||||
return headers.get("Authorization") ?? "";
|
||||
}
|
||||
if (Array.isArray(headers)) {
|
||||
return headers.find(([key]) => key.toLowerCase() === "authorization")?.[1] ?? "";
|
||||
}
|
||||
return headers?.Authorization ?? headers?.authorization ?? "";
|
||||
}
|
||||
|
||||
async function withRealChutesDiscovery<T>(
|
||||
run: (fetchMock: ReturnType<typeof vi.fn>) => Promise<T>,
|
||||
) {
|
||||
@@ -60,6 +71,10 @@ async function withRealChutesDiscovery<T>(
|
||||
}
|
||||
|
||||
describe("chutes implicit provider auth mode", () => {
|
||||
beforeEach(() => {
|
||||
clearChutesModelCacheForTests();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
@@ -101,8 +116,8 @@ describe("chutes implicit provider auth mode", () => {
|
||||
|
||||
const chutesCalls = fetchMock.mock.calls.filter(([url]) => String(url).includes("chutes.ai"));
|
||||
expect(chutesCalls.length).toBeGreaterThan(0);
|
||||
const request = chutesCalls[0]?.[1] as { headers?: Record<string, string> } | undefined;
|
||||
expect(request?.headers?.Authorization).toBe("Bearer my-chutes-access-token");
|
||||
const request = chutesCalls[0]?.[1] as { headers?: HeadersInit } | undefined;
|
||||
expect(readAuthorizationHeader(request)).toBe("Bearer my-chutes-access-token");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/chutes-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Chutes.ai provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/clickclack",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw ClickClack channel plugin",
|
||||
"type": "module",
|
||||
@@ -18,7 +18,7 @@
|
||||
"openclaw": "2026.5.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.2"
|
||||
"openclaw": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/cloudflare-ai-gateway-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Cloudflare AI Gateway provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/codex-supervisor",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Codex app-server fleet supervision plugin.",
|
||||
"type": "module",
|
||||
|
||||
4
extensions/codex/npm-shrinkwrap.json
generated
4
extensions/codex/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/codex",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/codex",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"dependencies": {
|
||||
"@openai/codex": "0.135.0",
|
||||
"typebox": "1.1.39",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/codex",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2"
|
||||
"openclawVersion": "2026.6.5-beta.6"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -380,7 +380,7 @@ describe("CodexNativeSubagentMonitor", () => {
|
||||
const runtime = createRuntime();
|
||||
const monitor = new CodexNativeSubagentMonitor(client, runtime, {
|
||||
codexHome,
|
||||
transcriptPollDelaysMs: [10],
|
||||
transcriptPollDelaysMs: [10, 1],
|
||||
});
|
||||
monitor.registerParent({
|
||||
parentThreadId: "parent-thread",
|
||||
@@ -400,15 +400,20 @@ describe("CodexNativeSubagentMonitor", () => {
|
||||
|
||||
expect(runtime.deliverAgentHarnessTaskCompletion).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
expect(runtime.deliverAgentHarnessTaskCompletion).not.toHaveBeenCalled();
|
||||
|
||||
expect(runtime.deliverAgentHarnessTaskCompletion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
childSessionId: "child-thread",
|
||||
status: "succeeded",
|
||||
statusLabel: "completed_without_final_message",
|
||||
result: "Codex native subagent completed without a final assistant message.",
|
||||
}),
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(runtime.deliverAgentHarnessTaskCompletion).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
childSessionId: "child-thread",
|
||||
status: "succeeded",
|
||||
statusLabel: "completed_without_final_message",
|
||||
result: "Codex native subagent completed without a final assistant message.",
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
client.close();
|
||||
|
||||
@@ -606,23 +606,31 @@ export class CodexNativeSubagentMonitor {
|
||||
const delayMs = noFinalCompletionFallbackDelayMs(this.transcriptPollDelaysMs);
|
||||
childState.noFinalCompletionFallbackTimer = setTimeout(() => {
|
||||
childState.noFinalCompletionFallbackTimer = undefined;
|
||||
void this.reconcileChildTranscript(childState.childThreadId)
|
||||
.catch((error: unknown) => {
|
||||
embeddedAgentLog.warn("Failed to reconcile Codex native subagent transcript", {
|
||||
childThreadId: childState.childThreadId,
|
||||
error: formatErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
})
|
||||
.then((reconciled) => {
|
||||
if (!reconciled && !childState.transcriptTerminal) {
|
||||
void this.processCompletion(state, completion, eventAt);
|
||||
}
|
||||
});
|
||||
void this.deliverNoFinalCompletionFallback(state, childState, completion, eventAt);
|
||||
}, delayMs);
|
||||
unrefTimer(childState.noFinalCompletionFallbackTimer);
|
||||
}
|
||||
|
||||
private async deliverNoFinalCompletionFallback(
|
||||
state: ParentState,
|
||||
childState: ChildState,
|
||||
completion: CodexNativeSubagentCompletion,
|
||||
eventAt: number,
|
||||
): Promise<void> {
|
||||
const reconciled = await this.reconcileChildTranscript(childState.childThreadId).catch(
|
||||
(error: unknown): false => {
|
||||
embeddedAgentLog.warn("Failed to reconcile Codex native subagent transcript", {
|
||||
childThreadId: childState.childThreadId,
|
||||
error: formatErrorMessage(error),
|
||||
});
|
||||
return false;
|
||||
},
|
||||
);
|
||||
if (!reconciled && !childState.transcriptTerminal) {
|
||||
await this.processCompletion(state, completion, eventAt);
|
||||
}
|
||||
}
|
||||
|
||||
private clearTimers(): void {
|
||||
if (this.taskRowReconcileTimer) {
|
||||
clearInterval(this.taskRowReconcileTimer);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { saveSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { readCodexAppServerBinding, writeCodexAppServerBinding } from "./session-binding.js";
|
||||
import { rotateOversizedCodexAppServerStartupBinding } from "./startup-binding.js";
|
||||
@@ -35,17 +34,14 @@ describe("Codex app-server startup binding", () => {
|
||||
|
||||
async function writeSessionRecord(sessionFile: string, record: Record<string, unknown>) {
|
||||
await fs.mkdir(path.dirname(sessionFile), { recursive: true });
|
||||
await saveSessionStore(
|
||||
await fs.writeFile(
|
||||
path.join(path.dirname(sessionFile), "sessions.json"),
|
||||
{
|
||||
JSON.stringify({
|
||||
"agent:main:session-1": {
|
||||
sessionId: "session-1",
|
||||
sessionFile,
|
||||
updatedAt: Date.now(),
|
||||
...record,
|
||||
},
|
||||
},
|
||||
{ skipMaintenance: true },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,33 +78,29 @@ describe("Codex app-server startup binding", () => {
|
||||
expect(savedBinding?.threadId).toBe("thread-existing");
|
||||
});
|
||||
|
||||
it("reads updated SQLite-backed session records between startup checks", async () => {
|
||||
it("reuses the session record cache while sessions.json is unchanged", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const agentDir = path.join(tempDir, "agent");
|
||||
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
|
||||
await writeSessionRecord(sessionFile, { totalTokens: 12_000 });
|
||||
const sessionsJson = path.join(path.dirname(sessionFile), "sessions.json");
|
||||
const readFileSpy = vi.spyOn(fs, "readFile");
|
||||
|
||||
const firstBinding = await rotateOversizedCodexAppServerStartupBinding({
|
||||
binding: await readCodexAppServerBinding(sessionFile),
|
||||
sessionFile,
|
||||
agentDir,
|
||||
config: undefined,
|
||||
});
|
||||
expect(firstBinding?.threadId).toBe("thread-existing");
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
const binding = await rotateOversizedCodexAppServerStartupBinding({
|
||||
binding: await readCodexAppServerBinding(sessionFile),
|
||||
sessionFile,
|
||||
agentDir,
|
||||
config: undefined,
|
||||
});
|
||||
expect(binding?.threadId).toBe("thread-existing");
|
||||
}
|
||||
|
||||
await writeSessionRecord(sessionFile, { totalTokens: 400_000 });
|
||||
|
||||
const secondBinding = await rotateOversizedCodexAppServerStartupBinding({
|
||||
binding: await readCodexAppServerBinding(sessionFile),
|
||||
sessionFile,
|
||||
agentDir,
|
||||
config: undefined,
|
||||
});
|
||||
|
||||
expect(secondBinding).toBeUndefined();
|
||||
const savedBinding = await readCodexAppServerBinding(sessionFile);
|
||||
expect(savedBinding).toBeUndefined();
|
||||
const sessionStoreReads = readFileSpy.mock.calls.filter(
|
||||
([file]) => typeof file === "string" && file === sessionsJson,
|
||||
);
|
||||
expect(sessionStoreReads).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("checks native rollout token pressure under default compaction config", async () => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
embeddedAgentLog,
|
||||
type EmbeddedRunAttemptParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { loadSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { resolveCodexAppServerHomeDir } from "./auth-bridge.js";
|
||||
import { isJsonObject, type JsonValue } from "./protocol.js";
|
||||
import { clearCodexAppServerBinding, type CodexAppServerThreadBinding } from "./session-binding.js";
|
||||
@@ -35,6 +34,15 @@ const CODEX_APP_SERVER_BYTE_UNITS: Record<string, number> = {
|
||||
tb: 1024 * 1024 * 1024 * 1024,
|
||||
tib: 1024 * 1024 * 1024 * 1024,
|
||||
};
|
||||
type CodexSessionRecordCacheEntry = {
|
||||
sessionsFile: string;
|
||||
mtimeMs: number;
|
||||
size: number;
|
||||
record: (Record<string, unknown> & { sessionKey: string }) | undefined;
|
||||
};
|
||||
|
||||
const codexSessionRecordCache = new Map<string, CodexSessionRecordCacheEntry>();
|
||||
|
||||
function parseCodexAppServerByteLimit(value: unknown): number | undefined {
|
||||
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
||||
return Math.floor(value);
|
||||
@@ -115,14 +123,35 @@ async function listCodexAppServerRolloutFilesForThread(
|
||||
async function readCodexSessionRecordForSessionFile(
|
||||
sessionFile: string,
|
||||
): Promise<(Record<string, unknown> & { sessionKey: string }) | undefined> {
|
||||
const storePath = path.join(path.dirname(sessionFile), "sessions.json");
|
||||
const sessionsFile = path.join(path.dirname(sessionFile), "sessions.json");
|
||||
const resolvedSessionFile = path.resolve(sessionFile);
|
||||
let store: Record<string, unknown>;
|
||||
let stat: Awaited<ReturnType<typeof fs.stat>>;
|
||||
try {
|
||||
store = loadSessionStore(storePath, { skipCache: true }) as Record<string, unknown>;
|
||||
stat = await fs.stat(sessionsFile);
|
||||
} catch {
|
||||
codexSessionRecordCache.delete(resolvedSessionFile);
|
||||
return undefined;
|
||||
}
|
||||
const cached = codexSessionRecordCache.get(resolvedSessionFile);
|
||||
if (
|
||||
cached?.sessionsFile === sessionsFile &&
|
||||
cached.mtimeMs === stat.mtimeMs &&
|
||||
cached.size === stat.size
|
||||
) {
|
||||
return cached.record;
|
||||
}
|
||||
let store: JsonValue | undefined;
|
||||
try {
|
||||
store = JSON.parse(await fs.readFile(sessionsFile, "utf8")) as JsonValue;
|
||||
} catch {
|
||||
codexSessionRecordCache.delete(resolvedSessionFile);
|
||||
return undefined;
|
||||
}
|
||||
if (!isJsonObject(store)) {
|
||||
codexSessionRecordCache.delete(resolvedSessionFile);
|
||||
return undefined;
|
||||
}
|
||||
let found: (Record<string, unknown> & { sessionKey: string }) | undefined;
|
||||
for (const [sessionKey, record] of Object.entries(store)) {
|
||||
if (!isJsonObject(record) || typeof record.sessionFile !== "string") {
|
||||
continue;
|
||||
@@ -130,9 +159,16 @@ async function readCodexSessionRecordForSessionFile(
|
||||
if (path.resolve(record.sessionFile) !== resolvedSessionFile) {
|
||||
continue;
|
||||
}
|
||||
return { sessionKey, ...record };
|
||||
found = { sessionKey, ...record };
|
||||
break;
|
||||
}
|
||||
return undefined;
|
||||
codexSessionRecordCache.set(resolvedSessionFile, {
|
||||
sessionsFile,
|
||||
mtimeMs: stat.mtimeMs,
|
||||
size: stat.size,
|
||||
record: found,
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
type CodexAppServerRolloutTokenSnapshot = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/comfy-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw ComfyUI provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/copilot-proxy",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Copilot Proxy provider plugin",
|
||||
"type": "module",
|
||||
|
||||
4
extensions/copilot/npm-shrinkwrap.json
generated
4
extensions/copilot/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/copilot",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/copilot",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "1.0.0-beta.9"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/copilot",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2",
|
||||
"openclawVersion": "2026.6.5-beta.6",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/deepgram-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Deepgram media-understanding provider",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/deepinfra-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw DeepInfra provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/deepseek-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw DeepSeek provider plugin",
|
||||
"type": "module",
|
||||
|
||||
4
extensions/diagnostics-otel/npm-shrinkwrap.json
generated
4
extensions/diagnostics-otel/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-otel",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/diagnostics-otel",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "1.9.1",
|
||||
"@opentelemetry/api-logs": "0.218.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-otel",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"description": "OpenClaw diagnostics OpenTelemetry exporter for metrics and traces.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -34,10 +34,10 @@
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2"
|
||||
"openclawVersion": "2026.6.5-beta.6"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-prometheus",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/diagnostics-prometheus",
|
||||
"version": "2026.6.2"
|
||||
"version": "2026.6.5-beta.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diagnostics-prometheus",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"description": "OpenClaw diagnostics Prometheus exporter for runtime metrics.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -21,10 +21,10 @@
|
||||
"minHostVersion": ">=2026.4.25"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2"
|
||||
"openclawVersion": "2026.6.5-beta.6"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/diffs-language-pack",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/diffs-language-pack",
|
||||
"version": "2026.6.2"
|
||||
"version": "2026.6.5-beta.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diffs-language-pack",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"description": "OpenClaw diffs viewer syntax highlighting language pack",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -22,13 +22,13 @@
|
||||
"minHostVersion": ">=2026.5.27"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"assetScripts": {
|
||||
"build": "node ../../scripts/build-diffs-viewer-runtime.mjs full"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2",
|
||||
"openclawVersion": "2026.6.5-beta.6",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./assets/viewer-runtime.js",
|
||||
|
||||
4
extensions/diffs/npm-shrinkwrap.json
generated
4
extensions/diffs/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/diffs",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/diffs",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"dependencies": {
|
||||
"@pierre/diffs": "1.2.4",
|
||||
"@pierre/theme": "1.0.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/diffs",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"assetScripts": {
|
||||
"build": "node ../../scripts/build-diffs-viewer-runtime.mjs curated"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2",
|
||||
"openclawVersion": "2026.6.5-beta.6",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./assets/viewer-runtime.js",
|
||||
|
||||
6
extensions/discord/npm-shrinkwrap.json
generated
6
extensions/discord/npm-shrinkwrap.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/discord",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/discord",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"dependencies": {
|
||||
"@discordjs/voice": "0.19.2",
|
||||
"discord-api-types": "0.38.48",
|
||||
@@ -16,7 +16,7 @@
|
||||
"ws": "8.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.2"
|
||||
"openclaw": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/discord",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"description": "OpenClaw Discord channel plugin for channels, DMs, commands, and app events.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -20,7 +20,7 @@
|
||||
"openclaw": "2026.5.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.2"
|
||||
"openclaw": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -67,10 +67,10 @@
|
||||
"allowInvalidConfigRecovery": true
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2"
|
||||
"openclawVersion": "2026.6.5-beta.6"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
createEmptyPluginRegistry,
|
||||
setActivePluginRegistry,
|
||||
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { clearSessionStoreCacheForTest } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
clearSessionStoreCacheForTest,
|
||||
saveSessionStore,
|
||||
type SessionEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ChannelType, type AutocompleteInteraction } from "../internal/discord.js";
|
||||
import { createNoopThreadBindingManager } from "./thread-bindings.js";
|
||||
@@ -130,6 +134,25 @@ let findCommandByNativeName: typeof import("openclaw/plugin-sdk/command-auth-nat
|
||||
let resolveCommandArgChoices: typeof import("openclaw/plugin-sdk/command-auth-native").resolveCommandArgChoices;
|
||||
let resolveDiscordNativeChoiceContext: typeof import("./native-command-model-picker-ui.js").resolveDiscordNativeChoiceContext;
|
||||
|
||||
async function saveSessionOverride(params: {
|
||||
providerOverride: string;
|
||||
modelOverride: string;
|
||||
}): Promise<void> {
|
||||
fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
|
||||
await saveSessionStore(
|
||||
STORE_PATH,
|
||||
{
|
||||
[SESSION_KEY]: {
|
||||
sessionId: "main",
|
||||
updatedAt: Date.now(),
|
||||
providerOverride: params.providerOverride,
|
||||
modelOverride: params.modelOverride,
|
||||
},
|
||||
} satisfies Record<string, SessionEntry>,
|
||||
{ skipMaintenance: true },
|
||||
);
|
||||
}
|
||||
|
||||
function installProviderThinkingRegistryForTest(): void {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.providers.push({
|
||||
@@ -199,7 +222,7 @@ describe("discord native /think autocomplete", () => {
|
||||
await loadDiscordThinkAutocompleteModulesForTest());
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
clearSessionStoreCacheForTest();
|
||||
ensureConfiguredBindingRouteReadyMock.mockReset();
|
||||
ensureConfiguredBindingRouteReadyMock.mockResolvedValue({ ok: true });
|
||||
@@ -218,18 +241,10 @@ describe("discord native /think autocomplete", () => {
|
||||
: undefined,
|
||||
);
|
||||
installProviderThinkingRegistryForTest();
|
||||
fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
STORE_PATH,
|
||||
JSON.stringify({
|
||||
[SESSION_KEY]: {
|
||||
updatedAt: Date.now(),
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-5.4",
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
await saveSessionOverride({
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-5.4",
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -318,17 +333,10 @@ describe("discord native /think autocomplete", () => {
|
||||
? { levels: [{ id: "off" }, { id: "max" }] }
|
||||
: undefined,
|
||||
);
|
||||
fs.writeFileSync(
|
||||
STORE_PATH,
|
||||
JSON.stringify({
|
||||
[SESSION_KEY]: {
|
||||
updatedAt: Date.now(),
|
||||
providerOverride: "anthropic",
|
||||
modelOverride: "claude-opus-4-7",
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
await saveSessionOverride({
|
||||
providerOverride: "anthropic",
|
||||
modelOverride: "claude-opus-4-7",
|
||||
});
|
||||
const cfg = createConfig();
|
||||
resolveConfiguredBindingRouteMock.mockImplementation(createConfiguredRouteResult);
|
||||
const interaction = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/document-extract-plugin",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw local document extraction plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/duckduckgo-plugin",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw DuckDuckGo plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/elevenlabs-speech",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw ElevenLabs speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/exa-plugin",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Exa plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/fal-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw fal provider plugin",
|
||||
"type": "module",
|
||||
|
||||
6
extensions/feishu/npm-shrinkwrap.json
generated
6
extensions/feishu/npm-shrinkwrap.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "@openclaw/feishu",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/feishu",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"dependencies": {
|
||||
"@larksuiteoapi/node-sdk": "1.66.0",
|
||||
"typebox": "1.1.39",
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.2"
|
||||
"openclaw": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/feishu",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2"
|
||||
"openclaw": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -51,10 +51,10 @@
|
||||
"minHostVersion": ">=2026.5.29"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2"
|
||||
"openclawVersion": "2026.6.5-beta.6"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -89,19 +89,109 @@ export function createFeishuApiError(
|
||||
return new Error(formatFeishuApiFailure(error, errorPrefix, options), { cause: error });
|
||||
}
|
||||
|
||||
// Feishu message-API error codes that signal a transient rate limit; safe to retry with backoff.
|
||||
// 230020: per-chat rate limit (ext=chat rate limit) — confirmed by real concurrent load test.
|
||||
// 11232: tenant-level "create message service trigger rate limit" (100/min, 5/sec per app/bot).
|
||||
// Distinct from FEISHU_BACKOFF_CODES in typing.ts, which covers the reaction API (99991400+).
|
||||
const FEISHU_SEND_RATE_LIMIT_CODES = new Set([230020, 11232]);
|
||||
const FEISHU_SEND_MAX_RETRIES = 2;
|
||||
const FEISHU_SEND_RETRY_BASE_MS = 500;
|
||||
|
||||
/**
|
||||
* Returns a numeric rate-limit signal when an AxiosError indicates a retryable
|
||||
* Feishu message-API rate limit. Sources, in priority order:
|
||||
* 1. Gateway-level HTTP 429 (app-wide quota; `x-ogw-ratelimit-reset` header)
|
||||
* 2. Business-level `code` in `error.response.data.code` matching
|
||||
* FEISHU_SEND_RATE_LIMIT_CODES (e.g. 230020 per-chat, 11232 tenant-level).
|
||||
* Returns `undefined` for all other errors so they propagate without retry.
|
||||
*/
|
||||
export function getFeishuSendRateLimitCode(error: unknown): number | undefined {
|
||||
if (!isRecord(error)) {
|
||||
return undefined;
|
||||
}
|
||||
const response = isRecord(error.response) ? error.response : undefined;
|
||||
// HTTP 429: Feishu Open API gateway-level rate limit, always retry.
|
||||
if (typeof response?.status === "number" && response.status === 429) {
|
||||
return 429;
|
||||
}
|
||||
const data = isRecord(response?.data) ? response.data : undefined;
|
||||
const code = data?.code;
|
||||
return typeof code === "number" && FEISHU_SEND_RATE_LIMIT_CODES.has(code) ? code : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a retryable rate-limit code when a fulfilled (non-throwing) Feishu
|
||||
* SDK response embeds it in the response body. The Feishu node SDK can resolve
|
||||
* with `{ code: 11232, msg: "..." }` instead of throwing — see typing.ts
|
||||
* (getBackoffCodeFromResponse) and issue #28157 for the same behavior on
|
||||
* messageReaction.create. Without this classification, requestFeishuApi would
|
||||
* `return` the rate-limited body and downstream `assertFeishuMessageApiSuccess`
|
||||
* would fail once with no retry.
|
||||
*/
|
||||
export function getFeishuSendRateLimitCodeFromResponse(response: unknown): number | undefined {
|
||||
if (!isRecord(response)) {
|
||||
return undefined;
|
||||
}
|
||||
const code = (response as { code?: unknown }).code;
|
||||
return typeof code === "number" && FEISHU_SEND_RATE_LIMIT_CODES.has(code) ? code : undefined;
|
||||
}
|
||||
|
||||
export async function requestFeishuApi<T>(
|
||||
request: () => Promise<T>,
|
||||
errorPrefix: string,
|
||||
options: {
|
||||
includeConfigParams?: boolean;
|
||||
includeNestedErrorLogId?: boolean;
|
||||
/** Base delay per retry attempt in ms; multiplied by attempt index. @internal */
|
||||
retryDelayMs?: number;
|
||||
} = {},
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await request();
|
||||
} catch (error) {
|
||||
throw createFeishuApiError(error, errorPrefix, options);
|
||||
const retryDelayMs = options.retryDelayMs ?? FEISHU_SEND_RETRY_BASE_MS;
|
||||
let lastFulfilledRateLimit: { response: unknown; code: number } | undefined;
|
||||
for (let attempt = 0; attempt <= FEISHU_SEND_MAX_RETRIES; attempt++) {
|
||||
if (attempt > 0) {
|
||||
// Linear backoff: delay grows with each attempt to give the rate-limit window time to reset.
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, attempt * retryDelayMs);
|
||||
});
|
||||
}
|
||||
try {
|
||||
const result = await request();
|
||||
// Feishu SDK may fulfill with a rate-limit body (e.g. { code: 11232, ... })
|
||||
// instead of throwing. Classify before returning so retry covers both shapes.
|
||||
const fulfilledRateLimit = getFeishuSendRateLimitCodeFromResponse(result);
|
||||
if (fulfilledRateLimit !== undefined) {
|
||||
// Capture for the synthetic-error path below; on a non-final attempt
|
||||
// continue retrying, on the final attempt fall through so the loop
|
||||
// exits and the wrapped exhaustion error is thrown.
|
||||
lastFulfilledRateLimit = { response: result, code: fulfilledRateLimit };
|
||||
if (attempt < FEISHU_SEND_MAX_RETRIES) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
const isRetryable =
|
||||
attempt < FEISHU_SEND_MAX_RETRIES && getFeishuSendRateLimitCode(error) !== undefined;
|
||||
if (!isRetryable) {
|
||||
throw createFeishuApiError(error, errorPrefix, options);
|
||||
}
|
||||
// Rate-limit on a non-final attempt — loop continues to next retry.
|
||||
}
|
||||
}
|
||||
// Exhausted retries while the SDK kept fulfilling rate-limit bodies. Surface
|
||||
// the last response as an error so callers see the same wrapped shape they
|
||||
// would have seen if the SDK had thrown.
|
||||
if (lastFulfilledRateLimit) {
|
||||
const synthetic = Object.assign(
|
||||
new Error(`Request fulfilled with rate-limit code ${lastFulfilledRateLimit.code}`),
|
||||
{ response: { status: 200, data: lastFulfilledRateLimit.response } },
|
||||
);
|
||||
throw createFeishuApiError(synthetic, errorPrefix, options);
|
||||
}
|
||||
// Unreachable: every iteration either returns or throws. Required for TypeScript exhaustiveness.
|
||||
throw createFeishuApiError(new Error("unreachable"), errorPrefix, options);
|
||||
}
|
||||
|
||||
type ParsedCommentDocumentRef = {
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
loadSessionStore,
|
||||
saveSessionStore,
|
||||
type SessionEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { loadSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import { isFeishuSessionStoreKey, runFeishuDoctorSequence } from "./doctor.js";
|
||||
@@ -63,13 +59,10 @@ function storePath(agentId = "main"): string {
|
||||
return path.join(sessionsDir(agentId), "sessions.json");
|
||||
}
|
||||
|
||||
async function writeStore(
|
||||
entries: Record<string, SessionEntry>,
|
||||
agentId = "main",
|
||||
): Promise<string> {
|
||||
function writeStore(entries: Record<string, unknown>, agentId = "main"): string {
|
||||
const target = storePath(agentId);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
await saveSessionStore(target, entries, { skipMaintenance: true });
|
||||
fs.writeFileSync(target, JSON.stringify(entries, null, 2));
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -138,7 +131,7 @@ describe("Feishu doctor state repair", () => {
|
||||
fs.writeFileSync(path.join(feishuDedupDir, "default.json"), JSON.stringify({ msg1: 1 }));
|
||||
|
||||
writeTranscript("sess-ok", [sessionHeader("sess-ok"), userMessage("hello")]);
|
||||
await writeStore({
|
||||
writeStore({
|
||||
"agent:main:feishu:direct:ou_user": {
|
||||
sessionId: "sess-ok",
|
||||
sessionFile: "sess-ok.jsonl",
|
||||
@@ -162,16 +155,15 @@ describe("Feishu doctor state repair", () => {
|
||||
]);
|
||||
const customStorePath = path.join(stateDir(), "custom-sessions", "sessions.json");
|
||||
fs.mkdirSync(path.dirname(customStorePath), { recursive: true });
|
||||
await saveSessionStore(
|
||||
fs.writeFileSync(
|
||||
customStorePath,
|
||||
{
|
||||
JSON.stringify({
|
||||
"agent:main:feishu:direct:ou_user": {
|
||||
sessionId: "sess-abs",
|
||||
sessionFile: transcriptPath,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
{ skipMaintenance: true },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runFeishuDoctorSequence({
|
||||
@@ -195,7 +187,7 @@ describe("Feishu doctor state repair", () => {
|
||||
userMessage("world"),
|
||||
userMessage(""),
|
||||
]);
|
||||
await writeStore({
|
||||
writeStore({
|
||||
"agent:main:feishu:direct:ou_user": {
|
||||
sessionId: "sess-separated-blanks",
|
||||
sessionFile: "sess-separated-blanks.jsonl",
|
||||
@@ -238,7 +230,7 @@ describe("Feishu doctor state repair", () => {
|
||||
sessionHeader("sess-ok"),
|
||||
userMessage("hello"),
|
||||
]);
|
||||
const targetStorePath = await writeStore({
|
||||
const targetStorePath = writeStore({
|
||||
"agent:main:feishu:direct:ou_user": {
|
||||
sessionId: "sess-ok",
|
||||
sessionFile: "sess-ok.jsonl",
|
||||
@@ -294,7 +286,7 @@ describe("Feishu doctor state repair", () => {
|
||||
userMessage(""),
|
||||
]);
|
||||
|
||||
const targetStorePath = await writeStore({
|
||||
const targetStorePath = writeStore({
|
||||
"agent:main:feishu:direct:ou_user": {
|
||||
sessionId: "sess-bad",
|
||||
sessionFile: "sess-bad.jsonl",
|
||||
@@ -353,38 +345,6 @@ describe("Feishu doctor state repair", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("archives unhealthy Feishu sessions from SQLite-only retired agent stores", async () => {
|
||||
const retiredAgent = "retired";
|
||||
const transcriptPath = writeTranscript(
|
||||
"sess-retired-bad",
|
||||
[sessionHeader("sess-retired-bad"), userMessage(""), userMessage(""), userMessage("")],
|
||||
retiredAgent,
|
||||
);
|
||||
const targetStorePath = storePath(retiredAgent);
|
||||
const entries: Record<string, SessionEntry> = {
|
||||
"agent:retired:feishu:direct:ou_user": {
|
||||
sessionId: "sess-retired-bad",
|
||||
sessionFile: "sess-retired-bad.jsonl",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
};
|
||||
await saveSessionStore(targetStorePath, entries, { skipMaintenance: true });
|
||||
expect(fs.existsSync(targetStorePath)).toBe(false);
|
||||
|
||||
const result = await runFeishuDoctorSequence({
|
||||
cfg: feishuConfig(),
|
||||
env: process.env,
|
||||
shouldRepair: true,
|
||||
});
|
||||
|
||||
expect(result.warningNotes).toEqual([]);
|
||||
expect(result.changeNotes.join("\n")).toContain("Removed 1 Feishu-scoped session entry");
|
||||
|
||||
const store = loadSessionStore(targetStorePath, { skipCache: true });
|
||||
expect(store["agent:retired:feishu:direct:ou_user"]).toBeUndefined();
|
||||
expect(fs.existsSync(transcriptPath)).toBe(false);
|
||||
});
|
||||
|
||||
it("archives unhealthy default-scope sessions when metadata identifies Feishu", async () => {
|
||||
const transcriptPath = writeTranscript("sess-default-feishu-bad", [
|
||||
sessionHeader("sess-default-feishu-bad"),
|
||||
@@ -392,7 +352,7 @@ describe("Feishu doctor state repair", () => {
|
||||
userMessage(""),
|
||||
userMessage(""),
|
||||
]);
|
||||
const targetStorePath = await writeStore({
|
||||
const targetStorePath = writeStore({
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-default-feishu-bad",
|
||||
sessionFile: "sess-default-feishu-bad.jsonl",
|
||||
|
||||
@@ -7,11 +7,11 @@ import type {
|
||||
ChannelDoctorSequenceResult,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveAllAgentSessionStoreTargetsSync,
|
||||
resolveSessionFilePath,
|
||||
type SessionStoreTarget,
|
||||
resolveStorePath,
|
||||
updateSessionStore,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
||||
@@ -46,7 +46,10 @@ type FeishuDoctorFinding =
|
||||
count: number;
|
||||
};
|
||||
|
||||
type FeishuSessionTarget = SessionStoreTarget;
|
||||
type FeishuSessionTarget = {
|
||||
agentId: string;
|
||||
storePath: string;
|
||||
};
|
||||
|
||||
type FeishuSessionEntry = {
|
||||
sessionId?: unknown;
|
||||
@@ -213,12 +216,57 @@ function isFeishuSessionEntry(key: string, value: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function collectConfiguredAgentIds(cfg: OpenClawConfig): string[] {
|
||||
const ids = new Set<string>();
|
||||
ids.add(resolveConfiguredDefaultAgentId(cfg));
|
||||
for (const agent of cfg.agents?.list ?? []) {
|
||||
if (typeof agent.id === "string" && agent.id.trim()) {
|
||||
ids.add(normalizeAgentId(agent.id));
|
||||
}
|
||||
}
|
||||
return [...ids].toSorted();
|
||||
}
|
||||
|
||||
function resolveConfiguredDefaultAgentId(cfg: OpenClawConfig): string {
|
||||
const agents = cfg.agents?.list ?? [];
|
||||
const chosen = agents.find((agent) => agent?.default) ?? agents[0];
|
||||
return normalizeAgentId(typeof chosen?.id === "string" && chosen.id.trim() ? chosen.id : "main");
|
||||
}
|
||||
|
||||
function collectFeishuSessionTargets(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
stateDir: string;
|
||||
}): FeishuSessionTarget[] {
|
||||
return resolveAllAgentSessionStoreTargetsSync(params.cfg, { env: params.env }).toSorted(
|
||||
(left, right) => left.storePath.localeCompare(right.storePath),
|
||||
const byStorePath = new Map<string, FeishuSessionTarget>();
|
||||
const addTarget = (target: FeishuSessionTarget) => {
|
||||
byStorePath.set(path.resolve(target.storePath), {
|
||||
...target,
|
||||
storePath: path.resolve(target.storePath),
|
||||
});
|
||||
};
|
||||
|
||||
for (const agentId of collectConfiguredAgentIds(params.cfg)) {
|
||||
addTarget({
|
||||
agentId,
|
||||
storePath: resolveStorePath(params.cfg.session?.store, { agentId, env: params.env }),
|
||||
});
|
||||
}
|
||||
|
||||
const agentsDir = path.join(params.stateDir, "agents");
|
||||
for (const agentDir of safeReadDir(agentsDir)) {
|
||||
if (!agentDir.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const agentId = normalizeAgentId(agentDir.name);
|
||||
const storePath = path.join(agentsDir, agentDir.name, "sessions", "sessions.json");
|
||||
if (existsFile(storePath)) {
|
||||
addTarget({ agentId, storePath });
|
||||
}
|
||||
}
|
||||
|
||||
return [...byStorePath.values()].toSorted((left, right) =>
|
||||
left.storePath.localeCompare(right.storePath),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -509,7 +557,7 @@ export function inspectFeishuDoctorState(params: {
|
||||
const findings: FeishuDoctorFinding[] = collectCorruptFeishuStateJsonFindings(feishuStateDir);
|
||||
const sessionEntries: FeishuDoctorInspection["sessionEntries"] = [];
|
||||
|
||||
for (const target of collectFeishuSessionTargets({ cfg: params.cfg, env })) {
|
||||
for (const target of collectFeishuSessionTargets({ cfg: params.cfg, env, stateDir })) {
|
||||
const store = loadSessionStore(target.storePath, { skipCache: true });
|
||||
for (const [key, entry] of Object.entries(store).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
@@ -577,6 +625,9 @@ function movePathToBackup(params: {
|
||||
}
|
||||
|
||||
function copyStoreBackup(params: { storePath: string; backupDir: string; agentId: string }) {
|
||||
if (!existsFile(params.storePath)) {
|
||||
return;
|
||||
}
|
||||
const targetPath = path.join(
|
||||
params.backupDir,
|
||||
"session-stores",
|
||||
@@ -584,14 +635,7 @@ function copyStoreBackup(params: { storePath: string; backupDir: string; agentId
|
||||
path.basename(params.storePath),
|
||||
);
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o700 });
|
||||
if (existsFile(params.storePath)) {
|
||||
fs.copyFileSync(params.storePath, resolveUniquePath(targetPath));
|
||||
return;
|
||||
}
|
||||
const store = loadSessionStore(params.storePath, { skipCache: true });
|
||||
if (Object.keys(store).length > 0) {
|
||||
fs.writeFileSync(resolveUniquePath(targetPath), JSON.stringify(store, null, 2));
|
||||
}
|
||||
fs.copyFileSync(params.storePath, resolveUniquePath(targetPath));
|
||||
}
|
||||
|
||||
function collectSessionArtifactPaths(params: {
|
||||
|
||||
288
extensions/feishu/src/send.concurrent.test.ts
Normal file
288
extensions/feishu/src/send.concurrent.test.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* Concurrent Feishu message send stress tests.
|
||||
*
|
||||
* Verifies that sendMessageFeishu behaves correctly under concurrent load,
|
||||
* including the rate-limit error code (230020) the Feishu API returns when
|
||||
* the per-chat request frequency is too high. Related: issue #70879.
|
||||
*/
|
||||
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ClawdbotConfig } from "../runtime-api.js";
|
||||
|
||||
const {
|
||||
mockClientCreate,
|
||||
mockCreateFeishuClient,
|
||||
mockResolveFeishuAccount,
|
||||
mockConvertMarkdownTables,
|
||||
mockResolveMarkdownTableMode,
|
||||
} = vi.hoisted(() => ({
|
||||
mockClientCreate: vi.fn(),
|
||||
mockCreateFeishuClient: vi.fn(),
|
||||
mockResolveFeishuAccount: vi.fn(),
|
||||
mockConvertMarkdownTables: vi.fn((text: string) => text),
|
||||
mockResolveMarkdownTableMode: vi.fn(() => "preserve"),
|
||||
}));
|
||||
|
||||
vi.mock("./client.js", () => ({ createFeishuClient: mockCreateFeishuClient }));
|
||||
vi.mock("./accounts.js", () => ({
|
||||
resolveFeishuAccount: mockResolveFeishuAccount,
|
||||
resolveFeishuRuntimeAccount: mockResolveFeishuAccount,
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/markdown-table-runtime", () => ({
|
||||
resolveMarkdownTableMode: mockResolveMarkdownTableMode,
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/text-chunking", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/text-chunking")>();
|
||||
return { ...actual, convertMarkdownTables: mockConvertMarkdownTables };
|
||||
});
|
||||
vi.mock("./runtime.js", () => ({
|
||||
getFeishuRuntime: () => ({
|
||||
channel: {
|
||||
text: {
|
||||
resolveMarkdownTableMode: vi.fn(() => "preserve"),
|
||||
convertMarkdownTables: vi.fn((text: string) => text),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
let sendMessageFeishu: typeof import("./send.js").sendMessageFeishu;
|
||||
|
||||
const MOCK_CFG = {} as ClawdbotConfig;
|
||||
|
||||
/** Build a successful send response. */
|
||||
function okResponse(messageId: string) {
|
||||
return { code: 0, data: { message_id: messageId } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an AxiosError-shaped object for a Feishu rate-limit HTTP 400 response.
|
||||
* Mirrors what @larksuiteoapi/node-sdk throws when the server returns code 230020.
|
||||
*/
|
||||
function axiosRateLimitError(code = 230020) {
|
||||
return Object.assign(new Error("Request failed with status code 400"), {
|
||||
response: {
|
||||
status: 400,
|
||||
data: {
|
||||
code,
|
||||
msg: "This operation triggers the frequency limit, ext=chat rate limit",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
({ sendMessageFeishu } = await import("./send.js"));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockResolveFeishuAccount.mockReturnValue({ accountId: "default", configured: true });
|
||||
mockResolveMarkdownTableMode.mockReturnValue("preserve");
|
||||
mockConvertMarkdownTables.mockImplementation((text: string) => text);
|
||||
mockCreateFeishuClient.mockReturnValue({
|
||||
im: { message: { create: mockClientCreate } },
|
||||
});
|
||||
});
|
||||
|
||||
describe("Concurrent Feishu sends — happy path", () => {
|
||||
it("all concurrent sends succeed when API responds without errors", async () => {
|
||||
const CONCURRENCY = 10;
|
||||
let n = 0;
|
||||
mockClientCreate.mockImplementation(() => Promise.resolve(okResponse(`om_happy_${n++}`)));
|
||||
|
||||
const results = await Promise.all(
|
||||
Array.from({ length: CONCURRENCY }, (_, i) =>
|
||||
sendMessageFeishu({ cfg: MOCK_CFG, to: `oc_chat_${i}`, text: `Message ${i}` }),
|
||||
),
|
||||
);
|
||||
|
||||
expect(results).toHaveLength(CONCURRENCY);
|
||||
for (const result of results) {
|
||||
expect(result.messageId).toBeTruthy();
|
||||
expect(result.receipt).toBeDefined();
|
||||
}
|
||||
expect(mockClientCreate).toHaveBeenCalledTimes(CONCURRENCY);
|
||||
});
|
||||
|
||||
it("sends 20 messages concurrently and all resolve independently", async () => {
|
||||
const CONCURRENCY = 20;
|
||||
let n = 0;
|
||||
mockClientCreate.mockImplementation(() => Promise.resolve(okResponse(`om_concurrent_${n++}`)));
|
||||
|
||||
const results = await Promise.all(
|
||||
Array.from({ length: CONCURRENCY }, (_, i) =>
|
||||
sendMessageFeishu({ cfg: MOCK_CFG, to: "oc_stress", text: `stress-${i}` }),
|
||||
),
|
||||
);
|
||||
|
||||
expect(results).toHaveLength(CONCURRENCY);
|
||||
expect(mockClientCreate).toHaveBeenCalledTimes(CONCURRENCY);
|
||||
|
||||
// All message IDs should be unique
|
||||
const messageIds = results.map((r) => r.messageId);
|
||||
expect(new Set(messageIds).size).toBe(CONCURRENCY);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Concurrent Feishu sends — rate-limit behavior (code 230020)", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("throws on rate-limit code 230020 after exhausting retries", async () => {
|
||||
vi.useFakeTimers();
|
||||
mockClientCreate.mockRejectedValue(axiosRateLimitError(230020));
|
||||
|
||||
// Promise.allSettled attaches a rejection handler synchronously, so when
|
||||
// vi.runAllTimersAsync advances timers and fires the rejection, it is
|
||||
// already handled. Using expect().rejects.toThrow() would defer the
|
||||
// attachment via Promise.resolve().then(), causing an unhandled-rejection
|
||||
// warning before the handler is registered.
|
||||
const settled = Promise.allSettled([
|
||||
sendMessageFeishu({ cfg: MOCK_CFG, to: "oc_rl", text: "rate limited" }),
|
||||
]);
|
||||
await vi.runAllTimersAsync();
|
||||
const [result] = await settled;
|
||||
|
||||
expect(result.status).toBe("rejected");
|
||||
// 1 initial attempt + 2 retries = 3 total calls
|
||||
expect(mockClientCreate).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("some concurrent sends fail with rate-limit while others succeed", async () => {
|
||||
vi.useFakeTimers();
|
||||
const HALF = 4;
|
||||
let n = 0;
|
||||
|
||||
// Distinguish sends by receive_id: targets containing "fail" always rate-limit.
|
||||
mockClientCreate.mockImplementation((params: { data?: { receive_id?: string } }) => {
|
||||
const target = params?.data?.receive_id ?? "";
|
||||
if (target.includes("fail")) {
|
||||
return Promise.reject(axiosRateLimitError());
|
||||
}
|
||||
return Promise.resolve(okResponse(`om_ok_${n++}`));
|
||||
});
|
||||
|
||||
const settled = Promise.allSettled([
|
||||
...Array.from({ length: HALF }, (_, i) =>
|
||||
sendMessageFeishu({ cfg: MOCK_CFG, to: `oc_fail_${i}`, text: `fail-${i}` }),
|
||||
),
|
||||
...Array.from({ length: HALF }, (_, i) =>
|
||||
sendMessageFeishu({ cfg: MOCK_CFG, to: `oc_ok_${i}`, text: `ok-${i}` }),
|
||||
),
|
||||
]);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
const results = await settled;
|
||||
|
||||
expect(results.filter((r) => r.status === "fulfilled")).toHaveLength(HALF);
|
||||
expect(results.filter((r) => r.status === "rejected")).toHaveLength(HALF);
|
||||
// Rate-limited sends: HALF × 3 calls (1 + 2 retries); successful sends: HALF × 1 call
|
||||
expect(mockClientCreate).toHaveBeenCalledTimes(HALF * 3 + HALF);
|
||||
});
|
||||
|
||||
it("all concurrent sends fail gracefully when API consistently rate-limits", async () => {
|
||||
vi.useFakeTimers();
|
||||
const CONCURRENCY = 5;
|
||||
mockClientCreate.mockRejectedValue(axiosRateLimitError(230020));
|
||||
|
||||
const settled = Promise.allSettled(
|
||||
Array.from({ length: CONCURRENCY }, (_, i) =>
|
||||
sendMessageFeishu({ cfg: MOCK_CFG, to: "oc_all_fail", text: `msg-${i}` }),
|
||||
),
|
||||
);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
const results = await settled;
|
||||
|
||||
expect(results.every((r) => r.status === "rejected")).toBe(true);
|
||||
// Each send retries twice: CONCURRENCY × 3 total calls
|
||||
expect(mockClientCreate).toHaveBeenCalledTimes(CONCURRENCY * 3);
|
||||
});
|
||||
|
||||
it("recovers when API rate-limits once then succeeds", async () => {
|
||||
vi.useFakeTimers();
|
||||
let n = 0;
|
||||
mockClientCreate
|
||||
.mockRejectedValueOnce(axiosRateLimitError(230020))
|
||||
.mockImplementation(() => Promise.resolve(okResponse(`om_recovered_${n++}`)));
|
||||
|
||||
const sendPromise = sendMessageFeishu({ cfg: MOCK_CFG, to: "oc_recover", text: "recover" });
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
const result = await sendPromise;
|
||||
expect(result.messageId).toMatch(/^om_recovered_/);
|
||||
// 1 rate-limited call + 1 successful retry
|
||||
expect(mockClientCreate).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("rate-limit error message surfaces feishu_code for caller detection", async () => {
|
||||
vi.useFakeTimers();
|
||||
mockClientCreate.mockRejectedValue(axiosRateLimitError(230020));
|
||||
|
||||
// Same pattern: allSettled attaches the handler synchronously before timers advance.
|
||||
const settled = Promise.allSettled([
|
||||
sendMessageFeishu({
|
||||
cfg: MOCK_CFG,
|
||||
to: "oc_err_msg",
|
||||
text: "check error message",
|
||||
}),
|
||||
]);
|
||||
await vi.runAllTimersAsync();
|
||||
const [result] = await settled;
|
||||
|
||||
expect(result.status).toBe("rejected");
|
||||
const error = result.status === "rejected" ? result.reason : null;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
// Error message must carry feishu_code so retry/circuit-breaker logic upstream can identify it
|
||||
expect((error as Error).message).toMatch(/230020/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Concurrent Feishu sends — timing and ordering", () => {
|
||||
it("concurrent sends complete faster than sequential would (all fire in parallel)", async () => {
|
||||
const CONCURRENCY = 5;
|
||||
const SIMULATED_DELAY_MS = 20;
|
||||
let n = 0;
|
||||
|
||||
mockClientCreate.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => resolve(okResponse(`om_timed_${n++}`)), SIMULATED_DELAY_MS);
|
||||
}),
|
||||
);
|
||||
|
||||
const start = Date.now();
|
||||
const results = await Promise.all(
|
||||
Array.from({ length: CONCURRENCY }, (_, i) =>
|
||||
sendMessageFeishu({ cfg: MOCK_CFG, to: `oc_timed_${i}`, text: `msg ${i}` }),
|
||||
),
|
||||
);
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(results).toHaveLength(CONCURRENCY);
|
||||
// Concurrent: should complete in roughly 1x delay, not CONCURRENCY * delay
|
||||
expect(elapsed).toBeLessThan(SIMULATED_DELAY_MS * CONCURRENCY);
|
||||
});
|
||||
|
||||
it("sends to multiple distinct targets resolve independently", async () => {
|
||||
const targets = ["oc_alpha", "oc_beta", "oc_gamma"];
|
||||
let n = 0;
|
||||
mockClientCreate.mockImplementation(() => Promise.resolve(okResponse(`om_target_${n++}`)));
|
||||
|
||||
const results = await Promise.all(
|
||||
targets.map((to) => sendMessageFeishu({ cfg: MOCK_CFG, to, text: "hello" })),
|
||||
);
|
||||
|
||||
expect(results).toHaveLength(targets.length);
|
||||
for (const result of results) {
|
||||
expect(result.messageId).toBeTruthy();
|
||||
}
|
||||
expect(mockClientCreate).toHaveBeenCalledTimes(targets.length);
|
||||
});
|
||||
});
|
||||
323
extensions/feishu/src/send.retry.test.ts
Normal file
323
extensions/feishu/src/send.retry.test.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* Unit tests for requestFeishuApi retry logic and getFeishuSendRateLimitCode.
|
||||
*
|
||||
* Tests the retry behaviour directly via requestFeishuApi with retryDelayMs:0
|
||||
* so no fake timers are needed. Related: issue #70879.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
getFeishuSendRateLimitCode,
|
||||
getFeishuSendRateLimitCodeFromResponse,
|
||||
requestFeishuApi,
|
||||
} from "./comment-shared.js";
|
||||
|
||||
/** Build an AxiosError-shaped object for a given Feishu body error code (HTTP 400). */
|
||||
function axiosError(code: number) {
|
||||
return Object.assign(new Error("Request failed with status code 400"), {
|
||||
response: {
|
||||
status: 400,
|
||||
data: { code, msg: "feishu error" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an AxiosError-shaped object for a Feishu Open API gateway HTTP 429
|
||||
* response (no Feishu business code in body — gateway short-circuits before
|
||||
* the message service).
|
||||
*/
|
||||
function http429Error() {
|
||||
return Object.assign(new Error("Request failed with status code 429"), {
|
||||
response: {
|
||||
status: 429,
|
||||
data: { msg: "Too Many Requests" },
|
||||
headers: { "x-ogw-ratelimit-reset": "1" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Use retryDelayMs: 0 throughout to keep tests fast with no real delays.
|
||||
const NO_DELAY = { retryDelayMs: 0 };
|
||||
|
||||
describe("getFeishuSendRateLimitCode", () => {
|
||||
it("returns 230020 for per-chat rate-limit AxiosError", () => {
|
||||
expect(getFeishuSendRateLimitCode(axiosError(230020))).toBe(230020);
|
||||
});
|
||||
|
||||
it("returns undefined for 230006 (not a transient rate limit)", () => {
|
||||
expect(getFeishuSendRateLimitCode(axiosError(230006))).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for a non-rate-limit code", () => {
|
||||
expect(getFeishuSendRateLimitCode(axiosError(230001))).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for a plain Error (no response shape)", () => {
|
||||
expect(getFeishuSendRateLimitCode(new Error("boom"))).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for null", () => {
|
||||
expect(getFeishuSendRateLimitCode(null)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestFeishuApi — success path", () => {
|
||||
it("resolves immediately on first attempt", async () => {
|
||||
const request = vi.fn().mockResolvedValue("ok");
|
||||
const result = await requestFeishuApi(request, "prefix", NO_DELAY);
|
||||
expect(result).toBe("ok");
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestFeishuApi — retry on rate-limit", () => {
|
||||
it("retries once and succeeds on second attempt (code 230020)", async () => {
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(axiosError(230020))
|
||||
.mockResolvedValueOnce("ok-retry");
|
||||
|
||||
const result = await requestFeishuApi(request, "prefix", NO_DELAY);
|
||||
expect(result).toBe("ok-retry");
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not retry on code 230006", async () => {
|
||||
const request = vi.fn().mockRejectedValue(axiosError(230006));
|
||||
|
||||
await expect(requestFeishuApi(request, "prefix", NO_DELAY)).rejects.toThrow();
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("exhausts all retries and throws after 3 total attempts", async () => {
|
||||
const request = vi.fn().mockRejectedValue(axiosError(230020));
|
||||
|
||||
await expect(requestFeishuApi(request, "Feishu send failed", NO_DELAY)).rejects.toThrow(
|
||||
/Feishu send failed/,
|
||||
);
|
||||
// 1 initial attempt + 2 retries
|
||||
expect(request).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("wraps the final error with feishu_code in the message", async () => {
|
||||
const request = vi.fn().mockRejectedValue(axiosError(230020));
|
||||
|
||||
const err = await requestFeishuApi(request, "Feishu send failed", NO_DELAY).catch(
|
||||
(e: unknown) => e,
|
||||
);
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect((err as Error).message).toMatch(/230020/);
|
||||
});
|
||||
|
||||
it("recovers on the third attempt after two rate-limit failures", async () => {
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(axiosError(230020))
|
||||
.mockRejectedValueOnce(axiosError(230020))
|
||||
.mockResolvedValueOnce("ok-third");
|
||||
|
||||
const result = await requestFeishuApi(request, "prefix", NO_DELAY);
|
||||
expect(result).toBe("ok-third");
|
||||
expect(request).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestFeishuApi — no retry for non-rate-limit errors", () => {
|
||||
it("throws immediately without retry for a non-rate-limit Feishu code", async () => {
|
||||
const request = vi.fn().mockRejectedValue(axiosError(230001));
|
||||
|
||||
await expect(requestFeishuApi(request, "prefix", NO_DELAY)).rejects.toThrow();
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("throws immediately without retry for a plain Error", async () => {
|
||||
const request = vi.fn().mockRejectedValue(new Error("network failure"));
|
||||
|
||||
await expect(requestFeishuApi(request, "prefix", NO_DELAY)).rejects.toThrow(/network failure/);
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFeishuSendRateLimitCode — expanded rate-limit signals", () => {
|
||||
// 11232 is the tenant-level "create message service trigger rate limit"
|
||||
// (100/min, 5/sec). Same nature as 230020 (per-chat) but at a higher scope.
|
||||
it("returns 11232 for tenant-level message rate-limit AxiosError", () => {
|
||||
expect(getFeishuSendRateLimitCode(axiosError(11232))).toBe(11232);
|
||||
});
|
||||
|
||||
// HTTP 429 is the Feishu Open API gateway-level limit (app-wide quota);
|
||||
// it short-circuits before hitting the message service so the body has no
|
||||
// Feishu business code. We must detect it from response.status alone.
|
||||
it("returns 429 for gateway-level HTTP 429 with no business code in body", () => {
|
||||
expect(getFeishuSendRateLimitCode(http429Error())).toBe(429);
|
||||
});
|
||||
|
||||
it("prefers HTTP 429 over body code when both are present", () => {
|
||||
const err = Object.assign(new Error("Request failed with status code 429"), {
|
||||
response: {
|
||||
status: 429,
|
||||
data: { code: 230001, msg: "ignored" },
|
||||
},
|
||||
});
|
||||
expect(getFeishuSendRateLimitCode(err)).toBe(429);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestFeishuApi — retry on expanded rate-limit signals", () => {
|
||||
it("retries once and succeeds on second attempt (code 11232)", async () => {
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(axiosError(11232))
|
||||
.mockResolvedValueOnce("ok-after-11232");
|
||||
|
||||
const result = await requestFeishuApi(request, "prefix", NO_DELAY);
|
||||
expect(result).toBe("ok-after-11232");
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("retries once and succeeds on second attempt (HTTP 429)", async () => {
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(http429Error())
|
||||
.mockResolvedValueOnce("ok-after-429");
|
||||
|
||||
const result = await requestFeishuApi(request, "prefix", NO_DELAY);
|
||||
expect(result).toBe("ok-after-429");
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("exhausts retries on persistent 11232 and surfaces feishu_code", async () => {
|
||||
const request = vi.fn().mockRejectedValue(axiosError(11232));
|
||||
|
||||
const err = await requestFeishuApi(request, "Feishu send failed", NO_DELAY).catch(
|
||||
(e: unknown) => e,
|
||||
);
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect((err as Error).message).toMatch(/11232/);
|
||||
// 1 initial attempt + 2 retries
|
||||
expect(request).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("exhausts retries on persistent HTTP 429 and surfaces http_status", async () => {
|
||||
const request = vi.fn().mockRejectedValue(http429Error());
|
||||
|
||||
const err = await requestFeishuApi(request, "Feishu send failed", NO_DELAY).catch(
|
||||
(e: unknown) => e,
|
||||
);
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
// The error wrapper records http_status:429 in the JSON-encoded message.
|
||||
expect((err as Error).message).toMatch(/429/);
|
||||
expect(request).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("recovers across mixed rate-limit signals (230020 → 11232 → ok)", async () => {
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(axiosError(230020))
|
||||
.mockRejectedValueOnce(axiosError(11232))
|
||||
.mockResolvedValueOnce("ok-mixed");
|
||||
|
||||
const result = await requestFeishuApi(request, "prefix", NO_DELAY);
|
||||
expect(result).toBe("ok-mixed");
|
||||
expect(request).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
// Feishu SDK can fulfill (no throw) with a rate-limit code in the body, e.g.
|
||||
// `{ code: 11232, msg: "..." }`. Same shape the typing path already handles
|
||||
// via getBackoffCodeFromResponse — see issue #28157.
|
||||
describe("getFeishuSendRateLimitCodeFromResponse — fulfilled body classification", () => {
|
||||
it("returns 230020 for a fulfilled per-chat rate-limit body", () => {
|
||||
expect(getFeishuSendRateLimitCodeFromResponse({ code: 230020, msg: "rate limit" })).toBe(
|
||||
230020,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns 11232 for a fulfilled tenant-level rate-limit body", () => {
|
||||
expect(getFeishuSendRateLimitCodeFromResponse({ code: 11232, msg: "rate limit" })).toBe(11232);
|
||||
});
|
||||
|
||||
it("returns undefined for a successful body (code=0)", () => {
|
||||
expect(getFeishuSendRateLimitCodeFromResponse({ code: 0, data: {} })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for a non-rate-limit error body", () => {
|
||||
expect(getFeishuSendRateLimitCodeFromResponse({ code: 230001 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for null / non-object", () => {
|
||||
expect(getFeishuSendRateLimitCodeFromResponse(null)).toBeUndefined();
|
||||
expect(getFeishuSendRateLimitCodeFromResponse(undefined)).toBeUndefined();
|
||||
expect(getFeishuSendRateLimitCodeFromResponse("oops")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestFeishuApi — retry on fulfilled rate-limit body (no throw)", () => {
|
||||
// Mirrors the typing-path fix from #28157: SDK resolves with the rate-limit
|
||||
// code instead of throwing, and the helper must classify before returning.
|
||||
it("retries when SDK fulfills with code 11232 then succeeds", async () => {
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ code: 11232, msg: "rate limit", data: null })
|
||||
.mockResolvedValueOnce({ code: 0, data: { message_id: "om_ok" } });
|
||||
|
||||
const result = await requestFeishuApi(request, "prefix", NO_DELAY);
|
||||
expect(result).toEqual({ code: 0, data: { message_id: "om_ok" } });
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("retries when SDK fulfills with code 230020 then succeeds", async () => {
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ code: 230020, msg: "rate limit" })
|
||||
.mockResolvedValueOnce({ code: 0, data: { message_id: "om_ok2" } });
|
||||
|
||||
const result = await requestFeishuApi(request, "prefix", NO_DELAY);
|
||||
expect((result as { data: { message_id: string } }).data.message_id).toBe("om_ok2");
|
||||
expect(request).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("exhausts retries when SDK keeps fulfilling 11232 and throws wrapped error", async () => {
|
||||
const request = vi.fn().mockResolvedValue({ code: 11232, msg: "rate limit" });
|
||||
|
||||
const err = await requestFeishuApi(request, "Feishu send failed", NO_DELAY).catch(
|
||||
(e: unknown) => e,
|
||||
);
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect((err as Error).message).toMatch(/Feishu send failed/);
|
||||
expect((err as Error).message).toMatch(/11232/);
|
||||
// 1 initial attempt + 2 retries
|
||||
expect(request).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("does not retry when SDK fulfills with a successful response (code=0)", async () => {
|
||||
const request = vi.fn().mockResolvedValue({ code: 0, data: { message_id: "om_first" } });
|
||||
|
||||
const result = await requestFeishuApi(request, "prefix", NO_DELAY);
|
||||
expect((result as { data: { message_id: string } }).data.message_id).toBe("om_first");
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not retry when SDK fulfills with a non-rate-limit error code", async () => {
|
||||
// Non-retryable error codes pass through to assertFeishuMessageApiSuccess upstream.
|
||||
const fulfilled = { code: 230001, msg: "permission error" };
|
||||
const request = vi.fn().mockResolvedValue(fulfilled);
|
||||
|
||||
const result = await requestFeishuApi(request, "prefix", NO_DELAY);
|
||||
expect(result).toBe(fulfilled);
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("recovers across thrown then fulfilled rate-limits (catch → try → ok)", async () => {
|
||||
const request = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(axiosError(230020))
|
||||
.mockResolvedValueOnce({ code: 11232, msg: "rate limit" })
|
||||
.mockResolvedValueOnce({ code: 0, data: { message_id: "om_recovered" } });
|
||||
|
||||
const result = await requestFeishuApi(request, "prefix", NO_DELAY);
|
||||
expect((result as { data: { message_id: string } }).data.message_id).toBe("om_recovered");
|
||||
expect(request).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import { convertMarkdownTables } from "openclaw/plugin-sdk/text-chunking";
|
||||
import type { ClawdbotConfig } from "../runtime-api.js";
|
||||
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { createFeishuApiError, requestFeishuApi } from "./comment-shared.js";
|
||||
import { requestFeishuApi } from "./comment-shared.js";
|
||||
import type { MentionTarget } from "./mention-target.types.js";
|
||||
import { buildMentionedCardContent, buildMentionedMessage } from "./mention.js";
|
||||
import { parsePostContent } from "./post.js";
|
||||
@@ -67,6 +67,11 @@ function isWithdrawnReplyError(err: unknown): boolean {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Wrapped error shape from createFeishuApiError: err.cause holds the original error.
|
||||
const cause = (err as { cause?: unknown }).cause;
|
||||
if (cause && cause !== err) {
|
||||
return isWithdrawnReplyError(cause);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -169,17 +174,22 @@ async function sendReplyOrFallbackDirect(
|
||||
|
||||
let response: { code?: number; msg?: string; data?: { message_id?: string } };
|
||||
try {
|
||||
response = await client.im.message.reply({
|
||||
path: { message_id: params.replyToMessageId },
|
||||
data: {
|
||||
content: params.content,
|
||||
msg_type: params.msgType,
|
||||
...(params.replyInThread ? { reply_in_thread: true } : {}),
|
||||
},
|
||||
});
|
||||
response = await requestFeishuApi(
|
||||
() =>
|
||||
client.im.message.reply({
|
||||
path: { message_id: params.replyToMessageId! },
|
||||
data: {
|
||||
content: params.content,
|
||||
msg_type: params.msgType,
|
||||
...(params.replyInThread ? { reply_in_thread: true } : {}),
|
||||
},
|
||||
}),
|
||||
params.replyErrorPrefix,
|
||||
{ includeNestedErrorLogId: true },
|
||||
);
|
||||
} catch (err) {
|
||||
if (!isWithdrawnReplyError(err)) {
|
||||
throw createFeishuApiError(err, params.replyErrorPrefix, { includeNestedErrorLogId: true });
|
||||
throw err;
|
||||
}
|
||||
if (replyTargetFallbackError) {
|
||||
throw replyTargetFallbackError;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { getFeishuUserAgent } from "./client.js";
|
||||
import { requestFeishuApi } from "./comment-shared.js";
|
||||
import { resolveFeishuCardTemplate, type CardHeaderConfig } from "./send.js";
|
||||
import type { FeishuDomain } from "./types.js";
|
||||
|
||||
@@ -295,32 +296,44 @@ export class FeishuStreamingSession {
|
||||
const sendOptions = options ?? {};
|
||||
const sendMode = resolveStreamingCardSendMode(sendOptions);
|
||||
if (sendMode === "reply") {
|
||||
sendRes = await this.client.im.message.reply({
|
||||
path: { message_id: sendOptions.replyToMessageId! },
|
||||
data: {
|
||||
msg_type: "interactive",
|
||||
content: cardContent,
|
||||
...(sendOptions.replyInThread ? { reply_in_thread: true } : {}),
|
||||
},
|
||||
});
|
||||
sendRes = await requestFeishuApi(
|
||||
() =>
|
||||
this.client.im.message.reply({
|
||||
path: { message_id: sendOptions.replyToMessageId! },
|
||||
data: {
|
||||
msg_type: "interactive",
|
||||
content: cardContent,
|
||||
...(sendOptions.replyInThread ? { reply_in_thread: true } : {}),
|
||||
},
|
||||
}),
|
||||
"Send card failed",
|
||||
);
|
||||
} else if (sendMode === "root_create") {
|
||||
// root_id is undeclared in the SDK types but accepted at runtime
|
||||
sendRes = await this.client.im.message.create({
|
||||
params: { receive_id_type: receiveIdType },
|
||||
data: Object.assign(
|
||||
{ receive_id: receiveId, msg_type: "interactive", content: cardContent },
|
||||
{ root_id: sendOptions.rootId },
|
||||
),
|
||||
});
|
||||
sendRes = await requestFeishuApi(
|
||||
() =>
|
||||
this.client.im.message.create({
|
||||
params: { receive_id_type: receiveIdType },
|
||||
data: Object.assign(
|
||||
{ receive_id: receiveId, msg_type: "interactive", content: cardContent },
|
||||
{ root_id: sendOptions.rootId },
|
||||
),
|
||||
}),
|
||||
"Send card failed",
|
||||
);
|
||||
} else {
|
||||
sendRes = await this.client.im.message.create({
|
||||
params: { receive_id_type: receiveIdType },
|
||||
data: {
|
||||
receive_id: receiveId,
|
||||
msg_type: "interactive",
|
||||
content: cardContent,
|
||||
},
|
||||
});
|
||||
sendRes = await requestFeishuApi(
|
||||
() =>
|
||||
this.client.im.message.create({
|
||||
params: { receive_id_type: receiveIdType },
|
||||
data: {
|
||||
receive_id: receiveId,
|
||||
msg_type: "interactive",
|
||||
content: cardContent,
|
||||
},
|
||||
}),
|
||||
"Send card failed",
|
||||
);
|
||||
}
|
||||
if (sendRes.code !== 0 || !sendRes.data?.message_id) {
|
||||
throw new Error(`Send card failed: ${sendRes.msg}`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/file-transfer",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"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.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Firecrawl plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/fireworks-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Fireworks provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/github-copilot-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw GitHub Copilot provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/gmi-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw GMI Cloud provider plugin",
|
||||
"type": "module",
|
||||
|
||||
6
extensions/google-meet/npm-shrinkwrap.json
generated
6
extensions/google-meet/npm-shrinkwrap.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@openclaw/google-meet",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/google-meet",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"dependencies": {
|
||||
"commander": "14.0.3",
|
||||
"typebox": "1.1.39"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.2"
|
||||
"openclaw": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/google-meet",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"description": "OpenClaw Google Meet participant plugin for joining calls through Chrome or Twilio transports.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -16,7 +16,7 @@
|
||||
"openclaw": "2026.5.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.2"
|
||||
"openclaw": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -33,10 +33,10 @@
|
||||
"minHostVersion": ">=2026.4.20"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2"
|
||||
"openclawVersion": "2026.6.5-beta.6"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/google-plugin",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Google plugin",
|
||||
"type": "module",
|
||||
|
||||
6
extensions/googlechat/npm-shrinkwrap.json
generated
6
extensions/googlechat/npm-shrinkwrap.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "@openclaw/googlechat",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/googlechat",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"dependencies": {
|
||||
"gaxios": "7.1.4",
|
||||
"google-auth-library": "10.6.2",
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.2"
|
||||
"openclaw": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/googlechat",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"description": "OpenClaw Google Chat channel plugin for spaces and direct messages.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -17,7 +17,7 @@
|
||||
"openclaw": "2026.5.28"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openclaw": ">=2026.6.2"
|
||||
"openclaw": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openclaw": {
|
||||
@@ -75,10 +75,10 @@
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2"
|
||||
"openclawVersion": "2026.6.5-beta.6"
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/gradium-speech",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Gradium speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/groq-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Groq media-understanding provider",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/huggingface-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Hugging Face provider plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/image-generation-core",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw image generation runtime package",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/imessage",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw iMessage channel plugin using imsg on a signed-in Mac",
|
||||
"type": "module",
|
||||
@@ -43,10 +43,10 @@
|
||||
]
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.2"
|
||||
"pluginApi": ">=2026.6.5-beta.6"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.2"
|
||||
"openclawVersion": "2026.6.5-beta.6"
|
||||
}
|
||||
},
|
||||
"pluginInspector": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/inworld-speech",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Inworld speech plugin",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/irc",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"description": "OpenClaw IRC channel plugin",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openclaw/kilocode-provider",
|
||||
"version": "2026.6.2",
|
||||
"version": "2026.6.5-beta.6",
|
||||
"private": true,
|
||||
"description": "OpenClaw Kilo Gateway provider plugin",
|
||||
"type": "module",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user