Compare commits

...

34 Commits

Author SHA1 Message Date
Peter Steinberger
aab2a64781 test(release): expose beta tag in plugin fixture registry 2026-05-04 02:29:34 +01:00
Vincent Koc
7bc9bdad7b fix(plugins): clean pinned externalized load paths 2026-05-04 02:29:14 +01:00
Vincent Koc
363b7fb260 fix(plugins): trust official externalized npm installs 2026-05-04 02:28:41 +01:00
Vincent Koc
6878c22de9 fix(plugins): update trusted prerelease installs 2026-05-04 01:34:35 +01:00
Vincent Koc
bd28223914 fix(plugins): filter unavailable optional tools 2026-05-04 01:34:35 +01:00
Peter Steinberger
788c896715 test: harden Codex binding provider normalization 2026-05-04 01:34:35 +01:00
Kelaw - Keshav's Agent
39c11560ee fix: resolve Codex native auth by profile provider 2026-05-04 01:34:35 +01:00
Kelaw - Keshav's Agent
e922bed9ce docs(changelog): note Codex binding auth fix 2026-05-04 01:34:35 +01:00
Kelaw - Keshav's Agent
e5f4cb3644 fix: select Codex OAuth profile for bound app-server turns 2026-05-04 01:34:35 +01:00
Kelaw - Keshav's Agent
b190fae70c fix: preserve Codex binding OAuth transport
(cherry picked from commit f45dc3168aea29030b80381dc9017e9ee7e82ba4)
2026-05-04 01:34:35 +01:00
Vincent Koc
df43768465 fix(openai): flatten realtime transcription session update 2026-05-04 01:34:35 +01:00
Vincent Koc
eadc3ee699 fix(realtime): label pre-ready transcription closes 2026-05-04 01:34:35 +01:00
Vincent Koc
d35303582a fix(openai): fail realtime voice pre-ready closes 2026-05-04 01:34:35 +01:00
Vincent Koc
6a1bcb1566 fix(openai): omit realtime transcription session type 2026-05-04 01:34:35 +01:00
Peter Steinberger
9f0a114dab fix: use prerelease tags for official plugin repair 2026-05-04 01:34:23 +01:00
Peter Steinberger
62adabf3ce docs: refresh plugin sdk baseline 2026-05-04 00:59:49 +01:00
Peter Steinberger
728cf41034 docs: refresh plugin sdk baseline 2026-05-04 00:57:06 +01:00
Peter Steinberger
22c211cb1b docs: refresh config baseline 2026-05-04 00:57:06 +01:00
Peter Steinberger
a389d455c1 test: pin google meet realtime platform 2026-05-04 00:57:06 +01:00
Peter Steinberger
e5a1fa4c3b fix: align official plugin repair versions 2026-05-04 00:57:06 +01:00
Peter Steinberger
50f581d97c fix: keep feishu sdk plugin-local 2026-05-04 00:57:05 +01:00
Peter Steinberger
6658cf33ed build: sync beta config schema 2026-05-04 00:57:05 +01:00
Peter Steinberger
130efb13ce ci: pin fixed clawhub package dry-run 2026-05-04 00:57:05 +01:00
Peter Steinberger
c6473d6461 fix: harden beta plugin release path 2026-05-04 00:57:05 +01:00
Peter Steinberger
cc8ae6ee12 docs(changelog): note beta plugin update channel fix 2026-05-04 00:57:05 +01:00
Peter Steinberger
54493bde15 fix(update): keep beta plugin updates on beta channel 2026-05-04 00:57:05 +01:00
Peter Steinberger
9c3919ccef chore(release): refresh beta 2 config schema 2026-05-04 00:57:05 +01:00
Peter Steinberger
d5254a7e43 fix(release): prepare 2026.5.3 beta 2 2026-05-04 00:57:05 +01:00
Peter Steinberger
6ffb3c3f3a chore(release): refresh beta plugin sdk baseline 2026-05-04 00:57:05 +01:00
Peter Steinberger
70be1cbcd8 docs(release): polish 2026.5.3 changelog 2026-05-04 00:57:05 +01:00
Peter Steinberger
c28b0081eb docs(release): use base changelog heading for beta 2026-05-04 00:56:34 +01:00
Peter Steinberger
c9a83707d5 chore(release): refresh beta plugin sdk baseline 2026-05-04 00:56:34 +01:00
Peter Steinberger
d7ce1aafad chore(release): refresh beta config schema baseline 2026-05-04 00:56:34 +01:00
Peter Steinberger
6f0175779e chore(release): prepare 2026.5.3 beta 1 2026-05-04 00:56:34 +01:00
165 changed files with 2099 additions and 519 deletions

View File

@@ -32,7 +32,7 @@ env:
CLAWHUB_REGISTRY: "https://clawhub.ai"
CLAWHUB_REPOSITORY: "openclaw/clawhub"
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.
CLAWHUB_REF: "199e6a0cdf32471702e0503e9899e8d24f06a527"
CLAWHUB_REF: "bbdde7fd5325666374a08537298d10316b1cd131"
jobs:
preview_plugins_clawhub:
@@ -203,6 +203,9 @@ jobs:
chmod +x "$RUNNER_TEMP/clawhub"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Verify package-local runtime build
run: pnpm release:plugins:npm:runtime:check --package "${{ matrix.plugin.packageDir }}"
- name: Preview publish command
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}

View File

@@ -176,6 +176,9 @@ jobs:
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
- name: Verify package-local runtime build
run: pnpm release:plugins:npm:runtime:check --package "${{ matrix.plugin.packageDir }}"
- name: Preview publish command
run: bash scripts/plugin-npm-publish.sh --dry-run "${{ matrix.plugin.packageDir }}"

View File

