Compare commits

...

21 Commits

Author SHA1 Message Date
Peter Steinberger
41f028e2ea fix(diagnostics): drop stale session recovery event cases 2026-05-05 06:06:02 +01:00
Peter Steinberger
303ff716d4 chore(release): refresh plugin SDK API baseline 2026-05-05 05:56:41 +01:00
Peter Steinberger
5fcdeae80c chore(release): bump to 2026.5.4-beta.3 2026-05-05 05:51:46 +01:00
6607changchun
b73317c217 fix(sandbox): support Windows drive-letter bind sources
Accept drive-absolute Windows sandbox Docker bind sources in config and runtime validation while keeping blocked-path and allowed-root comparisons case-insensitive for Windows drive paths.

Also remove a stale WhatsApp setup import that blocked extension lint after the rebase.

Co-authored-by: 6607changchun <84566142+6607changchun@users.noreply.github.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
(cherry picked from commit d02fbc6116)
2026-05-05 05:45:56 +01:00
兰之
8f6bf65162 fix(agents): enforce exact skill path from <available_skills> [AI-assisted] (#74161)
Summary:
- The PR updates agents skill prompt guidance to require exact `<location>` paths for single- and multi-skill selection, adds prompt assertions, and records the fix in the changelog.
- Reproducibility: yes. Static source reproduction is enough: current main lacks the exact-`<location>` guard  ... illsSection()`, while the PR diff adds it to both selection branches and asserts the resulting prompt text.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix: enforce exact skill paths for all skill matches

Validation:
- ClawSweeper review passed for head 743c9840c1.
- Required merge gates passed before the squash merge.

Prepared head SHA: 743c9840c1
Review: https://github.com/openclaw/openclaw/pull/74161#issuecomment-4341488109

Co-authored-by: tianguicheng <tianguicheng@xiaomi.com>
Co-authored-by: sallyom <somalley@redhat.com>
(cherry picked from commit c739088d62)
2026-05-05 05:45:56 +01:00
saram ali
8017dc4c3b fix(gateway): skip IPv6 loopback binding on Windows (#69701)
Bind the default loopback gateway listener only to `127.0.0.1` on Windows so libuv dual-stack `::1` behavior cannot wedge localhost HTTP requests.

Also keeps non-Windows dual-loopback behavior covered, replaces the redundant Windows passthrough test with guard coverage, and adds the required changelog entry.

Fixes #69674.

Tests:
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/gateway/net.ts src/gateway/net.test.ts
- pnpm test src/gateway/net.test.ts
- pnpm check:changed
- GitHub required checks: green

Thanks @SARAMALI15792.

Co-authored-by: saram ali <140950904+SARAMALI15792@users.noreply.github.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
(cherry picked from commit 978bc53e80)
2026-05-05 05:45:56 +01:00
Peter Steinberger
578d9072cf test: align beta plugin repair expectations 2026-05-05 05:40:52 +01:00
Vincent Koc
30b73bbf41 fix(plugins): honor beta channel for auto installs
(cherry picked from commit b0f841ef37)
2026-05-05 05:37:52 +01:00
Vincent Koc
ade922ba98 fix(telegram): reuse preview for long text finals (#77658)
* fix(telegram): reuse preview for long text finals

* test(qa): cover long telegram finals

* fix(qa): satisfy extension lint

* fix(qa): keep telegram long final fixture to two chunks

* test(telegram): cover three chunk finals

* fix(telegram): force long final preview boundary

(cherry picked from commit e03fe1e289)
2026-05-05 05:37:52 +01:00
Vincent Koc
997f8af734 fix(whatsapp): normalize onboarding allowlist numbers
Normalize WhatsApp onboarding allowlist entries to digit-only WhatsApp IDs and reject invalid owner-phone inputs during prompt validation.

(cherry picked from commit 68a500c465)
2026-05-05 05:37:52 +01:00
Vincent Koc
6204a6fecc fix(update): authenticate restart health probes
(cherry picked from commit b546aa91e1)
2026-05-05 05:37:25 +01:00
Peter Steinberger
9f15c29397 fix: explain missing git during plugin install
(cherry picked from commit a91c17c426)
2026-05-05 05:23:01 +01:00
Bek
cac973972c fix: slack mention-gating thread participation
(cherry picked from commit cf3ce08b91)
2026-05-05 05:14:29 +01:00
Peter Steinberger
f8f18d53fc fix: start configured generation providers
(cherry picked from commit 0eb06caae3)
2026-05-05 05:10:02 +01:00
pickaxe
696f639cf6 docs: note plugin peer-link update repair
(cherry picked from commit 712aa96a8f)
2026-05-05 05:06:31 +01:00
pickaxe
079b937b46 fix(plugins): repair missing openclaw peer links on update
(cherry picked from commit 2e8761c5c1)
2026-05-05 05:06:31 +01:00
Kelaw - Keshav's Agent
32e36d355d fix: recover missing Codex bound threads
(cherry picked from commit a373468d82)
2026-05-05 04:58:18 +01:00
Peter Steinberger
12e1c67f22 fix(build): route externalized plugin entry chunks 2026-05-05 04:31:46 +01:00
Peter Steinberger
766d02ff3b fix(build): route externalized plugin chunks 2026-05-05 04:23:24 +01:00
Peter Steinberger
e9ebb6ce6c fix(release): prune externalized plugin chunks 2026-05-05 04:15:20 +01:00
Peter Steinberger
e0002c4b5b chore(release): prepare 2026.5.4 beta 2 2026-05-05 02:42:03 +01:00
180 changed files with 2555 additions and 432 deletions

View File

@@ -2,7 +2,7 @@
Docs: https://docs.openclaw.ai
## Unreleased
## 2026.5.4
### Highlights
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
### Changes
- Gateway/Windows: bind the default loopback gateway listener only to `127.0.0.1` on Windows so libuv's dual-stack `::1` behavior cannot wedge localhost HTTP requests. (#69701, fixes #69674) Thanks @SARAMALI15792.
- Plugins/migration: emit catalog-backed install hints when `plugins.entries` or `plugins.allow` references an official external plugin that is not installed, so upgraded configs point operators to `openclaw plugins install <spec>` instead of telling them to remove valid plugin config. (#77483) Thanks @hclsys.
- OpenAI/Codex media: advertise Codex audio transcription in runtime and manifest metadata and route active Codex chat models to the OpenAI transcription default instead of sending chat model ids to audio transcription. Thanks @vincentkoc.
- Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure.
@@ -59,22 +60,32 @@ Docs: https://docs.openclaw.ai
- 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.
- Plugins/runtime state: add `registerIfAbsent` for atomic keyed-store dedupe claims that return whether a plugin successfully claimed a key without overwriting an existing live value. Thanks @amknight.
- Plugin SDK: add plugin-owned `SessionEntry` slot projection and scoped trusted-policy session extension reads. (#75609; replaces part of #73384/#74483) Thanks @100yenadmin.
- Sandbox/Windows: accept drive-absolute Docker bind sources while keeping sandbox blocked-path and allowed-root policy comparisons Windows-case-insensitive. (#42174) Thanks @6607changchun.
### Fixes
- Plugins/install: honor the beta update channel for onboarding and doctor-managed plugin installs by requesting floating npm and ClawHub specs with `@beta` while keeping persistent install records on the catalog default. Thanks @vincentkoc.
- WhatsApp/onboarding: canonicalize setup and pairing allowlist entries to WhatsApp's digit-only phone ids while still accepting E.164, JID, and `whatsapp:` inputs, so personal-phone allowlists match WhatsApp Web sender ids after setup. Thanks @vincentkoc.
- Gateway/startup: load provider plugins that own explicitly configured image, video, or music generation defaults so generation tools become live after gateway restart instead of remaining catalog-only. Fixes #77244. Thanks @buyuangtampan, @Nikoxx99, and @vincentkoc.
- Slack/subagents: keep resumed parent `message.send` calls in the originating Slack thread when ambient session thread context is present, and suppress successful silent child completion rows from follow-up findings. Thanks @bek91.
- Slack/mentions: record thread participation for successful visible threaded Slack sends, including message-tool and media delivery paths, so unmentioned replies in bot-participated threads can bypass mention gating as documented. Fixes #77648. Thanks @bek91.
- Infra/Windows: skip the POSIX `/tmp/openclaw` preferred path on Windows in `resolvePreferredOpenClawTmpDir` so log files, TTS temp files, and other writes land in `%TEMP%\openclaw-<uid>` instead of `C:\tmp\openclaw`. Fixes #60713. Thanks @juan-flores077.
- Media/Windows: open saved attachment temp files read/write before fsync so Windows WebChat and `chat.send` media offloads no longer fail with EPERM during durability flush. (#76593) Thanks @qq230849622-a11y.
- Agents/tools: honor narrow runtime tool allowlists when constructing embedded-runner tool families and bundled MCP/LSP runtimes, so cron/subagent runs that request tools such as `update_plan`, `browser`, `x_search`, channel login tools, or `group:plugins` no longer start with missing tools or unrelated bootstrap work. (#77519, #77532)
- Codex plugin: mirror the experimental upstream app-server protocol and format generated TypeScript before drift checks, keeping OpenClaw's `experimentalApi` bridge compatible with latest Codex while preserving formatter gates.
- Telegram/media: derive no-caption inbound media placeholders from saved MIME metadata instead of the Telegram `photo` shape, so non-image and mixed attachments no longer reach the model as `<media:image>`. Fixes #69793. Thanks @aspalagin.
- Telegram/streaming: reuse the active preview as the first chunk for long text finals, so multi-chunk replies no longer create a transient extra bubble that appears and then disappears. Thanks @vincentkoc.
- Agents/cache: keep per-turn runtime context out of ordinary chat system prompts while still delivering hidden current-turn context, restoring prompt-cache reuse on chat continuations. Fixes #77431. Thanks @Udjin79.
- Gateway/startup: include resolved thinking and fast-mode defaults in the `agent model` startup log line, defaulting unset startup thinking to `medium` without mixing in reasoning visibility.
- Gateway/update: resolve local gateway probe auth from the installed config during post-update restart verification, so token/device-authenticated VPS gateways are not misreported as unhealthy port conflicts after a package swap. Thanks @vincentkoc.
- Agents/Tools: add post-compaction loop guard in `pi-embedded-runner` that arms after auto-compaction-retry and aborts the run with `compaction_loop_persisted` when the agent emits the same `(tool, args, result)` triple `windowSize` times (default 3) within that window. Disable via existing `tools.loopDetection.enabled`; tune via `tools.loopDetection.postCompactionGuard.windowSize`. Targets the failure mode where context-overflow + compaction does not break a tool-call loop. Refs #77474; carries forward #21597. Thanks @efpiva.
- Gateway/watch: suppress sync-I/O trace output during `pnpm gateway:watch --benchmark` unless explicitly requested, so CPU profiling no longer floods the terminal with stack traces.
- Gateway/watch: when benchmark sync-I/O tracing is explicitly enabled, tee trace blocks to the benchmark output log and filter them from the terminal pane while keeping normal Gateway logs visible.
- Plugins/runtime-deps: include `json5` in the memory-core plugin runtime dependency set so packaged `memory_search` sandboxes can resolve generated OpenClaw runtime chunks that parse JSON5 config. Fixes #77461.
- Plugins/Windows: show a Git install hint when npm plugin installation fails with `spawn git ENOENT`, and document the WhatsApp plugin's Git-on-PATH requirement for Baileys/libsignal installs.
- Codex harness: preserve app-server usage-limit reset details and deliver OpenClaw-owned runtime failure notices through tool-only source-reply mode, so Telegram and other chat channels tell users when Codex subscription limits or API failures block a turn instead of going silent. (#77557) Thanks @pashpashpash.
- Agents/OpenAI: default direct OpenAI Responses models to the SSE transport instead of WebSocket auto-selection, preventing pi runtime chat turns from hanging on servers where the WebSocket path stalls while the OpenAI HTTP stream works. Thanks @vincentkoc.
- Plugins/update: repair missing plugin-local `openclaw` peer links before skipping unchanged npm plugin updates, so current external Codex installs can recover `openclaw/plugin-sdk/*` resolution during OTA repair. (#77544) Thanks @ProspectOre.
- Discord/replies: treat failed final reply delivery as a failed turn instead of counting it as a delivered automatic visible reply, so guild/channel turns no longer show done when the final message was dropped. Fixes #77520. Thanks @Patrick-Erichsen.
- Discord: prefer IPv4 for Discord REST and gateway WebSocket startup paths so IPv4-only networks no longer stall before Gateway READY and inbound message dispatch. Fixes #77398; refs #77526. Thanks @Beandon13.
- Channels/plugins: key bundled package-state probes, env/config presence, and read-only command defaults by channel id instead of manifest plugin id, preserving setup and native-command detection for channel plugins whose package id differs from the channel alias. Thanks @vincentkoc.
@@ -216,6 +227,7 @@ Docs: https://docs.openclaw.ai
- Google Meet: make Twilio setup status require an enabled `voice-call` plugin entry instead of treating a missing entry as ready. Thanks @vincentkoc.
- Telegram: render shared interactive reply buttons in reply delivery so plugin approval messages show inline keyboards. (#76238) Thanks @keshavbotagent.
- Cron/sessions: keep cron metadata rows without an on-disk transcript non-resumable until a transcript exists, so doctor and `sessions cleanup --fix-missing` no longer report or prune pre-transcript cron rows as broken sessions. Refs #77011.
- OpenAI Codex: recreate missing bound app-server threads once when a stale `/codex bind` sidecar survives a restart, preserving the selected auth profile and turn overrides before retrying the inbound turn. (#76936) Thanks @keshavbotagent.
- Agents/cli-runner: drop a saved `claude-cli` resume sessionId at preparation time when its on-disk transcript no longer exists in `~/.claude/projects/`, so a stale binding from a half-installed `update.run` cannot trap follow-up runs (auto-reply / Telegram direct) in a `claude --resume` timeout loop; the run starts fresh and the new sessionId is written back through the existing post-run flow. (#77030; refs #77011) Thanks @openperf.
- Release validation: install the cross-OS TypeScript harness through Windows-safe Node/npm shims so native Windows package checks reach the OpenClaw smoke suites instead of exiting before artifact capture. Thanks @vincentkoc.
- Release validation: let Windows packaged-upgrade checks continue after the shipped 2026.5.2 updater hits its native-module swap cleanup fallback, verifying the fallback-installed candidate through package metadata and downstream smoke instead of crashing on the immediate update-status probe. Thanks @vincentkoc.
@@ -1396,6 +1408,7 @@ Docs: https://docs.openclaw.ai
- Gateway/plugins: enable the native `require()` fast path on Windows for bundled plugin modules so plugin loading uses `require()` instead of Jiti's transform pipeline, reducing startup from ~39s to ~2s on typical 6-plugin setups. Fixes #68656. (#74173) Thanks @galiniliev.
- macOS app: detect stale Gateway TLS certificate pins, automatically repair trusted Tailscale Serve rotations, and surface paired-but-disconnected Mac companion nodes so partial Gateway connections no longer look healthy. Thanks @guti.
- Feishu: recreate WebSocket clients with monitor-owned backoff only after SDK reconnect exhaustion, preserving heartbeat defaults and shutdown cleanup without treating recoverable SDK callback errors as terminal, so persistent connections recover without manual gateway restart. Fixes #52618; duplicate evidence #59753; related #55532, #68766, #72411, and #73739. Thanks @vincentkoc, @schumilin, @alex-xuweilong, @120106835, @sirfengyu, and @tianhaocui.
- Agents/skills: require exact `<location>` skill paths for both single-skill and multi-skill prompt selection, so agents do not guess or hard-code skill file paths. (#74161) Thanks @lanzhi-lee.
## 2026.4.27

View File

@@ -1,2 +1,2 @@
43c6f668cd8301f485c64e6a663dc1b19d38c146ce2572943e2dc961973e0c6f plugin-sdk-api-baseline.json
1d877d94bebb634d90d929fe0581ba4bccf4d12d8342d179ae9bf1053e68c013 plugin-sdk-api-baseline.jsonl
50bd395c818460886af1fd545da4e3ace0fc7f7c36a43abd58f0a8cf76659f09 plugin-sdk-api-baseline.json
bc609d44abbd58515f69ec88c947531c679f7f7910208c0761f52fda4481fa6e plugin-sdk-api-baseline.jsonl

View File

@@ -344,6 +344,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
For text-only replies:
- short DM/group/topic previews: OpenClaw keeps the same preview message and performs a final edit in place, unless a visible non-preview message was sent after the preview appeared
- long text finals that split into multiple Telegram messages reuse the existing preview as the first final chunk when possible, then send only the remaining chunks
- previews followed by visible non-preview output: OpenClaw sends the completed reply as a fresh final message and cleans up the older preview, so the final answer appears after intermediate output
- previews older than about one minute: OpenClaw sends the completed reply as a fresh final message and then cleans up the preview, so Telegram's visible timestamp reflects completion time instead of the preview creation time

View File

@@ -26,6 +26,16 @@ openclaw plugins install @openclaw/whatsapp
Use the bare package to follow the current official release tag. Pin an exact
version only when you need a reproducible install.
On Windows, the WhatsApp plugin needs Git on `PATH` during npm install because
one of its Baileys/libsignal dependencies is fetched from a git URL. Install
Git for Windows, then restart the shell and rerun the install:
```powershell
winget install --id Git.Git -e
```
Portable Git also works if its `bin` directory is on `PATH`.
<CardGroup cols={3}>
<Card title="Pairing" icon="link" href="/channels/pairing">
Default DM policy is pairing for unknown senders.

View File

@@ -232,6 +232,8 @@ Scenarios (`extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime
- `telegram-tools-compact-command`
- `telegram-whoami-command`
- `telegram-context-command`
- `telegram-long-final-reuses-preview`
- `telegram-long-final-three-chunks`
Output artifacts:

View File

@@ -18,6 +18,16 @@ Adds the WhatsApp channel surface for sending and receiving OpenClaw messages.
channels: whatsapp
## Windows install note
On Windows, the WhatsApp plugin needs Git on `PATH` during npm install because one of its Baileys/libsignal dependencies is fetched from a git URL. Install Git for Windows, then restart the shell and rerun the install:
```powershell
winget install --id Git.Git -e
```
Portable Git also works if its `bin` directory is on `PATH`.
## Related docs
- [whatsapp](/channels/whatsapp)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/codex",
"version": "2026.5.4",
"version": "2026.5.4-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.4"
"pluginApi": ">=2026.5.4-beta.3"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.4-beta.3"
},
"release": {
"publishToClawHub": true,

View File

@@ -48,7 +48,10 @@ describe("codex conversation binding", () => {
});
beforeEach(() => {
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({ version: 1, profiles: {} });
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
profiles: {},
});
agentRuntimeMocks.resolveAuthProfileOrder.mockReturnValue([]);
agentRuntimeMocks.resolveOpenClawAgentDir.mockReturnValue("/agent");
agentRuntimeMocks.resolveProviderIdForAuth.mockImplementation((provider: string) => provider);
@@ -56,7 +59,9 @@ describe("codex conversation binding", () => {
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 config = {
auth: { order: { "openai-codex": ["openai-codex:default"] } },
};
const requests: Array<{ method: string; params: Record<string, unknown> }> = [];
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
@@ -220,6 +225,142 @@ describe("codex conversation binding", () => {
expect(result).toEqual({ handled: true });
});
it("recreates a missing bound thread and preserves auth plus turn overrides", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
profiles: {
work: {
type: "oauth",
provider: "openai-codex",
access: "access-token",
},
},
});
await fs.writeFile(
`${sessionFile}.codex-app-server.json`,
JSON.stringify({
schemaVersion: 1,
threadId: "thread-old",
cwd: tempDir,
authProfileId: "work",
model: "gpt-5.4-mini",
modelProvider: "openai",
approvalPolicy: "on-request",
sandbox: "workspace-write",
serviceTier: "fast",
}),
);
const requests: Array<{ method: string; params: Record<string, unknown> }> = [];
const notificationHandlers: Array<(notification: Record<string, unknown>) => void> = [];
sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
request: vi.fn(async (method: string, requestParams: Record<string, unknown>) => {
requests.push({ method, params: requestParams });
if (method === "turn/start" && requestParams.threadId === "thread-old") {
throw new Error("thread not found: thread-old");
}
if (method === "thread/start") {
return {
thread: { id: "thread-new", cwd: tempDir },
model: "gpt-5.4-mini",
};
}
if (method === "turn/start" && requestParams.threadId === "thread-new") {
setImmediate(() => {
for (const handler of notificationHandlers) {
handler({
method: "turn/completed",
params: {
threadId: "thread-new",
turn: {
id: "turn-new",
status: "completed",
items: [
{
id: "assistant-1",
type: "agentMessage",
text: "Recovered",
},
],
},
},
});
}
});
return { turn: { id: "turn-new" } };
}
throw new Error(`unexpected method: ${method}`);
}),
addNotificationHandler: vi.fn((handler) => {
notificationHandlers.push(handler);
return () => undefined;
}),
addRequestHandler: vi.fn(() => () => undefined),
});
const result = await handleCodexConversationInboundClaim(
{
content: "hi again",
bodyForAgent: "hi again",
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: 500 },
);
expect(result).toEqual({ handled: true, reply: { text: "Recovered" } });
expect(requests.map((request) => request.method)).toEqual([
"turn/start",
"thread/start",
"turn/start",
]);
expect(sharedClientMocks.getSharedCodexAppServerClient).toHaveBeenCalledWith(
expect.objectContaining({ authProfileId: "work" }),
);
expect(requests[1]?.params).toMatchObject({
model: "gpt-5.4-mini",
approvalPolicy: "on-request",
sandbox: "workspace-write",
serviceTier: "fast",
});
expect(requests[1]?.params).not.toHaveProperty("modelProvider");
expect(requests[2]?.params).toMatchObject({
threadId: "thread-new",
approvalPolicy: "on-request",
serviceTier: "fast",
});
const savedBinding = JSON.parse(
await fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8"),
);
expect(savedBinding).toMatchObject({
threadId: "thread-new",
authProfileId: "work",
approvalPolicy: "on-request",
sandbox: "workspace-write",
serviceTier: "fast",
});
expect(savedBinding).not.toHaveProperty("modelProvider");
});
it("returns a clean failure reply when app-server turn start rejects", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
await fs.writeFile(

View File

@@ -10,8 +10,11 @@ import { CODEX_CONTROL_METHODS } from "./app-server/capabilities.js";
import {
codexSandboxPolicyForTurn,
resolveCodexAppServerRuntimeOptions,
type CodexAppServerApprovalPolicy,
type CodexAppServerSandboxMode,
} from "./app-server/config.js";
import {
type CodexServiceTier,
type CodexThreadResumeResponse,
type CodexThreadStartResponse,
type CodexTurnStartResponse,
@@ -59,6 +62,9 @@ type CodexConversationStartParams = {
model?: string;
modelProvider?: string;
authProfileId?: string;
approvalPolicy?: CodexAppServerApprovalPolicy;
sandbox?: CodexAppServerSandboxMode;
serviceTier?: CodexServiceTier;
};
type BoundTurnResult = {
@@ -100,6 +106,9 @@ export async function startCodexConversationThread(
model: params.model,
modelProvider: params.modelProvider,
authProfileId,
approvalPolicy: params.approvalPolicy,
sandbox: params.sandbox,
serviceTier: params.serviceTier,
config: params.config,
});
} else {
@@ -110,6 +119,9 @@ export async function startCodexConversationThread(
model: params.model,
modelProvider: params.modelProvider,
authProfileId,
approvalPolicy: params.approvalPolicy,
sandbox: params.sandbox,
serviceTier: params.serviceTier,
config: params.config,
});
}
@@ -137,7 +149,7 @@ export async function handleCodexConversationInboundClaim(
}
try {
const result = await enqueueBoundTurn(data.sessionFile, () =>
runBoundTurn({
runBoundTurnWithMissingThreadRecovery({
data,
prompt,
event,
@@ -177,9 +189,14 @@ async function attachExistingThread(params: {
model?: string;
modelProvider?: string;
authProfileId?: string;
approvalPolicy?: CodexAppServerApprovalPolicy;
sandbox?: CodexAppServerSandboxMode;
serviceTier?: CodexServiceTier;
config?: CodexAppServerAuthProfileLookup["config"];
}): Promise<void> {
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
const runtime = resolveCodexAppServerRuntimeOptions({
pluginConfig: params.pluginConfig,
});
const modelProvider = resolveThreadRequestModelProvider({
authProfileId: params.authProfileId,
modelProvider: params.modelProvider,
@@ -196,10 +213,12 @@ async function attachExistingThread(params: {
threadId: params.threadId,
...(params.model ? { model: params.model } : {}),
...(modelProvider ? { modelProvider } : {}),
approvalPolicy: runtime.approvalPolicy,
approvalPolicy: params.approvalPolicy ?? runtime.approvalPolicy,
approvalsReviewer: runtime.approvalsReviewer,
sandbox: runtime.sandbox,
...(runtime.serviceTier ? { serviceTier: runtime.serviceTier } : {}),
sandbox: params.sandbox ?? runtime.sandbox,
...((params.serviceTier ?? runtime.serviceTier)
? { serviceTier: params.serviceTier ?? runtime.serviceTier }
: {}),
persistExtendedHistory: true,
},
{ timeoutMs: runtime.requestTimeoutMs },
@@ -217,9 +236,9 @@ async function attachExistingThread(params: {
authProfileId: params.authProfileId,
modelProvider: response.modelProvider ?? params.modelProvider,
}),
approvalPolicy: runtime.approvalPolicy,
sandbox: runtime.sandbox,
serviceTier: runtime.serviceTier,
approvalPolicy: params.approvalPolicy ?? runtime.approvalPolicy,
sandbox: params.sandbox ?? runtime.sandbox,
serviceTier: params.serviceTier ?? runtime.serviceTier,
},
{
config: params.config,
@@ -234,9 +253,14 @@ async function createThread(params: {
model?: string;
modelProvider?: string;
authProfileId?: string;
approvalPolicy?: CodexAppServerApprovalPolicy;
sandbox?: CodexAppServerSandboxMode;
serviceTier?: CodexServiceTier;
config?: CodexAppServerAuthProfileLookup["config"];
}): Promise<void> {
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
const runtime = resolveCodexAppServerRuntimeOptions({
pluginConfig: params.pluginConfig,
});
const modelProvider = resolveThreadRequestModelProvider({
authProfileId: params.authProfileId,
modelProvider: params.modelProvider,
@@ -253,10 +277,12 @@ async function createThread(params: {
cwd: params.workspaceDir,
...(params.model ? { model: params.model } : {}),
...(modelProvider ? { modelProvider } : {}),
approvalPolicy: runtime.approvalPolicy,
approvalPolicy: params.approvalPolicy ?? runtime.approvalPolicy,
approvalsReviewer: runtime.approvalsReviewer,
sandbox: runtime.sandbox,
...(runtime.serviceTier ? { serviceTier: runtime.serviceTier } : {}),
sandbox: params.sandbox ?? runtime.sandbox,
...((params.serviceTier ?? runtime.serviceTier)
? { serviceTier: params.serviceTier ?? runtime.serviceTier }
: {}),
developerInstructions:
"This Codex thread is bound to an OpenClaw conversation. Answer normally; OpenClaw will deliver your final response back to the conversation.",
experimentalRawEvents: true,
@@ -276,9 +302,9 @@ async function createThread(params: {
authProfileId: params.authProfileId,
modelProvider: response.modelProvider ?? params.modelProvider,
}),
approvalPolicy: runtime.approvalPolicy,
sandbox: runtime.sandbox,
serviceTier: runtime.serviceTier,
approvalPolicy: params.approvalPolicy ?? runtime.approvalPolicy,
sandbox: params.sandbox ?? runtime.sandbox,
serviceTier: params.serviceTier ?? runtime.serviceTier,
},
{
config: params.config,
@@ -293,7 +319,9 @@ async function runBoundTurn(params: {
pluginConfig?: unknown;
timeoutMs?: number;
}): Promise<BoundTurnResult> {
const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
const runtime = resolveCodexAppServerRuntimeOptions({
pluginConfig: params.pluginConfig,
});
const binding = await readCodexAppServerBinding(params.data.sessionFile);
const threadId = binding?.threadId;
if (!threadId) {
@@ -350,7 +378,10 @@ async function runBoundTurn(params: {
"turn/start",
{
threadId,
input: buildCodexConversationTurnInput({ prompt: params.prompt, event: params.event }),
input: buildCodexConversationTurnInput({
prompt: params.prompt,
event: params.event,
}),
cwd: binding.cwd || params.data.workspaceDir,
approvalPolicy: binding.approvalPolicy ?? runtime.approvalPolicy,
approvalsReviewer: runtime.approvalsReviewer,
@@ -389,6 +420,39 @@ async function runBoundTurn(params: {
}
}
async function runBoundTurnWithMissingThreadRecovery(params: {
data: CodexConversationBindingData;
prompt: string;
event: PluginHookInboundClaimEvent;
pluginConfig?: unknown;
timeoutMs?: number;
}): Promise<BoundTurnResult> {
try {
return await runBoundTurn(params);
} catch (error) {
if (!isCodexThreadNotFoundError(error)) {
throw error;
}
const binding = await readCodexAppServerBinding(params.data.sessionFile);
await startCodexConversationThread({
pluginConfig: params.pluginConfig,
sessionFile: params.data.sessionFile,
workspaceDir: binding?.cwd || params.data.workspaceDir,
model: binding?.model,
modelProvider: binding?.modelProvider,
authProfileId: binding?.authProfileId,
approvalPolicy: binding?.approvalPolicy,
sandbox: binding?.sandbox,
serviceTier: binding?.serviceTier,
});
return await runBoundTurn(params);
}
}
function isCodexThreadNotFoundError(error: unknown): boolean {
return /\bthread not found:/iu.test(formatErrorMessage(error));
}
function enqueueBoundTurn<T>(key: string, run: () => Promise<T>): Promise<T> {
const state = getGlobalState();
const previous = state.queues.get(key) ?? Promise.resolve();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/feishu",
"version": "2026.5.4",
"version": "2026.5.4-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.4"
"openclaw": ">=2026.5.4-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -47,10 +47,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.4-beta.3"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.4-beta.3"
},
"release": {
"publishToClawHub": true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/lobster",
"version": "2026.5.4",
"version": "2026.5.4-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.4"
"pluginApi": ">=2026.5.4-beta.3"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.4-beta.3"
},
"release": {
"publishToClawHub": true,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/memory-lancedb",
"version": "2026.5.4",
"version": "2026.5.4-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.4"
"pluginApi": ">=2026.5.4-beta.3"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.4-beta.3"
},
"release": {
"publishToClawHub": true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/nostr",
"version": "2026.5.4",
"version": "2026.5.4-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.4"
"openclaw": ">=2026.5.4-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -54,10 +54,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.4-beta.3"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.4-beta.3"
},
"release": {
"publishToClawHub": true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/openshell-sandbox",
"version": "2026.5.4",
"version": "2026.5.4-beta.3",
"private": true,
"description": "OpenClaw OpenShell sandbox backend",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/perplexity-plugin",
"version": "2026.5.4",
"version": "2026.5.4-beta.3",
"private": true,
"description": "OpenClaw Perplexity plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/qa-channel",
"version": "2026.5.4",
"version": "2026.5.4-beta.3",
"private": true,
"description": "OpenClaw QA synthetic channel plugin",
"type": "module",
@@ -18,7 +18,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.4"
"openclaw": ">=2026.5.4-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/qa-lab",
"version": "2026.5.4",
"version": "2026.5.4-beta.3",
"private": true,
"description": "OpenClaw QA lab plugin with private debugger UI and scenario runner",
"type": "module",
@@ -18,7 +18,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.4"
"openclaw": ">=2026.5.4-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -30,7 +30,7 @@
"./index.ts"
],
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.4-beta.3"
}
}
}

View File

@@ -333,6 +333,8 @@ describe("telegram live qa runtime", () => {
"telegram-context-command",
"telegram-current-session-status-tool",
"telegram-mentioned-message-reply",
"telegram-long-final-reuses-preview",
"telegram-long-final-three-chunks",
"telegram-mention-gating",
]);
expect(scenarios.map((scenario) => scenario.id)).toEqual([
@@ -343,6 +345,8 @@ describe("telegram live qa runtime", () => {
"telegram-context-command",
"telegram-current-session-status-tool",
"telegram-mentioned-message-reply",
"telegram-long-final-reuses-preview",
"telegram-long-final-three-chunks",
"telegram-mention-gating",
]);
expect(
@@ -355,6 +359,25 @@ describe("telegram live qa runtime", () => {
.find((scenario) => scenario.id === "telegram-mentioned-message-reply")
?.buildRun("sut_bot").replyToLatestSutMessage,
).toBe(true);
expect(
scenarios
.find((scenario) => scenario.id === "telegram-long-final-reuses-preview")
?.buildRun("sut_bot"),
).toMatchObject({
expectedJoinedSutTextIncludes: ["TELEGRAM-LONG-FINAL-BEGIN", "TELEGRAM-LONG-FINAL-END"],
expectedSutMessageCount: 2,
});
expect(
scenarios
.find((scenario) => scenario.id === "telegram-long-final-three-chunks")
?.buildRun("sut_bot"),
).toMatchObject({
expectedJoinedSutTextIncludes: [
"TELEGRAM-LONG-FINAL-3CHUNK-BEGIN",
"TELEGRAM-LONG-FINAL-3CHUNK-END",
],
expectedSutMessageCount: 3,
});
});
it("keeps bot-to-bot plain mentions out of the default Telegram live set", () => {
@@ -382,6 +405,160 @@ describe("telegram live qa runtime", () => {
).toEqual(["allowlist-block", "top-level-reply-shape", "restart-resume"]);
});
it("asserts long Telegram final replies reuse the streamed preview message", () => {
expect(() =>
__testing.assertTelegramScenarioMessageSet({
expectedJoinedSutTextIncludes: ["TELEGRAM-LONG-FINAL-BEGIN", "TELEGRAM-LONG-FINAL-END"],
expectedSutMessageCount: 2,
groupId: "-100123",
scenarioId: "telegram-long-final-reuses-preview",
sutBotId: 99,
observedMessages: [
{
updateId: 1,
messageId: 10,
chatId: -100123,
senderId: 99,
senderIsBot: true,
scenarioId: "telegram-long-final-reuses-preview",
scenarioTitle: "Telegram long final reuses the preview message",
matchedScenario: true,
text: "TELEGRAM-LONG-FINAL-BEGIN part one ",
timestamp: 1_700_000_000_000,
inlineButtons: [],
mediaKinds: [],
},
{
updateId: 2,
messageId: 11,
chatId: -100123,
senderId: 99,
senderIsBot: true,
scenarioId: "telegram-long-final-reuses-preview",
scenarioTitle: "Telegram long final reuses the preview message",
matchedScenario: true,
text: "part two TELEGRAM-LONG-FINAL-END",
timestamp: 1_700_000_001_000,
inlineButtons: [],
mediaKinds: [],
},
],
}),
).not.toThrow();
expect(() =>
__testing.assertTelegramScenarioMessageSet({
expectedSutMessageCount: 2,
groupId: "-100123",
scenarioId: "telegram-long-final-reuses-preview",
sutBotId: 99,
observedMessages: [
{
updateId: 1,
messageId: 10,
chatId: -100123,
senderId: 99,
senderIsBot: true,
scenarioId: "telegram-long-final-reuses-preview",
scenarioTitle: "Telegram long final reuses the preview message",
matchedScenario: true,
text: "preview",
timestamp: 1_700_000_000_000,
inlineButtons: [],
mediaKinds: [],
},
{
updateId: 2,
messageId: 11,
chatId: -100123,
senderId: 99,
senderIsBot: true,
scenarioId: "telegram-long-final-reuses-preview",
scenarioTitle: "Telegram long final reuses the preview message",
matchedScenario: true,
text: "final chunk one",
timestamp: 1_700_000_001_000,
inlineButtons: [],
mediaKinds: [],
},
{
updateId: 3,
messageId: 12,
chatId: -100123,
senderId: 99,
senderIsBot: true,
scenarioId: "telegram-long-final-reuses-preview",
scenarioTitle: "Telegram long final reuses the preview message",
matchedScenario: true,
text: "final chunk two",
timestamp: 1_700_000_002_000,
inlineButtons: [],
mediaKinds: [],
},
],
}),
).toThrow("expected 2 SUT message(s), observed 3");
});
it("accepts legitimate three-chunk Telegram final replies", () => {
expect(() =>
__testing.assertTelegramScenarioMessageSet({
expectedJoinedSutTextIncludes: [
"TELEGRAM-LONG-FINAL-3CHUNK-BEGIN",
"TELEGRAM-LONG-FINAL-3CHUNK-END",
],
expectedSutMessageCount: 3,
groupId: "-100123",
scenarioId: "telegram-long-final-three-chunks",
sutBotId: 99,
observedMessages: [
{
updateId: 1,
messageId: 10,
chatId: -100123,
senderId: 99,
senderIsBot: true,
scenarioId: "telegram-long-final-three-chunks",
scenarioTitle: "Telegram three-chunk final keeps only final chunks",
matchedScenario: true,
text: "TELEGRAM-LONG-FINAL-3CHUNK-BEGIN part one ",
timestamp: 1_700_000_000_000,
inlineButtons: [],
mediaKinds: [],
},
{
updateId: 2,
messageId: 11,
chatId: -100123,
senderId: 99,
senderIsBot: true,
scenarioId: "telegram-long-final-three-chunks",
scenarioTitle: "Telegram three-chunk final keeps only final chunks",
matchedScenario: true,
text: "part two ",
timestamp: 1_700_000_001_000,
inlineButtons: [],
mediaKinds: [],
},
{
updateId: 3,
messageId: 12,
chatId: -100123,
senderId: 99,
senderIsBot: true,
scenarioId: "telegram-long-final-three-chunks",
scenarioTitle: "Telegram three-chunk final keeps only final chunks",
matchedScenario: true,
text: "part three TELEGRAM-LONG-FINAL-3CHUNK-END",
timestamp: 1_700_000_002_000,
inlineButtons: [],
mediaKinds: [],
},
],
}),
).not.toThrow();
});
it("matches scenario replies by thread or exact marker", () => {
expect(
__testing.matchesTelegramScenarioReply({

View File

@@ -48,6 +48,8 @@ type TelegramQaScenarioId =
| "telegram-whoami-command"
| "telegram-context-command"
| "telegram-current-session-status-tool"
| "telegram-long-final-three-chunks"
| "telegram-long-final-reuses-preview"
| "telegram-mentioned-message-reply"
| "telegram-mention-gating";
@@ -56,8 +58,11 @@ type TelegramQaScenarioRun = {
expectReply: boolean;
input: string;
expectedTextIncludes?: string[];
expectedJoinedSutTextIncludes?: string[];
expectedSutMessageCount?: number;
matchText?: string;
replyToLatestSutMessage?: boolean;
settleMs?: number;
};
type TelegramQaScenarioDefinition = LiveTransportScenarioDefinition<TelegramQaScenarioId> & {
@@ -295,6 +300,39 @@ const TELEGRAM_QA_SCENARIOS: TelegramQaScenarioDefinition[] = [
replyToLatestSutMessage: true,
}),
},
{
id: "telegram-long-final-reuses-preview",
title: "Telegram long final reuses the preview message",
defaultEnabled: false,
timeoutMs: 60_000,
buildRun: (sutUsername) => ({
allowAnySutReply: true,
expectReply: true,
input: `@${sutUsername} Telegram long final QA check. Use the scripted long final response.`,
expectedTextIncludes: ["TELEGRAM-LONG-FINAL-BEGIN"],
expectedJoinedSutTextIncludes: ["TELEGRAM-LONG-FINAL-BEGIN", "TELEGRAM-LONG-FINAL-END"],
expectedSutMessageCount: 2,
settleMs: 4_000,
}),
},
{
id: "telegram-long-final-three-chunks",
title: "Telegram three-chunk final keeps only final chunks",
defaultEnabled: false,
timeoutMs: 60_000,
buildRun: (sutUsername) => ({
allowAnySutReply: true,
expectReply: true,
input: `@${sutUsername} Telegram long final three chunk QA check. Use the scripted three chunk final response.`,
expectedTextIncludes: ["TELEGRAM-LONG-FINAL-3CHUNK-BEGIN"],
expectedJoinedSutTextIncludes: [
"TELEGRAM-LONG-FINAL-3CHUNK-BEGIN",
"TELEGRAM-LONG-FINAL-3CHUNK-END",
],
expectedSutMessageCount: 3,
settleMs: 4_000,
}),
},
{
id: "telegram-mention-gating",
standardId: "mention-gating",
@@ -744,6 +782,102 @@ async function waitForObservedMessage(params: {
throw new Error(timeoutMessage);
}
async function collectObservedMessages(params: {
token: string;
initialOffset: number;
settleMs: number;
predicate: (message: TelegramObservedMessage) => boolean;
observedMessages: TelegramObservedMessage[];
observationScenarioId: string;
observationScenarioTitle: string;
}) {
const startedAt = Date.now();
let offset = params.initialOffset;
while (Date.now() - startedAt < params.settleMs) {
const remainingMs = Math.max(1, params.settleMs - (Date.now() - startedAt));
const timeoutSeconds = Math.max(1, Math.min(2, Math.ceil(remainingMs / 1000)));
let updates: TelegramUpdate[];
try {
updates = await callTelegramApi<TelegramUpdate[]>(
params.token,
"getUpdates",
{
offset,
timeout: timeoutSeconds,
allowed_updates: ["message", "edited_message"],
},
timeoutSeconds * 1000 + 5_000,
);
} catch (error) {
if (!isRecoverableTelegramQaPollError(error)) {
throw error;
}
await waitForTelegramPollRetryDelay(params.settleMs - (Date.now() - startedAt));
continue;
}
if (updates.length === 0) {
continue;
}
offset = (updates.at(-1)?.update_id ?? offset) + 1;
for (const update of updates) {
const normalized = normalizeTelegramObservedMessage(update);
if (!normalized) {
continue;
}
params.observedMessages.push({
...normalized,
scenarioId: params.observationScenarioId,
scenarioTitle: params.observationScenarioTitle,
matchedScenario: params.predicate(normalized),
});
}
}
return offset;
}
function assertTelegramScenarioMessageSet(params: {
expectedJoinedSutTextIncludes?: string[];
expectedSutMessageCount?: number;
groupId: string;
observedMessages: TelegramObservedMessage[];
scenarioId: string;
sutBotId: number;
}) {
if (
params.expectedSutMessageCount === undefined &&
(params.expectedJoinedSutTextIncludes ?? []).length === 0
) {
return;
}
const byMessageId = new Map<number, TelegramObservedMessage>();
for (const message of params.observedMessages) {
if (
message.scenarioId === params.scenarioId &&
message.chatId === Number(params.groupId) &&
message.senderId === params.sutBotId
) {
byMessageId.set(message.messageId, message);
}
}
const messages = [...byMessageId.values()].toSorted((a, b) => a.messageId - b.messageId);
if (
params.expectedSutMessageCount !== undefined &&
messages.length !== params.expectedSutMessageCount
) {
throw new Error(
`expected ${params.expectedSutMessageCount} SUT message(s), observed ${messages.length}: ${messages
.map((message) => message.messageId)
.join(", ")}`,
);
}
const joinedText = messages.map((message) => message.text).join("");
for (const expected of params.expectedJoinedSutTextIncludes ?? []) {
if (!joinedText.includes(expected)) {
throw new Error(`joined SUT reply text missing expected text: ${expected}`);
}
}
}
async function waitForTelegramChannelRunning(
gateway: Awaited<ReturnType<typeof startQaGatewayChild>>,
accountId: string,
@@ -1374,6 +1508,25 @@ export async function runTelegramQaLive(params: {
}),
});
driverOffset = matched.nextOffset;
if (scenarioRun.settleMs !== undefined) {
driverOffset = await collectObservedMessages({
token: runtimeEnv.driverToken,
initialOffset: driverOffset,
settleMs: scenarioRun.settleMs,
observedMessages,
observationScenarioId: scenario.id,
observationScenarioTitle: scenario.title,
predicate: (message) =>
matchesTelegramScenarioReply({
allowAnySutReply: scenarioRun.allowAnySutReply,
groupId: runtimeEnv.groupId,
matchText: scenarioRun.matchText,
message,
sentMessageId: sent.message_id,
sutBotId: sutIdentity.id,
}),
});
}
if (!scenarioRun.expectReply) {
throw new Error(`unexpected reply message ${matched.message.messageId} matched`);
}
@@ -1381,14 +1534,26 @@ export async function runTelegramQaLive(params: {
expectedTextIncludes: scenarioRun.expectedTextIncludes,
message: matched.message,
});
assertTelegramScenarioMessageSet({
expectedJoinedSutTextIncludes: scenarioRun.expectedJoinedSutTextIncludes,
expectedSutMessageCount: scenarioRun.expectedSutMessageCount,
groupId: runtimeEnv.groupId,
observedMessages,
scenarioId: scenario.id,
sutBotId: sutIdentity.id,
});
const rttMs = matched.observedAtMs - requestStartedAtMs;
const suffix =
scenarioRun.expectedSutMessageCount === undefined
? ""
: `; observed ${scenarioRun.expectedSutMessageCount} SUT message(s)`;
const result = {
id: scenario.id,
title: scenario.title,
status: "pass",
details: redactPublicMetadata
? `reply matched in ${rttMs}ms`
: `reply message ${matched.message.messageId} matched in ${rttMs}ms`,
? `reply matched in ${rttMs}ms${suffix}`
: `reply message ${matched.message.messageId} matched in ${rttMs}ms${suffix}`,
rttMs,
requestStartedAt,
responseObservedAt: new Date(matched.observedAtMs).toISOString(),
@@ -1565,6 +1730,7 @@ export const __testing = {
buildObservedMessagesArtifact,
canaryFailureMessage,
callTelegramApi,
assertTelegramScenarioMessageSet,
isRecoverableTelegramQaPollError,
assertTelegramScenarioReply,
classifyCanaryReply,

View File

@@ -221,6 +221,48 @@ describe("qa mock openai server", () => {
expect(partialBody).toContain('"type":"response.output_text.delta"');
expect(partialBody).toContain("QA_PARTIAL_OK");
const telegramLongResponse = await fetch(`${server.baseUrl}/v1/responses`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
stream: true,
input: [
makeUserInput("Telegram long final QA check. Use the scripted long final response."),
],
}),
});
expect(telegramLongResponse.status).toBe(200);
const telegramLongBody = await telegramLongResponse.text();
expect(telegramLongBody).toContain('"type":"response.output_text.delta"');
expect(telegramLongBody).toContain('"phase":"final_answer"');
expect(telegramLongBody).toContain("TELEGRAM-LONG-FINAL-BEGIN");
expect(telegramLongBody).toContain("TELEGRAM-LONG-FINAL-END");
expect(telegramLongBody.length).toBeGreaterThan(4_500);
const telegramThreeChunkLongResponse = await fetch(`${server.baseUrl}/v1/responses`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
stream: true,
input: [
makeUserInput(
"Telegram long final three chunk QA check. Use the scripted three chunk final response.",
),
],
}),
});
expect(telegramThreeChunkLongResponse.status).toBe(200);
const telegramThreeChunkLongBody = await telegramThreeChunkLongResponse.text();
expect(telegramThreeChunkLongBody).toContain('"type":"response.output_text.delta"');
expect(telegramThreeChunkLongBody).toContain('"phase":"final_answer"');
expect(telegramThreeChunkLongBody).toContain("TELEGRAM-LONG-FINAL-3CHUNK-BEGIN");
expect(telegramThreeChunkLongBody).toContain("TELEGRAM-LONG-FINAL-3CHUNK-END");
expect(telegramThreeChunkLongBody.length).toBeGreaterThan(8_000);
const blockResponse = await fetch(`${server.baseUrl}/v1/responses`, {
method: "POST",
headers: {

View File

@@ -153,6 +153,8 @@ const QA_GROUP_VISIBLE_REPLY_TOOL_PROMPT_RE = /qa group visible reply tool check
const QA_GROUP_MESSAGE_UNAVAILABLE_FALLBACK_PROMPT_RE =
/qa group message unavailable fallback check/i;
const QA_TELEGRAM_CURRENT_SESSION_STATUS_PROMPT_RE = /telegram current session_status qa check/i;
const QA_TELEGRAM_LONG_FINAL_THREE_CHUNK_PROMPT_RE = /telegram long final three chunk qa check/i;
const QA_TELEGRAM_LONG_FINAL_PROMPT_RE = /telegram long final qa check/i;
const QA_SUBAGENT_DIRECT_FALLBACK_PROMPT_RE = /subagent direct fallback qa check/i;
const QA_SUBAGENT_DIRECT_FALLBACK_WORKER_RE = /subagent direct fallback worker/i;
const QA_SUBAGENT_DIRECT_FALLBACK_MARKER = "QA-SUBAGENT-DIRECT-FALLBACK-OK";
@@ -1034,6 +1036,23 @@ function splitMockStreamingText(text: string, parts = 3) {
return chunks.length > 1 ? chunks : [text.slice(0, 1), text.slice(1)];
}
function buildTelegramLongFinalText({
endMarker = "TELEGRAM-LONG-FINAL-END",
segmentCount = 54,
startMarker = "TELEGRAM-LONG-FINAL-BEGIN",
}: {
endMarker?: string;
segmentCount?: number;
startMarker?: string;
} = {}) {
const body = Array.from(
{ length: segmentCount },
(_, index) =>
`telegram-long-final-segment-${String(index + 1).padStart(3, "0")} ${"x".repeat(54)}`,
).join("\n");
return `${startMarker}\n${body}\n${endMarker}`;
}
function buildAssistantOutputItem(spec: MockAssistantMessageSpec) {
return {
type: "message",
@@ -1310,6 +1329,32 @@ async function buildResponsesPayload(
}
return buildAssistantEvents("");
}
if (QA_TELEGRAM_LONG_FINAL_THREE_CHUNK_PROMPT_RE.test(allInputText)) {
const text = buildTelegramLongFinalText({
endMarker: "TELEGRAM-LONG-FINAL-3CHUNK-END",
segmentCount: 96,
startMarker: "TELEGRAM-LONG-FINAL-3CHUNK-BEGIN",
});
return buildAssistantEvents([
{
id: "msg_mock_telegram_long_final_three_chunk",
phase: "final_answer",
streamDeltas: splitMockStreamingText(text),
text,
},
]);
}
if (QA_TELEGRAM_LONG_FINAL_PROMPT_RE.test(allInputText)) {
const text = buildTelegramLongFinalText();
return buildAssistantEvents([
{
id: "msg_mock_telegram_long_final",
phase: "final_answer",
streamDeltas: splitMockStreamingText(text),
text,
},
]);
}
if (QA_STREAMING_PROMPT_RE.test(allInputText) && exactReplyDirective) {
return buildAssistantEvents([
{

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/qa-matrix",
"version": "2026.5.4",
"version": "2026.5.4-beta.3",
"private": true,
"description": "OpenClaw Matrix QA runner plugin",
"type": "module",
@@ -13,7 +13,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.4"
"openclaw": ">=2026.5.4-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -25,7 +25,7 @@
"./index.ts"
],
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.4-beta.3"
}
}
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/qqbot",
"version": "2026.5.4",
"version": "2026.5.4-beta.3",
"private": false,
"description": "OpenClaw QQ Bot channel plugin",
"repository": {
@@ -21,7 +21,7 @@
"openclaw": "workspace:*"
},
"peerDependencies": {
"openclaw": ">=2026.5.4"
"openclaw": ">=2026.5.4-beta.3"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -50,10 +50,10 @@
"minHostVersion": ">=2026.4.10"
},
"compat": {
"pluginApi": ">=2026.5.4"
"pluginApi": ">=2026.5.4-beta.3"
},
"build": {
"openclawVersion": "2026.5.4"
"openclawVersion": "2026.5.4-beta.3"
},
"release": {
"publishToClawHub": true,

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/searxng-plugin",
"version": "2026.5.4",
"version": "2026.5.4-beta.3",
"private": true,
"description": "OpenClaw SearXNG plugin",
"type": "module",

View File

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

View File

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

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