mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
34 Commits
josh/gmail
...
v2026.5.3-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aab2a64781 | ||
|
|
7bc9bdad7b | ||
|
|
363b7fb260 | ||
|
|
6878c22de9 | ||
|
|
bd28223914 | ||
|
|
788c896715 | ||
|
|
39c11560ee | ||
|
|
e922bed9ce | ||
|
|
e5f4cb3644 | ||
|
|
b190fae70c | ||
|
|
df43768465 | ||
|
|
eadc3ee699 | ||
|
|
d35303582a | ||
|
|
6a1bcb1566 | ||
|
|
9f0a114dab | ||
|
|
62adabf3ce | ||
|
|
728cf41034 | ||
|
|
22c211cb1b | ||
|
|
a389d455c1 | ||
|
|
e5a1fa4c3b | ||
|
|
50f581d97c | ||
|
|
6658cf33ed | ||
|
|
130efb13ce | ||
|
|
c6473d6461 | ||
|
|
cc8ae6ee12 | ||
|
|
54493bde15 | ||
|
|
9c3919ccef | ||
|
|
d5254a7e43 | ||
|
|
6ffb3c3f3a | ||
|
|
70be1cbcd8 | ||
|
|
c28b0081eb | ||
|
|
c9a83707d5 | ||
|
|
d7ce1aafad | ||
|
|
6f0175779e |
5
.github/workflows/plugin-clawhub-release.yml
vendored
5
.github/workflows/plugin-clawhub-release.yml
vendored
@@ -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 }}
|
||||
|
||||
3
.github/workflows/plugin-npm-release.yml
vendored
3
.github/workflows/plugin-npm-release.yml
vendored
@@ -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 }}"
|
||||
|
||||
|
||||
56
CHANGELOG.md
56
CHANGELOG.md
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" }));
|
||||
|
||||
@@ -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.`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user