@@ -2,11 +2,16 @@
Docs: https://docs.openclaw.ai
## Unreleased
## 2026.5.3
### Highlights
- Plugins/file-transfer: add bundled file-transfer plugin with `file_fetch`, `dir_list`, `dir_fetch`, and `file_write` agent tools for binary file ops on paired nodes; default-deny per-node path policy under `plugins.entries.file-transfer.config.nodes` with operator approval, symlink traversal refused by default (opt-in `followSymlinks`), and a 16 MB byte ceiling per round-trip. (#74742) Thanks @omarshahine.
- Plugins/install: harden official plugin install, uninstall, update, onboarding, ClawHub fallback, npm dependency-state reporting, and beta-channel update paths so externalized plugins behave like first-class package installs.
- Gateway/performance: trim startup and Control UI hot paths by lazy-loading plugin/runtime discovery, cron, schema, shutdown, sessions, and model metadata work only when needed.
- Channels/replies: improve Discord status reactions and degraded transport reporting, add WhatsApp Channel/Newsletter targets, and tighten Telegram, Feishu, Matrix, Microsoft Teams, and Slack delivery/recovery behavior.
- Install/update: recover broken macOS LaunchAgent upgrades, reject source-only plugin packages before runtime load, and repair stale Gateway/plugin state during updates and doctor runs.
- Agent/runtime reliability: preserve streamed provider replies, delayed A2A session replies, prompt/tool delivery, memory recall, web search provider discovery, and provider-specific thinking/model metadata across common edge cases.
### Changes
@@ -20,21 +25,27 @@ Docs: https://docs.openclaw.ai
- Gateway/performance: lazy-load early runtime discovery and shutdown-hook helpers, defer maintenance timers until after readiness, and trim duplicate plugin auto-enable work during Gateway startup.
- QA/Mantis: add a `pnpm openclaw qa mantis discord-smoke` runner and manual GitHub workflow that verify the Mantis Discord bot can see the configured guild/channel, post a smoke message, add a reaction, and upload artifacts.
- QA/Slack: add a Slack live transport QA runner with canary and mention-gating coverage for the private bot-to-bot harness. Thanks @vincentkoc.
- Gateway/performance: lazy-load the heavy cron runtime after the rest of Gateway startup, defer restart-sentinel refresh after readiness, and let the Gateway startup benchmark write per-run V8 CPU profiles with `--cpu-prof-dir`.
- Gateway/performance: keep raw channel-config schema parsing from discovering bundled plugin runtime metadata, and add `pnpm gateway:watch --benchmark-no-force` for profiling startup without the default port cleanup.
- Plugins/onboarding: let Manual setup install optional official plugins, including ClawHub-backed diagnostics with npm fallback, and expose the external Codex plugin as a selectable provider setup choice. Thanks @vincentkoc.
- Plugins/CLI: include package dependency install state in `openclaw plugins list --json` so scripts can spot missing plugin dependencies without runtime-loading plugins.
- Discord/status: add degraded Discord transport and gateway event-loop starvation signals to `openclaw channels status`, `openclaw status --deep`, and fetch-timeout logs so intermittent socket resets do not look like a healthy running channel. (#76327) Thanks @joshavant.
- Plugins/update: on the beta OpenClaw update channel, default-line npm and ClawHub plugin updates try `@beta` first and fall back to default/latest when no plugin beta release exists.
- Plugins/CLI/update: include package dependency install state in `openclaw plugins list --json`, trust official externalized npm migrations, clean stale bundled load paths for externalized installs, try plugin `@beta` updates first on the beta OpenClaw channel, and fall back to default/latest when no plugin beta release exists.
- Plugins/ClawHub: annotate 429 errors with reset windows and unauthenticated higher-rate-limit hints, so operators can tell when downloads recover and when signing in helps. Thanks @romneyda.
- Gateway/performance: lazy-load early runtime discovery, shutdown hooks, cron, channel-config schema metadata, restart sentinels, and maintenance timers after readiness; trim duplicate plugin auto-enable work and add startup CPU/profile controls.
- Gateway/config: stop Gateway startup and hot reload from auto-restoring invalid config; invalid config now fails closed and `openclaw doctor --fix` owns last-known-good repair.
- Discord/status: let explicit reaction tool calls opt into tracking later tool progress with `trackToolCalls: true`, share tool display emoji mapping, and surface degraded Discord transport or gateway event-loop starvation in status output. (#76327) Thanks @joshavant.
- Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow outbound target idea from #13424. Thanks @vincentkoc and @agentz-manfred.
- Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi.
- Agents/tools: skip optional media and PDF tool factories when the effective tool denylist already blocks them, avoiding unnecessary hot-path setup for tools that will be filtered out before model use. (#76773) Thanks @dorukardahan.
- Agents/sandbox: store sandbox container and browser registry entries as per-runtime shard files, reducing unrelated session lock contention while `openclaw doctor --fix` migrates legacy monolithic registry files. (#74831) Thanks @luckylhb90.
- Plugins/ClawHub: annotate 429 errors from ClawHub with the reset window from `RateLimit-Reset`/`Retry-After` and append a `Sign in for higher rate limits.` hint when the request was unauthenticated, so users can see when downloads will recover and how to lift the cap. Thanks @romneyda.
- Tools/BTW: add `/side` as a text and native slash-command alias for `/btw` side questions.
- Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi.
- QA/Mantis: add a `pnpm openclaw qa mantis discord-smoke` runner and manual GitHub workflow that verify the Mantis Discord bot can see the configured guild/channel, post a smoke message, add a reaction, and upload artifacts.
### Fixes
- Channels/WhatsApp: allow `@whiskeysockets/libsignal-node` in `onlyBuiltDependencies` so pnpm v9+ `blockExoticSubdeps` no longer rejects the baileys git-tarball subdep and silences all inbound agent replies. Fixes #76539. Thanks @ottodeng and @vincentkoc.
- Gateway/systemd: preserve operator-added secrets in the Gateway env file across re-stage while clearing OpenClaw-managed keys (such as `OPENCLAW_GATEWAY_TOKEN`) so a fresh staging value is never shadowed by a stale env-file copy; operator secrets are also retained when the state-dir `.env` is empty. Fixes #76860. Thanks @hclsys.
- Plugin updates: do not short-circuit trusted official npm updates as unchanged when the default/latest spec still resolves to an already-installed prerelease that the installer should replace with a stable fallback. Thanks @vincentkoc.
- Plugin tools: keep auth-unavailable optional tools hidden even when another default tool from the same plugin is available and `tools.alsoAllow` names the optional tool. Thanks @vincentkoc.
- Realtime transcription: report socket closes before provider readiness as closed-before-ready failures instead of mislabeling them as connection timeouts for OpenAI, xAI, and Deepgram streaming transcription. Thanks @vincentkoc.
- OpenAI/Google Meet: fail realtime voice connection attempts when the socket closes before `session.updated`, avoiding stuck Meet joins waiting on a bridge that never became ready. Thanks @vincentkoc.
- QA/cache: require the full `CACHE-OK <suffix>` marker before live cache probes stop retrying, so suffix-only prose cannot hide a broken probe response. Thanks @vincentkoc.
- Slack/Matrix: avoid creating blank progress-draft messages when `streaming.progress.label=false` and progress tool lines are disabled. Thanks @vincentkoc.
- QA/Matrix: keep the mock OpenAI tool-progress provider aligned with exact-marker Matrix prompts so the hardened live preview scenario still forces a deterministic read before final delivery. Thanks @vincentkoc.
@@ -62,6 +73,7 @@ Docs: https://docs.openclaw.ai
- Discord: resolve SecretRef-backed bot tokens from the active runtime snapshot for named accounts and keep unresolved configured tokens from crashing status or health checks. (#76987) Thanks @joshavant.
- Channels/streaming: expose `streaming.progress.label`, `labels`, `maxLines`, and `toolProgress` in bundled channel config metadata so progress draft settings appear in config, docs, and control surfaces. Thanks @vincentkoc.
- Channels/streaming: normalize whitespace and case for `streaming.progress.label: "auto"` so progress draft labels keep using the built-in label pool instead of rendering a literal `auto` title. Thanks @vincentkoc.
- Plugins/Codex: preserve Codex-native OAuth routing for `/codex bind` app-server turns so bound sessions keep the selected Codex auth profile instead of falling back to public OpenAI credentials. (#76714) Thanks @keshavbotagent.
- Gateway/install: prefer supported system Node over nvm/fnm/volta/asdf/mise when regenerating managed gateway services, so `gateway install --force` no longer recreates service definitions that doctor immediately flags as version-manager-backed. Fixes #76339. Thanks @brokemac79.
- Cron/status: render explicit `delivery.mode: "none"` jobs as no-delivery previews and label cron session history distinctly instead of showing fallback delivery or direct-session rows. Fixes #76945.
- Gateway/usage: serve `usage.cost` and `sessions.usage` from a durable transcript aggregate cache with lock-safe background refreshes and localized stale-cache status, so large usage views avoid repeated full scans. (#76650) Thanks @Marvinthebored.
@@ -88,6 +100,9 @@ Docs: https://docs.openclaw.ai
- Status/sessions: ignore malformed non-string persisted session provider/model metadata instead of throwing while rendering status summaries. Fixes #76206. Thanks @vincentkoc.
- CLI/config: remove only the targeted array element for `openclaw config unset array[index]` instead of replaying the unset during config write and deleting the shifted next element. Fixes #76290. Thanks @SymbolStar and @vincentkoc.
- Plugins/voice-call: treat abnormal local Gateway close code 1006 as a standalone CLI fallback case, so `voicecall smoke` and related commands can still run the provider check path when the Gateway socket closes before returning a response.
- CLI/doctor: migrate legacy per-channel `streaming.progress` config into `streaming.preview.toolProgress`, so upgrades with stale Discord or Telegram streaming keys validate again instead of blocking plugin commands.
- Plugins/release: reject ClawHub code-plugin packages that contain TypeScript runtime entries without compiled `dist/*.js` output, and run package-local runtime-build checks during npm and ClawHub plugin release previews.
- Plugins/update: keep beta-installed OpenClaw package updates on the beta plugin channel even when config still says stable, so Discord and other externalized plugins update from compiled `@beta` packages instead of stale source-only `latest` artifacts.
- Agents/tools: stop treating `tools.deny: ["write"]` as an implicit `apply_patch` deny; operators who want to block patch writes should deny `apply_patch` or `group:fs` explicitly. Fixes #76749. (#76795) Thanks @Nek-12 and @hclsys.
- Plugins/release: verify published plugin npm tarballs expose compiled runtime entries after publish, catching TS-only package artifacts before release closeout. Thanks @vincentkoc.
- CLI/message: exit cleanly with a nonzero status when message-command plugin registry loading fails before dispatch, preventing `openclaw-message` children from staying alive after plugin load errors. Fixes #76168.
@@ -147,8 +162,7 @@ Docs: https://docs.openclaw.ai
- CLI/onboarding: mask credential inputs (model-auth provider API keys, gateway tokens and passwords, web-search provider keys, and skill env-var values) in the interactive `openclaw onboard` wizard so pasted secrets no longer echo into terminal scrollback, `Start-Transcript` logs, or screenshots; existing tokens/passwords are preserved through a masked-preview confirm step before the sensitive prompt. Thanks @anurag-bg-neu.
- Control UI/Talk: fix Talk (OpenAI Realtime WebRTC) CORS failure by stripping server-side-only attribution headers (`originator`, `version`, `User-Agent`) from browser offer headers; `api.openai.com/v1/realtime/calls` only allows `authorization` and `content-type` in its CORS preflight, so forwarding these headers caused the browser SDP exchange to fail. Fixes #76435. Thanks @hclsys.
- Chat delivery: make `/verbose on|full|off` changes affect subsequent tool-use chat bubbles again, including channels with draft preview tool progress enabled, while preserving one-shot verbose directives.
- CLI/logs: auto-reconnect `openclaw logs --follow` on transient gateway disconnects (WebSocket close, timeout, connection drop) with bounded exponential backoff (up to 8 retries, capped at 30 s) and stderr retry warnings, while still exiting immediately on non-recoverable auth or configuration errors. Fixes #74782. (#75059) Thanks @shashank-poola.
- CLI/logs: announce `--follow` recovery with a `[logs] gateway reconnected` notice once a poll succeeds after a transient outage, and emit JSON `notice` records in `--json` mode for both the retry warning and the reconnect transition, so live monitoring scripts can react to the recovery. Carries forward #75059. (#75372) Thanks @romneyda.
- CLI/logs: auto-reconnect `openclaw logs --follow` on transient gateway disconnects with bounded backoff, stderr retry warnings, `[logs] gateway reconnected` recovery notices, and JSON `notice` records while still exiting immediately on non-recoverable auth or configuration errors. Fixes #74782. (#75059, #75372) Thanks @shashank-poola and @romneyda.
- Codex/WhatsApp: keep the `message` dynamic tool available when Codex source replies are configured for message-tool delivery, so coding-profile chat agents do not complete turns privately without a visible channel reply. Fixes #76660. (#76663) Thanks @VishalJ99.
- Codex/heartbeat: send heartbeat-specific initiative guidance through Codex turn-scoped collaboration-mode instructions, keeping ordinary message-tool chat turns in Default mode without heartbeat prompt leakage. Thanks @pashpashpash.
- Plugins/onboarding: trust optional official plugin and web-search installs selected from the official catalog so npm security scanning treats them like other source-linked official install paths. Thanks @vincentkoc.
@@ -168,10 +182,8 @@ Docs: https://docs.openclaw.ai
- TUI/Control UI: fix `/think` command showing only base thinking levels when the active session uses a different model from the default, so provider-specific levels like DeepSeek V4 Pro's `xhigh` and `max` are now visible and selectable. Fixes #76482. Thanks @amknight.
- CLI/sessions: keep intentional empty agent replies silent after tool-delivered channel output, instead of surfacing a misleading "No reply from agent." fallback. Thanks @vincentkoc.
- Config/doctor: cap `.clobbered.*` forensic snapshots per config path and serialize snapshot writes so repeated `doctor --fix` recovery loops cannot flood the config directory. Fixes #76454; carries forward #65649. Thanks @JUSTICEESSIELP, @rsnow, and @vincentkoc.
- Feishu: suppress duplicate text when replies send native voice media while preserving captions for ordinary audio files and falling back to text plus attachment links when voice uploads fail.
- Feishu: send the skipped reply text when `audioAsVoice` falls back to a generic file attachment after transcode failure, so voice-intent replies do not lose their caption.
- TTS/plugins: activate the configured speech provider plugin during Gateway startup, so Microsoft and Local CLI voice replies work immediately after selecting them instead of staying invisible in the startup plugin set. Fixes #76481. Thanks @amknight.
- TTS/plugins: include speech providers selected through inherited agent, channel, and account TTS personas during Gateway startup, matching the runtime TTS config merge. Carries forward #76481. Thanks @amknight.
- Feishu: suppress duplicate text when replies send native voice media, preserve captions for ordinary audio files, and send fallback text plus attachment links when `audioAsVoice` transcode/upload fallback produces a generic file.
- TTS/plugins: activate configured and inherited speech provider plugins during Gateway startup, so Microsoft and Local CLI voice replies work immediately after persona selection instead of staying invisible in the startup plugin set. Fixes #76481. Thanks @amknight.
- Feishu: keep packaged Feishu startup from bundling the Lark SDK's ESM `__dirname` path by loading the SDK as a plugin-local runtime dependency. Fixes #76291 and #76494. (#76392) Thanks @zqchris.
- Plugins/npm: build package-local runtime dist files for publishable plugins and stop listing root-package-excluded plugin sidecars in the core package metadata, so npm plugin installs such as `@openclaw/diffs` and `@openclaw/discord` no longer publish source-only runtime payloads. Fixes #76426. Thanks @PrinceOfEgypt.
- Channels/secrets: resolve SecretRef-backed channel credentials through external plugin secret contracts after the plugin split, covering runtime startup, target discovery, webhook auth, disabled-account enumeration, and late-bound web_search config. Fixes #76371. (#76449) Thanks @joshavant and @neeravmakwana.
@@ -185,22 +197,17 @@ Docs: https://docs.openclaw.ai
- Plugins/install: resolve bare official external plugin IDs such as `brave` through the official catalog when no bundled source is available, so packaged installs fetch the intended scoped npm package instead of an unrelated unscoped package. Fixes #76373. Thanks @bek91 and @vincentkoc.
- Plugins/install: require OpenClaw-owned install provenance before granting official npm plugin scanner trust, so direct npm package names no longer bypass launch-code scanning while catalog, onboarding, and doctor installs stay trusted. Thanks @fede-kamel and @vincentkoc.
- Network proxy: preserve target TLS hostname validation for Node HTTPS requests routed through the managed HTTP proxy, so Discord-style CONNECT traffic no longer validates certificates against the local proxy host. Fixes #74809. (#76442) Thanks @jesse-merhi and @abnershang.
- Gateway/sessions: keep async `sessions.list` title and preview hydration bounded to transcript head/tail reads so Control UI polling cannot full-scan large session transcripts every refresh. Thanks @vincentkoc.
- Gateway/sessions: cache manifest model-id normalization and bundled setup CLI fallback metadata against the active plugin metadata snapshot, so Control UI `sessions.list` polling avoids repeated plugin manifest scans while still refreshing after plugin reloads. Thanks @rolandrscheel.
- Gateway/sessions: keep `sessions.list` rows lightweight by bounding title/preview hydration to transcript head/tail reads and caching manifest model-id normalization plus setup fallback metadata against the active plugin snapshot. Thanks @vincentkoc and @rolandrscheel.
- Gateway/performance: cache per-run verbose-level session reads, skip a redundant `lsof` scan in `gateway --force` when no listener was killed, and make the Gateway startup benchmark print usage for `--help`.
- Gateway/sessions: keep agent runtime metadata on lightweight `sessions.list` rows so model-only session patches do not make Control UI lose runtime identity. Thanks @vincentkoc.
- Gateway/sessions: keep bulk `sessions.list` rows lightweight by skipping per-row transcript usage fallback, display model inference, and plugin projection, avoiding event-loop stalls in large session stores. Thanks @Marvinthebored and @vincentkoc.
- Gateway/models: keep read-only `models.list` fallbacks on persisted/current metadata and configured rows while using static auth checks, so missing `models.json` files no longer runtime-load provider discovery or stall gateway after restart. Fixes #76382; refs #76360 and #75707. Thanks @trojy13, @RayWoo, @AnathemaOfficial, and @vincentkoc.
- Gateway/models: keep agent image attachment capability checks on the full catalog while preserving the read-only `models.list` path, so image sends are not rejected after static catalog fallback.
- Gateway/sessions: keep agent runtime metadata on lightweight `sessions.list` rows and skip per-row transcript usage fallback, display model inference, and plugin projection, avoiding identity loss and event-loop stalls in large session stores. Thanks @Marvinthebored and @vincentkoc.
- Gateway/models: keep read-only `models.list` fallbacks on persisted/current metadata, configured rows, registry-compatible fallbacks, and static auth checks while preserving full-catalog image attachment capability checks. Fixes #76382; refs #76360 and #75707. Thanks @trojy13, @RayWoo, @AnathemaOfficial, @Marvinthebored, and @vincentkoc.
- CLI/plugins: reject missing plugin ids before config writes in `plugins enable` and `plugins disable` so a typo no longer persists a stale config entry. (#73554) Thanks @ai-hpc.
- Agents/sessions: preserve delivered trailing assistant replies during session-file repair so Telegram/WebChat history is not rewritten to drop already-delivered responses. Fixes #76329. Thanks @obviyus.
- Gateway/chat history: preserve oversized transcript turns as explicit omitted-message placeholders while avoiding large JSONL parse stalls. Thanks @Marvinthebored and @vincentkoc.
- Gateway/models: keep read-only model-list responses on registry-compatible fallbacks and metadata defaults, so empty or minimal persisted model files do not hide built-ins or custom model capabilities. Thanks @Marvinthebored.
- CLI/doctor: load the configured memory-slot plugin when resolving memory diagnostics so bundled `memory-core` no longer triggers a false “no active memory plugin” warning on standalone `doctor` / `status` runs. Fixes #76367. Thanks @neeravmakwana.
- Gateway: preserve stack diagnostics when `chat.send` or agent attachment parsing/staging fails, improving image-send failure triage. Refs #63432. (#75135) Thanks @keen0206.
- Agents/idle-timeout: add a cost-runaway breaker to the outer embedded-run retry loop that halts further attempts after 5 consecutive idle timeouts without completed model progress, so a wedged provider can no longer fan paid model calls out across the same run; completed text or tool-call progress resets the breaker, but partial tool-argument token dribbles do not. Fixes #76293. Thanks @ThePuma312.
- Heartbeats/Codex: stop sending the legacy `HEARTBEAT_OK` prompt instruction when heartbeat turns have the structured `heartbeat_respond` tool, while keeping the text sentinel for legacy automatic heartbeat replies. Thanks @pashpashpash.
- Heartbeats/Codex: keep structured heartbeat prompts aligned with actual `heartbeat_respond` tool availability and keep tool-disabled commitment check-ins on the legacy ack path. Thanks @pashpashpash and @vincentkoc.
- Heartbeats/Codex: align structured heartbeat prompts with actual `heartbeat_respond` tool availability, stop sending legacy `HEARTBEAT_OK` when the tool exists, and keep tool-disabled commitment check-ins on the legacy ack path. Thanks @pashpashpash and @vincentkoc.
- Agent runtimes: fail explicit plugin runtime selections honestly when the requested harness is unavailable instead of silently falling back to the embedded PI runtime. Thanks @pashpashpash.
- Maintainer workflow: push prepared PR heads through GitHub's verified commit API by default and require an explicit override before git-protocol pushes can publish unsigned commits. Thanks @BunsDev.
- Feishu: resolve setup/status probes through the selected/default account so multi-account configs with account-scoped app credentials show as configured and probeable. Fixes #72930. Thanks @brokemac79.
@@ -215,8 +222,7 @@ Docs: https://docs.openclaw.ai
- Cron: preserve manual `cron.run` IDs in `cron.runs` history so manual run acknowledgements can be correlated with finished run records. Fixes #76276.
- CLI/devices: request `operator.admin` for `openclaw devices approve <requestId>` only when the exact pending device request would mint or inherit admin-scoped operator access, while keeping lower-scope approvals on the pairing scope.
- Memory/embedding: broaden the embedding reindex retry classifier to include transient socket-layer errors (`fetch failed`, `ECONNRESET`, `socket hang up`, `UND_ERR_*`, `closed`) so memory reindex survives provider network hiccups instead of aborting mid-run. Related #56815, #44166. (#76311) Thanks @buyitsydney.
- Memory/sessions: keep rotated and deleted session transcripts (`.jsonl.reset.<iso>` / `.jsonl.deleted.<iso>`) searchable end-to-end by indexing their real content in `buildSessionEntry` instead of short-circuiting to empty entries, and by mapping archive hit paths back to their live transcript stem during `memory_search` visibility filtering so hits are no longer dropped at the guard. `.jsonl.bak.<iso>` backups and compaction checkpoints remain opaque. Refs #56131. Thanks @buyitsydney.
- Memory/sessions: emit a `sessionTranscriptUpdate` event when `archiveFileOnDisk` rotates a live session transcript into `.jsonl.reset.<iso>` / `.jsonl.deleted.<iso>` / `.jsonl.bak.<iso>`, and bypass the delta-bytes / delta-messages threshold gate in `processSessionDeltaBatch` for usage-counted archive paths (`.jsonl.reset.<iso>` and `.jsonl.deleted.<iso>`). Without the bypass the archive event was forwarded to the listener but dropped at the threshold check, because an archive is a one-shot file-rename mutation rather than an incremental append and would typically land below the default `deltaBytes: 100000` / `deltaMessages: 50` reindex thresholds. Archives now feed the memory sync incremental path the same way `appendMessage` / compaction / tool-result rewrite / chat inject / command execution events already do. Refs #56131. Thanks @buyitsydney.
- Memory/sessions: keep rotated and deleted transcripts (`.jsonl.reset.<iso>` / `.jsonl.deleted.<iso>`) searchable by indexing archive content, mapping archive hits back to live transcript stems, emitting transcript update events on archive rotation, and bypassing incremental delta thresholds for one-shot archive mutations while keeping backups and compaction checkpoints opaque. Refs #56131. Thanks @buyitsydney.
- Memory/search: keep sqlite-vec optional in packaged installs and point missing-extension recovery at the valid `agents.defaults.memorySearch.store.vector.extensionPath` setting. Thanks @willemsej and @vincentkoc.
- Gateway: keep directly requested plugin tools invokable under restrictive tool profiles while preserving explicit deny lists and the HTTP safety deny list, preventing catalog/invoke mismatches that surface as "Tool not available". Thanks @BunsDev.
- Gateway/update: allow beta binaries to refresh gateway services when the config was last written by the matching stable release version, avoiding false newer-config downgrade blocks during beta channel updates.

View File

@@ -1,2 +1,2 @@
0dd4f5abaf72f0d6b3fe5777cbf16c7a8c8052eece17436dc0ac2809b0ea27de plugin-sdk-api-baseline.json
2c2170cf2f1193f7dbecdef3ccd1b601992407e3d99863d1aa13cb1817c238fd plugin-sdk-api-baseline.jsonl
701356478634a8f3e71f941ed21a00e0456d947d287edcafb56231013b27a057 plugin-sdk-api-baseline.json
ed17426dd5e9db4b83db77162e7490eee3c0439170c1a9d1e84c01d7027d580c plugin-sdk-api-baseline.jsonl

View File

@@ -159,7 +159,9 @@ the maintainer-only release runbook.
QA-lab through a local OTLP/HTTP receiver and verifies the exported trace
span names, bounded attributes, and content/identifier redaction without
requiring Opik, Langfuse, or another external collector.
- Run `pnpm release:check` before every tagged release
- Run `pnpm release:check` before every tagged release; it also builds and
verifies package-local plugin runtimes so TypeScript plugin entries cannot
ship without matching `dist/*.js` output.
- Run `OpenClaw Release Publish` for the mutating publish sequence after the
tag exists. Dispatch it from `release/YYYY.M.D` (or `main` when publishing a
main-reachable tag), pass the release tag and successful OpenClaw npm

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/acpx",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw ACP runtime backend",
"repository": {
"type": "git",
@@ -25,10 +25,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3",
"openclawVersion": "2026.5.3-beta.3",
"staticAssets": [
{
"source": "./src/runtime-internals/mcp-proxy.mjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/alibaba-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Alibaba Model Studio video provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Amazon Bedrock provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Anthropic Vertex provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/anthropic-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Anthropic provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/arcee-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Arcee provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/azure-speech",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Azure Speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/bluebubbles",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw BlueBubbles channel plugin",
"repository": {
"type": "git",
@@ -12,7 +12,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -53,10 +53,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/bonjour",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Bonjour/mDNS gateway discovery",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/brave-plugin",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Brave plugin",
"repository": {
"type": "git",
@@ -20,10 +20,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/browser-plugin",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw browser tool plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/byteplus-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw BytePlus provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/cerebras-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Cerebras provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/chutes-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Chutes.ai provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/cloudflare-ai-gateway-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Cloudflare AI Gateway provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/codex",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Codex harness and model provider plugin",
"repository": {
"type": "git",
@@ -27,10 +27,10 @@
"minHostVersion": ">=2026.5.1-beta.1"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -387,6 +387,40 @@ describe("bridgeCodexAppServerStartOptions", () => {
}
});
it("applies the default OpenAI Codex OAuth profile when no profile id is explicit", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:default",
credential: {
type: "oauth",
provider: "openai-codex",
access: "default-access-token",
refresh: "default-refresh-token",
expires: Date.now() + 24 * 60 * 60_000,
accountId: "account-default",
email: "codex-default@example.test",
},
});
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
});
expect(request).toHaveBeenCalledWith("account/login/start", {
type: "chatgptAuthTokens",
accessToken: "default-access-token",
chatgptAccountId: "account-default",
chatgptPlanType: null,
});
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
}
});
it("refreshes an expired OpenAI Codex OAuth profile before app-server login", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));

View File

@@ -3,8 +3,10 @@ import path from "node:path";
import {
ensureAuthProfileStore,
loadAuthProfileStoreForSecretsRuntime,
resolveAuthProfileOrder,
resolveProviderIdForAuth,
resolveApiKeyForProfile,
resolveOpenClawAgentDir,
resolvePersistedAuthProfileOwnerAgentDir,
saveAuthProfileStore,
type AuthProfileCredential,
@@ -28,6 +30,8 @@ const OPENAI_API_KEY_ENV_VAR = "OPENAI_API_KEY";
const CODEX_APP_SERVER_API_KEY_ENV_VARS = [CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR];
const CODEX_APP_SERVER_ISOLATION_ENV_VARS = [CODEX_HOME_ENV_VAR, HOME_ENV_VAR];
type AuthProfileOrderConfig = Parameters<typeof resolveAuthProfileOrder>[0]["cfg"];
export async function bridgeCodexAppServerStartOptions(params: {
startOptions: CodexAppServerStartOptions;
agentDir: string;
@@ -41,15 +45,49 @@ export async function bridgeCodexAppServerStartOptions(params: {
params.agentDir,
);
const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
const authProfileId = resolveCodexAppServerAuthProfileId({
authProfileId: params.authProfileId,
store,
});
const shouldClearInheritedOpenAiApiKey = shouldClearOpenAiApiKeyForCodexAuthProfile({
store,
authProfileId: params.authProfileId,
authProfileId,
});
return shouldClearInheritedOpenAiApiKey
? withClearedEnvironmentVariables(isolatedStartOptions, CODEX_APP_SERVER_API_KEY_ENV_VARS)
: isolatedStartOptions;
}
export function resolveCodexAppServerAuthProfileId(params: {
authProfileId?: string;
store: ReturnType<typeof ensureAuthProfileStore>;
config?: AuthProfileOrderConfig;
}): string | undefined {
const requested = params.authProfileId?.trim();
if (requested) {
return requested;
}
return resolveAuthProfileOrder({
cfg: params.config,
store: params.store,
provider: CODEX_APP_SERVER_AUTH_PROVIDER,
})[0]?.trim();
}
export function resolveCodexAppServerAuthProfileIdForAgent(params: {
authProfileId?: string;
agentDir?: string;
config?: AuthProfileOrderConfig;
}): string | undefined {
const agentDir = params.agentDir?.trim() || resolveOpenClawAgentDir();
const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
return resolveCodexAppServerAuthProfileId({
authProfileId: params.authProfileId,
store,
config: params.config,
});
}
export function resolveCodexAppServerHomeDir(agentDir: string): string {
return path.join(path.resolve(agentDir), CODEX_APP_SERVER_HOME_DIRNAME);
}
@@ -153,11 +191,14 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: {
authProfileId?: string;
forceOAuthRefresh?: boolean;
}): Promise<LoginAccountParams | undefined> {
const profileId = params.authProfileId?.trim();
const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
const profileId = resolveCodexAppServerAuthProfileId({
authProfileId: params.authProfileId,
store,
});
if (!profileId) {
return undefined;
}
const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
const credential = store.profiles[profileId];
if (!credential) {
throw new Error(`Codex app-server auth profile "${profileId}" was not found.`);

View File

@@ -40,7 +40,11 @@ import {
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
import { handleCodexAppServerApprovalRequest } from "./approval-bridge.js";
import { refreshCodexAppServerAuthTokens } from "./auth-bridge.js";
import {
refreshCodexAppServerAuthTokens,
resolveCodexAppServerAuthProfileId,
resolveCodexAppServerAuthProfileIdForAgent,
} from "./auth-bridge.js";
import {
createCodexAppServerClientFactoryTestHooks,
defaultCodexAppServerClientFactory,
@@ -377,16 +381,31 @@ export async function runCodexAppServerAttempt(
agentId: params.agentId,
});
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
const runtimeParams = { ...params, sessionKey: sandboxSessionKey };
const startupBinding = await readCodexAppServerBinding(params.sessionFile);
const startupAuthProfileCandidate =
params.runtimePlan?.auth.forwardedAuthProfileId ??
params.authProfileId ??
startupBinding?.authProfileId;
const startupAuthProfileId = params.authProfileStore
? resolveCodexAppServerAuthProfileId({
authProfileId: startupAuthProfileCandidate,
store: params.authProfileStore,
config: params.config,
})
: resolveCodexAppServerAuthProfileIdForAgent({
authProfileId: startupAuthProfileCandidate,
agentDir,
config: params.config,
});
const runtimeParams = {
...params,
sessionKey: sandboxSessionKey,
...(startupAuthProfileId ? { authProfileId: startupAuthProfileId } : {}),
};
const activeContextEngine = isActiveHarnessContextEngine(params.contextEngine)
? params.contextEngine
: undefined;
let yieldDetected = false;
const startupBinding = await readCodexAppServerBinding(params.sessionFile);
const startupAuthProfileId =
params.runtimePlan?.auth.forwardedAuthProfileId ??
params.authProfileId ??
startupBinding?.authProfileId;
const tools = await buildDynamicTools({
params,
resolvedWorkspace,
@@ -553,7 +572,7 @@ export async function runCodexAppServerAttempt(
});
const startupThread = await startOrResumeThread({
client: startupClient,
params,
params: runtimeParams,
cwd: effectiveWorkspace,
dynamicTools: toolBridge.specs,
appServer,

View File

@@ -7,10 +7,26 @@ import {
readCodexAppServerBinding,
resolveCodexAppServerBindingPath,
writeCodexAppServerBinding,
type CodexAppServerAuthProfileLookup,
} from "./session-binding.js";
let tempDir: string;
const nativeAuthLookup: Pick<CodexAppServerAuthProfileLookup, "authProfileStore"> = {
authProfileStore: {
version: 1,
profiles: {
work: {
type: "oauth",
provider: "openai-codex",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
},
},
},
};
describe("codex app-server session binding", () => {
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-binding-"));
@@ -44,6 +60,96 @@ describe("codex app-server session binding", () => {
await expect(fs.stat(resolveCodexAppServerBindingPath(sessionFile))).resolves.toBeTruthy();
});
it("does not persist public OpenAI as the provider for Codex-native auth bindings", async () => {
const sessionFile = path.join(tempDir, "session.json");
await writeCodexAppServerBinding(
sessionFile,
{
threadId: "thread-123",
cwd: tempDir,
authProfileId: "work",
model: "gpt-5.4-mini",
modelProvider: "openai",
},
nativeAuthLookup,
);
const raw = await fs.readFile(resolveCodexAppServerBindingPath(sessionFile), "utf8");
const binding = await readCodexAppServerBinding(sessionFile, nativeAuthLookup);
expect(raw).not.toContain('"modelProvider": "openai"');
expect(binding).toMatchObject({
threadId: "thread-123",
authProfileId: "work",
model: "gpt-5.4-mini",
});
expect(binding?.modelProvider).toBeUndefined();
});
it("normalizes older Codex-native bindings that stored public OpenAI provider", async () => {
const sessionFile = path.join(tempDir, "session.json");
await fs.writeFile(
resolveCodexAppServerBindingPath(sessionFile),
`${JSON.stringify({
schemaVersion: 1,
threadId: "thread-123",
sessionFile,
cwd: tempDir,
authProfileId: "work",
model: "gpt-5.4-mini",
modelProvider: "openai",
createdAt: "2026-05-03T00:00:00.000Z",
updatedAt: "2026-05-03T00:00:00.000Z",
})}\n`,
);
const binding = await readCodexAppServerBinding(sessionFile, nativeAuthLookup);
expect(binding?.authProfileId).toBe("work");
expect(binding?.modelProvider).toBeUndefined();
});
it("does not infer native Codex auth from the profile id prefix", async () => {
const sessionFile = path.join(tempDir, "session.json");
await writeCodexAppServerBinding(
sessionFile,
{
threadId: "thread-123",
cwd: tempDir,
authProfileId: "openai-codex:work",
model: "gpt-5.4-mini",
modelProvider: "openai",
},
{
authProfileStore: {
version: 1,
profiles: {
"openai-codex:work": {
type: "api_key",
provider: "openai",
key: "sk-test",
},
},
},
},
);
const binding = await readCodexAppServerBinding(sessionFile, {
authProfileStore: {
version: 1,
profiles: {
"openai-codex:work": {
type: "api_key",
provider: "openai",
key: "sk-test",
},
},
},
});
expect(binding?.modelProvider).toBe("openai");
});
it("clears missing bindings without throwing", async () => {
const sessionFile = path.join(tempDir, "missing.json");
await clearCodexAppServerBinding(sessionFile);

View File

@@ -1,8 +1,27 @@
import fs from "node:fs/promises";
import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime";
import {
ensureAuthProfileStore,
resolveOpenClawAgentDir,
resolveProviderIdForAuth,
type AuthProfileStore,
} from "openclaw/plugin-sdk/agent-runtime";
import type { CodexAppServerApprovalPolicy, CodexAppServerSandboxMode } from "./config.js";
import type { CodexServiceTier } from "./protocol.js";
const CODEX_APP_SERVER_NATIVE_AUTH_PROVIDER = "openai-codex";
const PUBLIC_OPENAI_MODEL_PROVIDER = "openai";
type ProviderAuthAliasLookupParams = Parameters<typeof resolveProviderIdForAuth>[1];
type ProviderAuthAliasConfig = NonNullable<ProviderAuthAliasLookupParams>["config"];
export type CodexAppServerAuthProfileLookup = {
authProfileId?: string;
authProfileStore?: AuthProfileStore;
agentDir?: string;
config?: ProviderAuthAliasConfig;
};
export type CodexAppServerThreadBinding = {
schemaVersion: 1;
threadId: string;
@@ -25,6 +44,7 @@ export function resolveCodexAppServerBindingPath(sessionFile: string): string {
export async function readCodexAppServerBinding(
sessionFile: string,
lookup: Omit<CodexAppServerAuthProfileLookup, "authProfileId"> = {},
): Promise<CodexAppServerThreadBinding | undefined> {
const path = resolveCodexAppServerBindingPath(sessionFile);
let raw: string;
@@ -42,14 +62,20 @@ export async function readCodexAppServerBinding(
if (parsed.schemaVersion !== 1 || typeof parsed.threadId !== "string") {
return undefined;
}
const authProfileId =
typeof parsed.authProfileId === "string" ? parsed.authProfileId : undefined;
return {
schemaVersion: 1,
threadId: parsed.threadId,
sessionFile,
cwd: typeof parsed.cwd === "string" ? parsed.cwd : "",
authProfileId: typeof parsed.authProfileId === "string" ? parsed.authProfileId : undefined,
authProfileId,
model: typeof parsed.model === "string" ? parsed.model : undefined,
modelProvider: typeof parsed.modelProvider === "string" ? parsed.modelProvider : undefined,
modelProvider: normalizeCodexAppServerBindingModelProvider({
...lookup,
authProfileId,
modelProvider: typeof parsed.modelProvider === "string" ? parsed.modelProvider : undefined,
}),
approvalPolicy: readApprovalPolicy(parsed.approvalPolicy),
sandbox: readSandboxMode(parsed.sandbox),
serviceTier: readServiceTier(parsed.serviceTier),
@@ -74,6 +100,7 @@ export async function writeCodexAppServerBinding(
> & {
createdAt?: string;
},
lookup: Omit<CodexAppServerAuthProfileLookup, "authProfileId"> = {},
): Promise<void> {
const now = new Date().toISOString();
const payload: CodexAppServerThreadBinding = {
@@ -83,7 +110,11 @@ export async function writeCodexAppServerBinding(
cwd: binding.cwd,
authProfileId: binding.authProfileId,
model: binding.model,
modelProvider: binding.modelProvider,
modelProvider: normalizeCodexAppServerBindingModelProvider({
...lookup,
authProfileId: binding.authProfileId,
modelProvider: binding.modelProvider,
}),
approvalPolicy: binding.approvalPolicy,
sandbox: binding.sandbox,
serviceTier: binding.serviceTier,
@@ -111,6 +142,80 @@ function isNotFound(error: unknown): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
}
export function isCodexAppServerNativeAuthProfile(
lookup: CodexAppServerAuthProfileLookup,
): boolean {
const authProfileId = lookup.authProfileId?.trim();
if (!authProfileId) {
return false;
}
try {
const credential = resolveCodexAppServerAuthProfileCredential({
...lookup,
authProfileId,
});
return isCodexAppServerNativeAuthProvider({
provider: credential?.provider,
config: lookup.config,
});
} catch (error) {
embeddedAgentLog.debug("failed to resolve codex app-server auth profile provider", {
authProfileId,
error,
});
return false;
}
}
export function normalizeCodexAppServerBindingModelProvider(params: {
authProfileId?: string;
modelProvider?: string;
authProfileStore?: AuthProfileStore;
agentDir?: string;
config?: ProviderAuthAliasConfig;
}): string | undefined {
const modelProvider = params.modelProvider?.trim();
if (!modelProvider) {
return undefined;
}
if (
isCodexAppServerNativeAuthProfile(params) &&
modelProvider.toLowerCase() === PUBLIC_OPENAI_MODEL_PROVIDER
) {
return undefined;
}
return modelProvider;
}
function resolveCodexAppServerAuthProfileCredential(
lookup: CodexAppServerAuthProfileLookup,
): AuthProfileStore["profiles"][string] | undefined {
const authProfileId = lookup.authProfileId?.trim();
if (!authProfileId) {
return undefined;
}
const store = lookup.authProfileStore ?? loadCodexAppServerAuthProfileStore(lookup.agentDir);
return store.profiles[authProfileId];
}
function loadCodexAppServerAuthProfileStore(agentDir: string | undefined): AuthProfileStore {
return ensureAuthProfileStore(agentDir?.trim() || resolveOpenClawAgentDir(), {
allowKeychainPrompt: false,
});
}
function isCodexAppServerNativeAuthProvider(params: {
provider?: string;
config?: ProviderAuthAliasConfig;
}): boolean {
const provider = params.provider?.trim();
return Boolean(
provider &&
resolveProviderIdForAuth(provider, { config: params.config }) ===
CODEX_APP_SERVER_NATIVE_AUTH_PROVIDER,
);
}
function readApprovalPolicy(value: unknown): CodexAppServerApprovalPolicy | undefined {
return value === "never" ||
value === "on-request" ||

View File

@@ -1,5 +1,116 @@
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
import { describe, expect, it } from "vitest";
import { resolveReasoningEffort } from "./thread-lifecycle.js";
import {
buildThreadResumeParams,
buildThreadStartParams,
resolveReasoningEffort,
} from "./thread-lifecycle.js";
function createAttemptParams(params: {
provider: string;
authProfileId?: string;
authProfileProvider?: string;
authProfileProviders?: Record<string, string>;
}): EmbeddedRunAttemptParams {
const authProfileProviders =
params.authProfileProviders ??
(params.authProfileId
? { [params.authProfileId]: params.authProfileProvider ?? "openai-codex" }
: {});
return {
provider: params.provider,
modelId: "gpt-5.4",
authProfileId: params.authProfileId,
authProfileStore: {
version: 1,
profiles: Object.fromEntries(
Object.entries(authProfileProviders).map(([profileId, provider]) => [
profileId,
{
type: "oauth" as const,
provider,
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
},
]),
),
},
} as EmbeddedRunAttemptParams;
}
function createAppServerOptions() {
return {
approvalPolicy: "on-request",
approvalsReviewer: "user",
sandbox: "workspace-write",
} as const;
}
describe("Codex app-server model provider selection", () => {
it.each(["openai", "openai-codex"])(
"omits public %s modelProvider when forwarding native Codex auth on thread/start",
(provider) => {
const request = buildThreadStartParams(
createAttemptParams({ provider, authProfileId: "work" }),
{
cwd: "/repo",
dynamicTools: [],
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
},
);
expect(request).not.toHaveProperty("modelProvider");
},
);
it("uses the bound native Codex auth profile when deciding thread/resume modelProvider", () => {
const request = buildThreadResumeParams(
createAttemptParams({
provider: "openai",
authProfileProviders: { bound: "openai-codex" },
}),
{
threadId: "thread-1",
authProfileId: "bound",
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
},
);
expect(request).not.toHaveProperty("modelProvider");
});
it("does not infer native Codex auth from the profile id prefix", () => {
const request = buildThreadStartParams(
createAttemptParams({
provider: "openai",
authProfileId: "openai-codex:work",
authProfileProvider: "openai",
}),
{
cwd: "/repo",
dynamicTools: [],
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
},
);
expect(request).toMatchObject({ modelProvider: "openai" });
});
it("keeps public OpenAI modelProvider when no native Codex auth profile is selected", () => {
const request = buildThreadStartParams(createAttemptParams({ provider: "openai" }), {
cwd: "/repo",
dynamicTools: [],
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
});
expect(request).toMatchObject({ modelProvider: "openai" });
});
});
describe("resolveReasoningEffort (#71946)", () => {
describe("modern Codex models (none/low/medium/high/xhigh enum)", () => {

View File

@@ -25,8 +25,10 @@ import {
} from "./protocol.js";
import {
clearCodexAppServerBinding,
isCodexAppServerNativeAuthProfile,
readCodexAppServerBinding,
writeCodexAppServerBinding,
type CodexAppServerAuthProfileLookup,
type CodexAppServerThreadBinding,
} from "./session-binding.js";
@@ -40,7 +42,11 @@ export async function startOrResumeThread(params: {
config?: JsonObject;
}): Promise<CodexAppServerThreadBinding> {
const dynamicToolsFingerprint = fingerprintDynamicTools(params.dynamicTools);
const binding = await readCodexAppServerBinding(params.params.sessionFile);
const binding = await readCodexAppServerBinding(params.params.sessionFile, {
authProfileStore: params.params.authProfileStore,
agentDir: params.params.agentDir,
config: params.params.config,
});
if (binding?.threadId) {
// `/codex resume <thread>` writes a binding before the next turn can know
// the dynamic tool catalog, so only invalidate fingerprints we actually have.
@@ -57,28 +63,44 @@ export async function startOrResumeThread(params: {
await clearCodexAppServerBinding(params.params.sessionFile);
} else {
try {
const authProfileId = params.params.authProfileId ?? binding.authProfileId;
const response = assertCodexThreadResumeResponse(
await params.client.request(
"thread/resume",
buildThreadResumeParams(params.params, {
threadId: binding.threadId,
authProfileId,
appServer: params.appServer,
developerInstructions: params.developerInstructions,
config: params.config,
}),
),
);
const boundAuthProfileId = params.params.authProfileId ?? binding.authProfileId;
const fallbackModelProvider = resolveCodexAppServerModelProvider(params.params.provider);
await writeCodexAppServerBinding(params.params.sessionFile, {
threadId: response.thread.id,
cwd: params.cwd,
const boundAuthProfileId = authProfileId;
const fallbackModelProvider = resolveCodexAppServerModelProvider({
provider: params.params.provider,
authProfileId: boundAuthProfileId,
model: params.params.modelId,
modelProvider: response.modelProvider ?? fallbackModelProvider,
dynamicToolsFingerprint,
createdAt: binding.createdAt,
authProfileStore: params.params.authProfileStore,
agentDir: params.params.agentDir,
config: params.params.config,
});
await writeCodexAppServerBinding(
params.params.sessionFile,
{
threadId: response.thread.id,
cwd: params.cwd,
authProfileId: boundAuthProfileId,
model: params.params.modelId,
modelProvider: response.modelProvider ?? fallbackModelProvider,
dynamicToolsFingerprint,
createdAt: binding.createdAt,
},
{
authProfileStore: params.params.authProfileStore,
agentDir: params.params.agentDir,
config: params.params.config,
},
);
return {
...binding,
threadId: response.thread.id,
@@ -112,17 +134,31 @@ export async function startOrResumeThread(params: {
}),
),
);
const modelProvider = resolveCodexAppServerModelProvider(params.params.provider);
const createdAt = new Date().toISOString();
await writeCodexAppServerBinding(params.params.sessionFile, {
threadId: response.thread.id,
cwd: params.cwd,
const modelProvider = resolveCodexAppServerModelProvider({
provider: params.params.provider,
authProfileId: params.params.authProfileId,
model: response.model ?? params.params.modelId,
modelProvider: response.modelProvider ?? modelProvider,
dynamicToolsFingerprint,
createdAt,
authProfileStore: params.params.authProfileStore,
agentDir: params.params.agentDir,
config: params.params.config,
});
const createdAt = new Date().toISOString();
await writeCodexAppServerBinding(
params.params.sessionFile,
{
threadId: response.thread.id,
cwd: params.cwd,
authProfileId: params.params.authProfileId,
model: response.model ?? params.params.modelId,
modelProvider: response.modelProvider ?? modelProvider,
dynamicToolsFingerprint,
createdAt,
},
{
authProfileStore: params.params.authProfileStore,
agentDir: params.params.agentDir,
config: params.params.config,
},
);
return {
schemaVersion: 1,
threadId: response.thread.id,
@@ -147,7 +183,13 @@ export function buildThreadStartParams(
config?: JsonObject;
},
): CodexThreadStartParams {
const modelProvider = resolveCodexAppServerModelProvider(params.provider);
const modelProvider = resolveCodexAppServerModelProvider({
provider: params.provider,
authProfileId: params.authProfileId,
authProfileStore: params.authProfileStore,
agentDir: params.agentDir,
config: params.config,
});
return {
model: params.modelId,
...(modelProvider ? { modelProvider } : {}),
@@ -169,12 +211,19 @@ export function buildThreadResumeParams(
params: EmbeddedRunAttemptParams,
options: {
threadId: string;
authProfileId?: string;
appServer: CodexAppServerRuntimeOptions;
developerInstructions?: string;
config?: JsonObject;
},
): CodexThreadResumeParams {
const modelProvider = resolveCodexAppServerModelProvider(params.provider);
const modelProvider = resolveCodexAppServerModelProvider({
provider: params.provider,
authProfileId: options.authProfileId ?? params.authProfileId,
authProfileStore: params.authProfileStore,
agentDir: params.agentDir,
config: params.config,
});
return {
threadId: options.threadId,
model: params.modelId,
@@ -326,14 +375,30 @@ function buildUserInput(
];
}
function resolveCodexAppServerModelProvider(provider: string): string | undefined {
const normalized = provider.trim();
if (!normalized || normalized === "codex") {
function resolveCodexAppServerModelProvider(params: {
provider: string;
authProfileId?: string;
authProfileStore?: CodexAppServerAuthProfileLookup["authProfileStore"];
agentDir?: string;
config?: CodexAppServerAuthProfileLookup["config"];
}): string | undefined {
const normalized = params.provider.trim();
const normalizedLower = normalized.toLowerCase();
if (!normalized || normalizedLower === "codex") {
// `codex` is OpenClaw's virtual provider; let Codex app-server keep its
// native provider/auth selection instead of forcing the legacy OpenAI path.
return undefined;
}
return normalized === "openai-codex" ? "openai" : normalized;
if (
isCodexAppServerNativeAuthProfile(params) &&
(normalizedLower === "openai" || normalizedLower === "openai-codex")
) {
// When OpenClaw is forwarding ChatGPT/Codex OAuth, `openai` is Codex's
// native provider id, not a public OpenAI API-key choice. Omit the override
// so app-server keeps its configured provider/auth pair for this session.
return undefined;
}
return normalizedLower === "openai-codex" ? "openai" : normalized;
}
// Modern Codex models (gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.2) use the

View File

@@ -335,14 +335,21 @@ async function bindConversation(
};
}
const workspaceDir = parsed.cwd ?? deps.resolveCodexDefaultWorkspaceDir(pluginConfig);
const data = await deps.startCodexConversationThread({
const existingBinding = await deps.readCodexAppServerBinding(ctx.sessionFile);
const authProfileId = existingBinding?.authProfileId;
const startParams: Parameters<CodexCommandDeps["startCodexConversationThread"]>[0] = {
pluginConfig,
config: ctx.config,
sessionFile: ctx.sessionFile,
workspaceDir,
threadId: parsed.threadId,
model: parsed.model,
modelProvider: parsed.provider,
});
};
if (authProfileId) {
startParams.authProfileId = authProfileId;
}
const data = await deps.startCodexConversationThread(startParams);
const binding = await deps.readCodexAppServerBinding(ctx.sessionFile);
const threadId = binding?.threadId ?? parsed.threadId ?? "new thread";
const summary = `Codex app-server thread ${threadId} in ${workspaceDir}`;

View File

@@ -1374,7 +1374,13 @@ describe("codex command", () => {
const sessionFile = path.join(tempDir, "session.jsonl");
await fs.writeFile(
`${sessionFile}.codex-app-server.json`,
JSON.stringify({ schemaVersion: 1, threadId: "thread-123", cwd: "/repo" }),
JSON.stringify({
schemaVersion: 1,
threadId: "thread-123",
cwd: "/repo",
authProfileId: "openai-codex:work",
modelProvider: "openai",
}),
);
const startCodexConversationThread = vi.fn(async () => ({
kind: "codex-app-server-session" as const,
@@ -1416,11 +1422,13 @@ describe("codex command", () => {
});
expect(startCodexConversationThread).toHaveBeenCalledWith({
pluginConfig: undefined,
config: {},
sessionFile,
workspaceDir: "/repo",
threadId: "thread-123",
model: "gpt-5.4",
modelProvider: "openai",
authProfileId: "openai-codex:work",
});
expect(requestConversationBinding).toHaveBeenCalledWith({
summary: "Codex app-server thread thread-123 in /repo",

View File

@@ -1,10 +1,30 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const sharedClientMocks = vi.hoisted(() => ({
getSharedCodexAppServerClient: vi.fn(),
}));
const agentRuntimeMocks = vi.hoisted(() => ({
ensureAuthProfileStore: vi.fn(),
loadAuthProfileStoreForSecretsRuntime: vi.fn(),
resolveApiKeyForProfile: vi.fn(),
resolveAuthProfileOrder: vi.fn(),
resolveOpenClawAgentDir: vi.fn(() => "/agent"),
resolvePersistedAuthProfileOwnerAgentDir: vi.fn(),
resolveProviderIdForAuth: vi.fn((provider: string) => provider),
saveAuthProfileStore: vi.fn(),
}));
vi.mock("./app-server/shared-client.js", () => sharedClientMocks);
vi.mock("openclaw/plugin-sdk/agent-runtime", () => agentRuntimeMocks);
import {
handleCodexConversationBindingResolved,
handleCodexConversationInboundClaim,
startCodexConversationThread,
} from "./conversation-binding.js";
let tempDir: string;
@@ -15,9 +35,135 @@ describe("codex conversation binding", () => {
});
afterEach(async () => {
sharedClientMocks.getSharedCodexAppServerClient.mockReset();
agentRuntimeMocks.ensureAuthProfileStore.mockReset();
agentRuntimeMocks.loadAuthProfileStoreForSecretsRuntime.mockReset();
agentRuntimeMocks.resolveApiKeyForProfile.mockReset();
agentRuntimeMocks.resolveAuthProfileOrder.mockReset();
agentRuntimeMocks.resolveOpenClawAgentDir.mockClear();
agentRuntimeMocks.resolvePersistedAuthProfileOwnerAgentDir.mockReset();
agentRuntimeMocks.resolveProviderIdForAuth.mockClear();
agentRuntimeMocks.saveAuthProfileStore.mockReset();
await fs.rm(tempDir, { recursive: true, force: true });
});
beforeEach(() => {
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({ version: 1, profiles: {} });
agentRuntimeMocks.resolveAuthProfileOrder.mockReturnValue([]);
agentRuntimeMocks.resolveOpenClawAgentDir.mockReturnValue("/agent");
agentRuntimeMocks.resolveProviderIdForAuth.mockImplementation((provider: string) => provider);
});
it("uses the default Codex auth profile and omits the public OpenAI provider for new binds", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const config = { auth: { order: { "openai-codex": ["openai-codex:default"] } } };
const requests: Array<{ method: string; params: Record<string, unknown> }> = [];
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
profiles: {
"openai-codex:default": {
type: "oauth",
provider: "openai-codex",
access: "access-token",
},
},
});
agentRuntimeMocks.resolveAuthProfileOrder.mockReturnValue(["openai-codex:default"]);
sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
request: vi.fn(async (method: string, requestParams: Record<string, unknown>) => {
requests.push({ method, params: requestParams });
return {
thread: { id: "thread-new", cwd: tempDir },
model: "gpt-5.4-mini",
};
}),
});
await startCodexConversationThread({
config: config as never,
sessionFile,
workspaceDir: tempDir,
model: "gpt-5.4-mini",
modelProvider: "openai",
});
expect(agentRuntimeMocks.resolveAuthProfileOrder).toHaveBeenCalledWith(
expect.objectContaining({ cfg: config, provider: "openai-codex" }),
);
expect(sharedClientMocks.getSharedCodexAppServerClient).toHaveBeenCalledWith(
expect.objectContaining({ authProfileId: "openai-codex:default" }),
);
expect(requests).toHaveLength(1);
expect(requests[0]).toMatchObject({
method: "thread/start",
params: expect.objectContaining({ model: "gpt-5.4-mini" }),
});
expect(requests[0]?.params).not.toHaveProperty("modelProvider");
await expect(fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8")).resolves.toContain(
'"authProfileId": "openai-codex:default"',
);
});
it("preserves Codex auth and omits the public OpenAI provider for native bind threads", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
profiles: {
work: {
type: "oauth",
provider: "openai-codex",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
},
},
});
await fs.writeFile(
`${sessionFile}.codex-app-server.json`,
JSON.stringify({
schemaVersion: 1,
threadId: "thread-old",
cwd: tempDir,
authProfileId: "work",
modelProvider: "openai",
}),
);
const requests: Array<{ method: string; params: Record<string, unknown> }> = [];
sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
request: vi.fn(async (method: string, requestParams: Record<string, unknown>) => {
requests.push({ method, params: requestParams });
return {
thread: { id: "thread-new", cwd: tempDir },
model: "gpt-5.4-mini",
modelProvider: "openai",
};
}),
});
await startCodexConversationThread({
sessionFile,
workspaceDir: tempDir,
model: "gpt-5.4-mini",
modelProvider: "openai",
});
expect(sharedClientMocks.getSharedCodexAppServerClient).toHaveBeenCalledWith(
expect.objectContaining({ authProfileId: "work" }),
);
expect(requests).toHaveLength(1);
expect(requests[0]).toMatchObject({
method: "thread/start",
params: expect.objectContaining({ model: "gpt-5.4-mini" }),
});
expect(requests[0]?.params).not.toHaveProperty("modelProvider");
await expect(fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8")).resolves.toContain(
'"authProfileId": "work"',
);
await expect(
fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8"),
).resolves.not.toContain('"modelProvider": "openai"');
});
it("clears the Codex app-server sidecar when a pending bind is denied", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const sidecar = `${sessionFile}.codex-app-server.json`;
@@ -73,4 +219,76 @@ describe("codex conversation binding", () => {
expect(result).toEqual({ handled: true });
});
it("returns a clean failure reply when app-server turn start rejects", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
await fs.writeFile(
`${sessionFile}.codex-app-server.json`,
JSON.stringify({
schemaVersion: 1,
threadId: "thread-1",
cwd: tempDir,
authProfileId: "openai-codex:work",
}),
);
const unhandledRejections: unknown[] = [];
const onUnhandledRejection = (reason: unknown) => {
unhandledRejections.push(reason);
};
process.on("unhandledRejection", onUnhandledRejection);
sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
request: vi.fn(async (method: string) => {
if (method === "turn/start") {
throw new Error(
"unexpected status 401 Unauthorized: Missing bearer or basic authentication in header",
);
}
throw new Error(`unexpected method: ${method}`);
}),
addNotificationHandler: vi.fn(() => () => undefined),
addRequestHandler: vi.fn(() => () => undefined),
});
try {
const result = await handleCodexConversationInboundClaim(
{
content: "hi",
bodyForAgent: "hi",
channel: "telegram",
isGroup: false,
commandAuthorized: true,
},
{
channelId: "telegram",
pluginBinding: {
bindingId: "binding-1",
pluginId: "codex",
pluginRoot: tempDir,
channel: "telegram",
accountId: "default",
conversationId: "5185575566",
boundAt: Date.now(),
data: {
kind: "codex-app-server-session",
version: 1,
sessionFile,
workspaceDir: tempDir,
},
},
},
{ timeoutMs: 50 },
);
await new Promise<void>((resolve) => setImmediate(resolve));
expect(result).toEqual({
handled: true,
reply: {
text: "Codex app-server turn failed: unexpected status 401 Unauthorized: Missing bearer or basic authentication in header",
},
});
expect(unhandledRejections).toEqual([]);
} finally {
process.off("unhandledRejection", onUnhandledRejection);
}
});
});

View File

@@ -5,6 +5,7 @@ import type {
PluginHookInboundClaimEvent,
} from "openclaw/plugin-sdk/plugin-entry";
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-payload";
import { resolveCodexAppServerAuthProfileIdForAgent } from "./app-server/auth-bridge.js";
import { CODEX_CONTROL_METHODS } from "./app-server/capabilities.js";
import {
codexSandboxPolicyForTurn,
@@ -18,8 +19,11 @@ import {
} from "./app-server/protocol.js";
import {
clearCodexAppServerBinding,
isCodexAppServerNativeAuthProfile,
normalizeCodexAppServerBindingModelProvider,
readCodexAppServerBinding,
writeCodexAppServerBinding,
type CodexAppServerAuthProfileLookup,
} from "./app-server/session-binding.js";
import { getSharedCodexAppServerClient } from "./app-server/shared-client.js";
import {
@@ -47,11 +51,13 @@ type CodexConversationRunOptions = {
type CodexConversationStartParams = {
pluginConfig?: unknown;
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
sessionFile: string;
workspaceDir?: string;
threadId?: string;
model?: string;
modelProvider?: string;
authProfileId?: string;
};
type BoundTurnResult = {
@@ -77,6 +83,13 @@ export async function startCodexConversationThread(
): Promise<CodexConversationBindingData> {
const workspaceDir =
params.workspaceDir?.trim() || resolveCodexDefaultWorkspaceDir(params.pluginConfig);
const existingBinding = await readCodexAppServerBinding(params.sessionFile, {
config: params.config,
});
const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
authProfileId: params.authProfileId ?? existingBinding?.authProfileId,
config: params.config,
});
if (params.threadId?.trim()) {
await attachExistingThread({
pluginConfig: params.pluginConfig,
@@ -85,6 +98,8 @@ export async function startCodexConversationThread(
workspaceDir,
model: params.model,
modelProvider: params.modelProvider,
authProfileId,
config: params.config,
});
} else {
await createThread({
@@ -93,6 +108,8 @@ export async function startCodexConversationThread(
workspaceDir,
model: params.model,
modelProvider: params.modelProvider,
authProfileId,
config: params.config,
});
}
return createCodexConversationBindingData({
@@ -158,18 +175,26 @@ async function attachExistingThread(params: {
workspaceDir: string;
model?: string;
modelProvider?: string;
authProfileId?: string;
config?: CodexAppServerAuthProfileLookup["config"];
}): Promise<void> {
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
const modelProvider = resolveThreadRequestModelProvider({
authProfileId: params.authProfileId,
modelProvider: params.modelProvider,
config: params.config,
});
const client = await getSharedCodexAppServerClient({
startOptions: runtime.start,
timeoutMs: runtime.requestTimeoutMs,
authProfileId: params.authProfileId,
});
const response: CodexThreadResumeResponse = await client.request(
CODEX_CONTROL_METHODS.resumeThread,
{
threadId: params.threadId,
...(params.model ? { model: params.model } : {}),
...(params.modelProvider ? { modelProvider: params.modelProvider } : {}),
...(modelProvider ? { modelProvider } : {}),
approvalPolicy: runtime.approvalPolicy,
approvalsReviewer: runtime.approvalsReviewer,
sandbox: runtime.sandbox,
@@ -179,15 +204,26 @@ async function attachExistingThread(params: {
{ timeoutMs: runtime.requestTimeoutMs },
);
const thread = response.thread;
await writeCodexAppServerBinding(params.sessionFile, {
threadId: thread.id,
cwd: thread.cwd ?? params.workspaceDir,
model: response.model ?? params.model,
modelProvider: response.modelProvider ?? params.modelProvider,
approvalPolicy: runtime.approvalPolicy,
sandbox: runtime.sandbox,
serviceTier: runtime.serviceTier,
});
await writeCodexAppServerBinding(
params.sessionFile,
{
threadId: thread.id,
cwd: thread.cwd ?? params.workspaceDir,
authProfileId: params.authProfileId,
model: response.model ?? params.model,
modelProvider: normalizeCodexAppServerBindingModelProvider({
config: params.config,
authProfileId: params.authProfileId,
modelProvider: response.modelProvider ?? params.modelProvider,
}),
approvalPolicy: runtime.approvalPolicy,
sandbox: runtime.sandbox,
serviceTier: runtime.serviceTier,
},
{
config: params.config,
},
);
}
async function createThread(params: {
@@ -196,18 +232,26 @@ async function createThread(params: {
workspaceDir: string;
model?: string;
modelProvider?: string;
authProfileId?: string;
config?: CodexAppServerAuthProfileLookup["config"];
}): Promise<void> {
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
const modelProvider = resolveThreadRequestModelProvider({
authProfileId: params.authProfileId,
modelProvider: params.modelProvider,
config: params.config,
});
const client = await getSharedCodexAppServerClient({
startOptions: runtime.start,
timeoutMs: runtime.requestTimeoutMs,
authProfileId: params.authProfileId,
});
const response: CodexThreadStartResponse = await client.request(
"thread/start",
{
cwd: params.workspaceDir,
...(params.model ? { model: params.model } : {}),
...(params.modelProvider ? { modelProvider: params.modelProvider } : {}),
...(modelProvider ? { modelProvider } : {}),
approvalPolicy: runtime.approvalPolicy,
approvalsReviewer: runtime.approvalsReviewer,
sandbox: runtime.sandbox,
@@ -219,15 +263,26 @@ async function createThread(params: {
},
{ timeoutMs: runtime.requestTimeoutMs },
);
await writeCodexAppServerBinding(params.sessionFile, {
threadId: response.thread.id,
cwd: response.thread.cwd ?? params.workspaceDir,
model: response.model ?? params.model,
modelProvider: response.modelProvider ?? params.modelProvider,
approvalPolicy: runtime.approvalPolicy,
sandbox: runtime.sandbox,
serviceTier: runtime.serviceTier,
});
await writeCodexAppServerBinding(
params.sessionFile,
{
threadId: response.thread.id,
cwd: response.thread.cwd ?? params.workspaceDir,
authProfileId: params.authProfileId,
model: response.model ?? params.model,
modelProvider: normalizeCodexAppServerBindingModelProvider({
config: params.config,
authProfileId: params.authProfileId,
modelProvider: response.modelProvider ?? params.modelProvider,
}),
approvalPolicy: runtime.approvalPolicy,
sandbox: runtime.sandbox,
serviceTier: runtime.serviceTier,
},
{
config: params.config,
},
);
}
async function runBoundTurn(params: {
@@ -342,10 +397,30 @@ function enqueueBoundTurn<T>(key: string, run: () => Promise<T>): Promise<T> {
() => undefined,
);
state.queues.set(key, queued);
void next.finally(() => {
if (state.queues.get(key) === queued) {
state.queues.delete(key);
}
});
void next
.finally(() => {
if (state.queues.get(key) === queued) {
state.queues.delete(key);
}
})
.catch(() => undefined);
return next;
}
function resolveThreadRequestModelProvider(params: {
authProfileId?: string;
modelProvider?: string;
config?: CodexAppServerAuthProfileLookup["config"];
}): string | undefined {
const modelProvider = params.modelProvider?.trim();
if (!modelProvider || modelProvider.toLowerCase() === "codex") {
return undefined;
}
if (
isCodexAppServerNativeAuthProfile(params) &&
(modelProvider.toLowerCase() === "openai" || modelProvider.toLowerCase() === "openai-codex")
) {
return undefined;
}
return modelProvider.toLowerCase() === "openai-codex" ? "openai" : modelProvider;
}

View File

@@ -1,24 +1,37 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { clearRuntimeAuthProfileStoreSnapshots } from "openclaw/plugin-sdk/agent-runtime";
import { upsertAuthProfile } from "openclaw/plugin-sdk/provider-auth";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
readCodexAppServerBinding,
writeCodexAppServerBinding,
} from "./app-server/session-binding.js";
import {
setCodexConversationFastMode,
setCodexConversationModel,
setCodexConversationPermissions,
} from "./conversation-control.js";
let tempDir: string;
const sharedClientMocks = vi.hoisted(() => ({
getSharedCodexAppServerClient: vi.fn(),
}));
vi.mock("./app-server/shared-client.js", () => sharedClientMocks);
describe("codex conversation controls", () => {
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-control-"));
vi.stubEnv("OPENCLAW_STATE_DIR", tempDir);
sharedClientMocks.getSharedCodexAppServerClient.mockReset();
});
afterEach(async () => {
vi.unstubAllEnvs();
clearRuntimeAuthProfileStoreSnapshots();
await fs.rm(tempDir, { recursive: true, force: true });
});
@@ -47,4 +60,46 @@ describe("codex conversation controls", () => {
sandbox: "workspace-write",
});
});
it("does not persist public OpenAI provider after model changes on native auth bindings", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
upsertAuthProfile({
profileId: "work",
credential: {
type: "oauth",
provider: "openai-codex",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
},
});
await writeCodexAppServerBinding(sessionFile, {
threadId: "thread-1",
cwd: tempDir,
authProfileId: "work",
model: "gpt-5.4",
modelProvider: "openai",
});
sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
request: vi.fn(async () => ({
thread: { id: "thread-1", cwd: tempDir },
model: "gpt-5.5",
modelProvider: "openai",
})),
});
await expect(setCodexConversationModel({ sessionFile, model: "gpt-5.5" })).resolves.toBe(
"Codex model set to gpt-5.5.",
);
const raw = await fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8");
const binding = await readCodexAppServerBinding(sessionFile);
expect(raw).not.toContain('"modelProvider": "openai"');
expect(binding).toMatchObject({
threadId: "thread-1",
authProfileId: "work",
model: "gpt-5.5",
});
expect(binding?.modelProvider).toBeUndefined();
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/comfy-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw ComfyUI provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/copilot-proxy",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Copilot Proxy provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepgram-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Deepgram media-understanding provider",
"type": "module",

View File

@@ -232,6 +232,8 @@ function createDeepgramRealtimeTranscriptionSession(
reconnectDelayMs: DEEPGRAM_REALTIME_RECONNECT_DELAY_MS,
maxQueuedBytes: DEEPGRAM_REALTIME_MAX_QUEUED_BYTES,
connectTimeoutMessage: "Deepgram realtime transcription connection timeout",
connectClosedBeforeReadyMessage:
"Deepgram realtime transcription connection closed before ready",
reconnectLimitMessage: "Deepgram realtime transcription reconnect limit reached",
sendAudio: (audio, transport) => {
transport.sendBinary(audio);

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepinfra-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw DeepInfra provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/deepseek-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw DeepSeek provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-otel",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw diagnostics OpenTelemetry exporter",
"repository": {
"type": "git",
@@ -34,10 +34,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-prometheus",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw diagnostics Prometheus exporter",
"repository": {
"type": "git",
@@ -21,10 +21,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diffs",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw diff viewer plugin",
"repository": {
"type": "git",
@@ -30,10 +30,10 @@
"minHostVersion": ">=2026.4.30"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3",
"openclawVersion": "2026.5.3-beta.3",
"staticAssets": [
{
"source": "./assets/viewer-runtime.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/discord",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Discord channel plugin",
"repository": {
"type": "git",
@@ -21,7 +21,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -65,10 +65,10 @@
"allowInvalidConfigRecovery": true
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -75,6 +75,44 @@ describe("discord doctor", () => {
]);
});
it("normalizes legacy discord streaming progress config", () => {
const normalize = discordDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();
if (!normalize) {
return;
}
const result = normalize({
cfg: {
channels: {
discord: {
streaming: {
mode: "partial",
progress: {
label: "Working",
maxLines: 3,
toolProgress: false,
},
},
},
},
} as never,
});
expect(result.config.channels?.discord).toEqual({
streaming: {
mode: "partial",
preview: {
toolProgress: false,
},
},
});
expect(result.changes).toEqual([
"Moved channels.discord.streaming.progress.toolProgress → channels.discord.streaming.preview.toolProgress.",
"Removed channels.discord.streaming.progress legacy object.",
]);
});
it("moves account voice.tts.edge into providers.microsoft", () => {
const normalize = discordDoctor.normalizeCompatibilityConfig;
expect(normalize).toBeDefined();

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/document-extract-plugin",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw local document extraction plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/duckduckgo-plugin",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw DuckDuckGo plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/elevenlabs-speech",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw ElevenLabs speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/exa-plugin",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Exa plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/fal-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw fal provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/feishu",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
"repository": {
"type": "git",
@@ -16,7 +16,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -47,10 +47,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/file-transfer",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw file transfer plugin (file_fetch, dir_list, dir_fetch, file_write)",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/firecrawl-plugin",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Firecrawl plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/fireworks-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Fireworks provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/github-copilot-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw GitHub Copilot provider plugin",
"type": "module",

View File

@@ -1866,51 +1866,59 @@ describe("google-meet plugin", () => {
});
it("grants local Chrome Meet media permissions against the opened tab", async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", { value: "darwin" });
const callGatewayFromCli = mockLocalMeetBrowserRequest({
inCall: true,
micMuted: false,
title: "Meet call",
url: "https://meet.google.com/abc-defg-hij",
});
const { methods } = setup({
defaultMode: "realtime",
defaultTransport: "chrome",
chrome: {
audioBridgeCommand: ["bridge", "start"],
},
realtime: { introMessage: "" },
});
const handler = methods.get("googlemeet.join") as
| ((ctx: {
params: Record<string, unknown>;
respond: ReturnType<typeof vi.fn>;
}) => Promise<void>)
| undefined;
const respond = vi.fn();
try {
const { methods } = setup({
defaultMode: "realtime",
defaultTransport: "chrome",
chrome: {
audioBridgeCommand: ["bridge", "start"],
},
realtime: { introMessage: "" },
});
const handler = methods.get("googlemeet.join") as
| ((ctx: {
params: Record<string, unknown>;
respond: ReturnType<typeof vi.fn>;
}) => Promise<void>)
| undefined;
const respond = vi.fn();
await handler?.({
params: { url: "https://meet.google.com/abc-defg-hij" },
respond,
});
await handler?.({
params: { url: "https://meet.google.com/abc-defg-hij" },
respond,
});
expect(respond.mock.calls[0]?.[0]).toBe(true);
expect(callGatewayFromCli).toHaveBeenCalledWith(
"browser.request",
expect.any(Object),
expect.objectContaining({
method: "POST",
path: "/permissions/grant",
body: expect.objectContaining({
origin: "https://meet.google.com",
permissions: ["audioCapture", "videoCapture"],
targetId: "local-meet-tab",
expect(respond.mock.calls[0]?.[0]).toBe(true);
expect(callGatewayFromCli).toHaveBeenCalledWith(
"browser.request",
expect.any(Object),
expect.objectContaining({
method: "POST",
path: "/permissions/grant",
body: expect.objectContaining({
origin: "https://meet.google.com",
permissions: ["audioCapture", "videoCapture"],
targetId: "local-meet-tab",
}),
}),
}),
{ progress: false },
);
{ progress: false },
);
} finally {
Object.defineProperty(process, "platform", { value: originalPlatform });
}
});
it("starts the local realtime audio bridge after Meet is inspected", async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", { value: "darwin" });
const events: string[] = [];
const callGatewayFromCli = vi.fn(
async (
@@ -1951,43 +1959,51 @@ describe("google-meet plugin", () => {
},
);
chromeTransportTesting.setDepsForTest({ callGatewayFromCli });
const { methods } = setup(
{
defaultMode: "realtime",
defaultTransport: "chrome",
chrome: {
audioBridgeCommand: ["bridge", "start"],
try {
const { methods } = setup(
{
defaultMode: "realtime",
defaultTransport: "chrome",
chrome: {
audioBridgeCommand: ["bridge", "start"],
},
realtime: { introMessage: "" },
},
realtime: { introMessage: "" },
},
{
runCommandWithTimeoutHandler: async (argv) => {
events.push(`command:${argv.join(" ")}`);
return argv[0] === "/usr/sbin/system_profiler"
? { code: 0, stdout: "BlackHole 2ch", stderr: "" }
: { code: 0, stdout: "", stderr: "" };
{
runCommandWithTimeoutHandler: async (argv) => {
events.push(`command:${argv.join(" ")}`);
return argv[0] === "/usr/sbin/system_profiler"
? { code: 0, stdout: "BlackHole 2ch", stderr: "" }
: { code: 0, stdout: "", stderr: "" };
},
},
},
);
const handler = methods.get("googlemeet.join") as
| ((ctx: {
params: Record<string, unknown>;
respond: ReturnType<typeof vi.fn>;
}) => Promise<void>)
| undefined;
const respond = vi.fn();
);
const handler = methods.get("googlemeet.join") as
| ((ctx: {
params: Record<string, unknown>;
respond: ReturnType<typeof vi.fn>;
}) => Promise<void>)
| undefined;
const respond = vi.fn();
await handler?.({
params: { url: "https://meet.google.com/abc-defg-hij" },
respond,
});
await handler?.({
params: { url: "https://meet.google.com/abc-defg-hij" },
respond,
});
expect(respond.mock.calls[0]?.[0]).toBe(true);
expect(events.indexOf("browser:/act")).toBeGreaterThan(-1);
expect(events.indexOf("command:bridge start")).toBeGreaterThan(events.indexOf("browser:/act"));
expect(respond.mock.calls[0]?.[0]).toBe(true);
expect(events.indexOf("browser:/act")).toBeGreaterThan(-1);
expect(events.indexOf("command:bridge start")).toBeGreaterThan(
events.indexOf("browser:/act"),
);
} finally {
Object.defineProperty(process, "platform", { value: originalPlatform });
}
});
it("does not start the local realtime audio bridge while Meet admission is pending", async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", { value: "darwin" });
const events: string[] = [];
const callGatewayFromCli = vi.fn(
async (
@@ -2028,41 +2044,45 @@ describe("google-meet plugin", () => {
},
);
chromeTransportTesting.setDepsForTest({ callGatewayFromCli });
const { methods } = setup(
{
defaultMode: "realtime",
defaultTransport: "chrome",
chrome: {
audioBridgeCommand: ["bridge", "start"],
waitForInCallMs: 1,
try {
const { methods } = setup(
{
defaultMode: "realtime",
defaultTransport: "chrome",
chrome: {
audioBridgeCommand: ["bridge", "start"],
waitForInCallMs: 1,
},
realtime: { introMessage: "" },
},
realtime: { introMessage: "" },
},
{
runCommandWithTimeoutHandler: async (argv) => {
events.push(`command:${argv.join(" ")}`);
return argv[0] === "/usr/sbin/system_profiler"
? { code: 0, stdout: "BlackHole 2ch", stderr: "" }
: { code: 0, stdout: "", stderr: "" };
{
runCommandWithTimeoutHandler: async (argv) => {
events.push(`command:${argv.join(" ")}`);
return argv[0] === "/usr/sbin/system_profiler"
? { code: 0, stdout: "BlackHole 2ch", stderr: "" }
: { code: 0, stdout: "", stderr: "" };
},
},
},
);
const handler = methods.get("googlemeet.join") as
| ((ctx: {
params: Record<string, unknown>;
respond: ReturnType<typeof vi.fn>;
}) => Promise<void>)
| undefined;
const respond = vi.fn();
);
const handler = methods.get("googlemeet.join") as
| ((ctx: {
params: Record<string, unknown>;
respond: ReturnType<typeof vi.fn>;
}) => Promise<void>)
| undefined;
const respond = vi.fn();
await handler?.({
params: { url: "https://meet.google.com/abc-defg-hij" },
respond,
});
await handler?.({
params: { url: "https://meet.google.com/abc-defg-hij" },
respond,
});
expect(respond.mock.calls[0]?.[0]).toBe(true);
expect(events).toContain("browser:/act");
expect(events).not.toContain("command:bridge start");
expect(respond.mock.calls[0]?.[0]).toBe(true);
expect(events).toContain("browser:/act");
expect(events).not.toContain("command:bridge start");
} finally {
Object.defineProperty(process, "platform", { value: originalPlatform });
}
});
it("refreshes observe-only caption health when status is requested", async () => {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/google-meet",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Google Meet participant plugin",
"repository": {
"type": "git",
@@ -16,7 +16,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -33,10 +33,10 @@
"minHostVersion": ">=2026.4.20"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/google-plugin",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Google plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/googlechat",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Google Chat channel plugin",
"repository": {
"type": "git",
@@ -17,7 +17,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -75,10 +75,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/gradium-speech",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Gradium speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/groq-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Groq media-understanding provider",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/huggingface-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Hugging Face provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/image-generation-core",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw image generation runtime package",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/imessage",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw iMessage channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/inworld-speech",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Inworld speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/irc",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw IRC channel plugin",
"type": "module",
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/kilocode-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Kilo Gateway provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/kimi-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Kimi provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/line",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw LINE channel plugin",
"repository": {
"type": "git",
@@ -15,7 +15,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -45,10 +45,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/litellm-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw LiteLLM provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/llm-task",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw JSON-only LLM task plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/lmstudio-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw LM Studio provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/lobster",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
"repository": {
"type": "git",
@@ -25,10 +25,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/matrix",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Matrix channel plugin",
"repository": {
"type": "git",
@@ -21,7 +21,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/mattermost",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Mattermost channel plugin",
"repository": {
"type": "git",
@@ -15,7 +15,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/media-understanding-core",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw media understanding runtime package",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/memory-core",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw core memory search plugin",
"type": "module",
@@ -13,7 +13,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/memory-lancedb",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw LanceDB-backed long-term memory plugin with auto-recall/capture",
"repository": {
"type": "git",
@@ -26,10 +26,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/memory-wiki",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw persistent wiki plugin",
"type": "module",
@@ -13,7 +13,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/microsoft-foundry",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Microsoft Foundry provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/microsoft-speech",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Microsoft speech plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/migrate-claude",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "Claude to OpenClaw migration provider",
"type": "module",
@@ -9,7 +9,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/migrate-hermes",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "Hermes to OpenClaw migration provider",
"type": "module",
@@ -12,7 +12,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/minimax-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw MiniMax provider and OAuth plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/mistral-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Mistral provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/moonshot-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Moonshot provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/msteams",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Microsoft Teams channel plugin",
"repository": {
"type": "git",
@@ -22,7 +22,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -58,10 +58,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/nextcloud-talk",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Nextcloud Talk channel plugin",
"repository": {
"type": "git",
@@ -15,7 +15,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -47,10 +47,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/nostr",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
"repository": {
"type": "git",
@@ -16,7 +16,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.3"
"openclaw": ">=2026.5.3-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -54,10 +54,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.3"
"pluginApi": ">=2026.5.3-beta.3"
},
"build": {
"openclawVersion": "2026.5.3"
"openclawVersion": "2026.5.3-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/nvidia-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw NVIDIA provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/ollama-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw Ollama provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/open-prose",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenProse VM skill pack plugin (slash command + telemetry).",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/openai-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw OpenAI provider plugins",
"type": "module",

View File

@@ -54,22 +54,17 @@ type SentRealtimeEvent = {
type: string;
audio?: string;
session?: {
type?: string;
audio?: {
input?: {
format?: { type?: string };
transcription?: {
model?: string;
language?: string;
prompt?: string;
};
turn_detection?: {
type?: string;
threshold?: number;
prefix_padding_ms?: number;
silence_duration_ms?: number;
};
};
input_audio_format?: string;
input_audio_transcription?: {
model?: string;
language?: string;
prompt?: string;
};
turn_detection?: {
type?: string;
threshold?: number;
prefix_padding_ms?: number;
silence_duration_ms?: number;
};
};
};
@@ -180,22 +175,17 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
{
type: "transcription_session.update",
session: {
type: "transcription",
audio: {
input: {
format: { type: "audio/pcmu" },
transcription: {
model: "gpt-4o-transcribe",
language: "en",
prompt: "expect OpenClaw product names",
},
turn_detection: {
type: "server_vad",
threshold: 0.45,
prefix_padding_ms: 300,
silence_duration_ms: 900,
},
},
input_audio_format: "g711_ulaw",
input_audio_transcription: {
model: "gpt-4o-transcribe",
language: "en",
prompt: "expect OpenClaw product names",
},
turn_detection: {
type: "server_vad",
threshold: 0.45,
prefix_padding_ms: 300,
silence_duration_ms: 900,
},
},
},
@@ -209,22 +199,17 @@ describe("buildOpenAIRealtimeTranscriptionProvider", () => {
{
type: "transcription_session.update",
session: {
type: "transcription",
audio: {
input: {
format: { type: "audio/pcmu" },
transcription: {
model: "gpt-4o-transcribe",
language: "en",
prompt: "expect OpenClaw product names",
},
turn_detection: {
type: "server_vad",
threshold: 0.45,
prefix_padding_ms: 300,
silence_duration_ms: 900,
},
},
input_audio_format: "g711_ulaw",
input_audio_transcription: {
model: "gpt-4o-transcribe",
language: "en",
prompt: "expect OpenClaw product names",
},
turn_detection: {
type: "server_vad",
threshold: 0.45,
prefix_padding_ms: 300,
silence_duration_ms: 900,
},
},
},

View File

@@ -138,6 +138,7 @@ function createOpenAIRealtimeTranscriptionSession(
maxReconnectAttempts: OPENAI_REALTIME_TRANSCRIPTION_MAX_RECONNECT_ATTEMPTS,
reconnectDelayMs: OPENAI_REALTIME_TRANSCRIPTION_RECONNECT_DELAY_MS,
connectTimeoutMessage: "OpenAI realtime transcription connection timeout",
connectClosedBeforeReadyMessage: "OpenAI realtime transcription connection closed before ready",
reconnectLimitMessage: "OpenAI realtime transcription reconnect limit reached",
sendAudio: (audio, transport) => {
transport.sendJson({
@@ -149,24 +150,17 @@ function createOpenAIRealtimeTranscriptionSession(
transport.sendJson({
type: "transcription_session.update",
session: {
type: "transcription",
audio: {
input: {
format: {
type: "audio/pcmu",
},
transcription: {
model: config.model,
...(config.language ? { language: config.language } : {}),
...(config.prompt ? { prompt: config.prompt } : {}),
},
turn_detection: {
type: "server_vad",
threshold: config.vadThreshold,
prefix_padding_ms: 300,
silence_duration_ms: config.silenceDurationMs,
},
},
input_audio_format: "g711_ulaw",
input_audio_transcription: {
model: config.model,
...(config.language ? { language: config.language } : {}),
...(config.prompt ? { prompt: config.prompt } : {}),
},
turn_detection: {
type: "server_vad",
threshold: config.vadThreshold,
prefix_padding_ms: 300,
silence_duration_ms: config.silenceDurationMs,
},
},
});

View File

@@ -394,6 +394,27 @@ describe("buildOpenAIRealtimeVoiceProvider", () => {
expect(bridge.isConnected()).toBe(false);
});
it("rejects connection when the socket closes before session readiness", async () => {
const provider = buildOpenAIRealtimeVoiceProvider();
const bridge = provider.createBridge({
providerConfig: { apiKey: "sk-test" }, // pragma: allowlist secret
onAudio: vi.fn(),
onClearAudio: vi.fn(),
});
const connecting = bridge.connect();
const socket = FakeWebSocket.instances[0];
if (!socket) {
throw new Error("expected bridge to create a websocket");
}
socket.readyState = FakeWebSocket.OPEN;
socket.emit("open");
socket.close(1006, "session closed");
await expect(connecting).rejects.toThrow("OpenAI realtime connection closed before ready");
expect(bridge.isConnected()).toBe(false);
});
it("can request PCM16 24 kHz realtime audio for Chrome command-pair bridges", async () => {
const provider = buildOpenAIRealtimeVoiceProvider();
const bridge = provider.createBridge({

View File

@@ -425,6 +425,10 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
this.config.onClose?.("completed");
return;
}
if (!this.sessionConfigured && !settled) {
settleReject(new Error("OpenAI realtime connection closed before ready"));
return;
}
void this.attemptReconnect();
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/opencode-go-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw OpenCode Go provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/opencode-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw OpenCode Zen provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/openrouter-provider",
"version": "2026.5.3",
"version": "2026.5.3-beta.3",
"private": true,
"description": "OpenClaw OpenRouter provider plugin",
"type": "module",

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