Merge remote-tracking branch 'origin/main' into codex/cortex-openclaw-integration
# Conflicts: # scripts/test-parallel.mjs
This commit is contained in:
commit
945418db64
@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/chat: add an expand-to-canvas button on assistant chat bubbles and in-app session navigation from Sessions and Cron views. Thanks @BunsDev.
|
||||
- Plugins/context engines: expose `delegateCompactionToRuntime(...)` on the public plugin SDK, refactor the legacy engine to use the shared helper, and clarify `ownsCompaction` delegation semantics for non-owning engines. (#49061) Thanks @jalehman.
|
||||
- Plugins/MiniMax: add MiniMax-M2.7 and MiniMax-M2.7-highspeed models and update the default model from M2.5 to M2.7. (#49691) Thanks @liyuan97.
|
||||
- Plugins/Xiaomi: switch the bundled Xiaomi provider to the `/v1` OpenAI-compatible endpoint and add MiMo V2 Pro plus MiMo V2 Omni to the built-in catalog. (#49214) thanks @DJjjjhao.
|
||||
|
||||
### Fixes
|
||||
|
||||
@ -168,6 +169,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Onboarding/custom providers: keep Azure AI Foundry `*.services.ai.azure.com` custom endpoints on the selected compatibility path instead of forcing Responses, so chat-completions Foundry models still work after setup. Fixes #50528. (#50535) Thanks @obviyus.
|
||||
- Plugins/update: let `openclaw plugins update <npm-spec>` target tracked npm installs by dist-tag or exact version, and preserve the recorded npm spec for later id-based updates. (#49998) Thanks @huntharo.
|
||||
- Tests/CLI: reduce command-secret gateway test import pressure while keeping the real protocol payload validator in place, so the isolated lane no longer carries the heavier runtime-web and message-channel graphs. (#50663) Thanks @huntharo.
|
||||
- Gateway/plugins: share plugin interactive callback routing and plugin bind approval state across duplicate module graphs so Telegram Codex picker buttons and plugin bind approvals no longer fall through to normal inbound message routing. (#50722) Thanks @huntharo.
|
||||
|
||||
### Breaking
|
||||
|
||||
@ -180,6 +182,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/message discovery: require `ChannelMessageActionAdapter.describeMessageTool(...)` for shared `message` tool discovery. The legacy `listActions`, `getCapabilities`, and `getToolSchema` adapter methods are removed. Plugin authors should migrate message discovery to `describeMessageTool(...)` and keep channel-specific action runtime code inside the owning plugin package. Thanks @gumadeiras.
|
||||
- Exec/env sandbox: block build-tool JVM injection (`MAVEN_OPTS`, `SBT_OPTS`, `GRADLE_OPTS`, `ANT_OPTS`), glibc tunable exploitation (`GLIBC_TUNABLES`), and .NET dependency resolution hijack (`DOTNET_ADDITIONAL_DEPS`) from the host exec environment, and restrict Gradle init script redirect (`GRADLE_USER_HOME`) as an override-only block so user-configured Gradle homes still propagate. (#49702)
|
||||
- Plugins/Matrix: add a new Matrix plugin backed by the official `matrix-js-sdk`. If you are upgrading from the previous public Matrix plugin, follow the migration guide: https://docs.openclaw.ai/install/migrating-matrix Thanks @gumadeiras.
|
||||
- Discord/commands: switch native command deployment to Carbon reconcile by default so Discord restarts stop churning slash commands through OpenClaw’s local deploy path. (#46597) Thanks @huntharo and @thewilloftheshadow.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
|
||||
@ -22101,6 +22101,34 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.ackReaction",
|
||||
"kind": "channel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.ackReactionScope",
|
||||
"kind": "channel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"enumValues": [
|
||||
"group-mentions",
|
||||
"group-all",
|
||||
"direct",
|
||||
"all",
|
||||
"none",
|
||||
"off"
|
||||
],
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.actions",
|
||||
"kind": "channel",
|
||||
@ -22151,6 +22179,16 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.actions.profile",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.actions.reactions",
|
||||
"kind": "channel",
|
||||
@ -22161,6 +22199,16 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.actions.verification",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.allowlistOnly",
|
||||
"kind": "channel",
|
||||
@ -22209,6 +22257,16 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.avatarUrl",
|
||||
"kind": "channel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.chunkMode",
|
||||
"kind": "channel",
|
||||
@ -22233,6 +22291,16 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.deviceId",
|
||||
"kind": "channel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.deviceName",
|
||||
"kind": "channel",
|
||||
@ -22651,6 +22719,20 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.reactionNotifications",
|
||||
"kind": "channel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"enumValues": [
|
||||
"off",
|
||||
"own"
|
||||
],
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.replyToMode",
|
||||
"kind": "channel",
|
||||
@ -22859,6 +22941,30 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.startupVerification",
|
||||
"kind": "channel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"enumValues": [
|
||||
"off",
|
||||
"if-unverified"
|
||||
],
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.startupVerificationCooldownHours",
|
||||
"kind": "channel",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.textChunkLimit",
|
||||
"kind": "channel",
|
||||
@ -22869,6 +22975,66 @@
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.threadBindings",
|
||||
"kind": "channel",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.threadBindings.enabled",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.threadBindings.idleHours",
|
||||
"kind": "channel",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.threadBindings.maxAgeHours",
|
||||
"kind": "channel",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.threadBindings.spawnAcpSessions",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.threadBindings.spawnSubagentSessions",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.matrix.threadReplies",
|
||||
"kind": "channel",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5518}
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5533}
|
||||
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@ -1984,18 +1984,24 @@
|
||||
{"recordType":"path","path":"channels.matrix.accessToken","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.matrix.accounts.*","kind":"channel","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.ackReaction","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.ackReactionScope","kind":"channel","type":"string","required":false,"enumValues":["group-mentions","group-all","direct","all","none","off"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.actions","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.matrix.actions.channelInfo","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.actions.memberInfo","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.actions.messages","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.actions.pins","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.actions.profile","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.actions.reactions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.actions.verification","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.allowlistOnly","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.autoJoin","kind":"channel","type":"string","required":false,"enumValues":["always","allowlist","off"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.autoJoinAllowlist","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.matrix.autoJoinAllowlist.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.avatarUrl","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.chunkMode","kind":"channel","type":"string","required":false,"enumValues":["length","newline"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.defaultAccount","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.deviceId","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.deviceName","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.dm","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.matrix.dm.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
@ -2035,6 +2041,7 @@
|
||||
{"recordType":"path","path":"channels.matrix.password.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.password.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.password.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.reactionNotifications","kind":"channel","type":"string","required":false,"enumValues":["off","own"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.replyToMode","kind":"channel","type":"string","required":false,"enumValues":["off","first","all"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.responsePrefix","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.rooms","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
@ -2055,7 +2062,15 @@
|
||||
{"recordType":"path","path":"channels.matrix.rooms.*.tools.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.rooms.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.matrix.rooms.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.startupVerification","kind":"channel","type":"string","required":false,"enumValues":["off","if-unverified"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.startupVerificationCooldownHours","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.textChunkLimit","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.threadBindings","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.matrix.threadBindings.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.threadBindings.idleHours","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.threadBindings.maxAgeHours","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.threadBindings.spawnAcpSessions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.threadBindings.spawnSubagentSessions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.threadReplies","kind":"channel","type":"string","required":false,"enumValues":["off","inbound","always"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.matrix.userId","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.mattermost","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Mattermost","help":"self-hosted Slack-style chat; install the plugin to enable.","hasChildren":true}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "Use Xiaomi MiMo (mimo-v2-flash) with OpenClaw"
|
||||
summary: "Use Xiaomi MiMo models with OpenClaw"
|
||||
read_when:
|
||||
- You want Xiaomi MiMo models in OpenClaw
|
||||
- You need XIAOMI_API_KEY setup
|
||||
@ -8,15 +8,18 @@ title: "Xiaomi MiMo"
|
||||
|
||||
# Xiaomi MiMo
|
||||
|
||||
Xiaomi MiMo is the API platform for **MiMo** models. It provides REST APIs compatible with
|
||||
OpenAI and Anthropic formats and uses API keys for authentication. Create your API key in
|
||||
the [Xiaomi MiMo console](https://platform.xiaomimimo.com/#/console/api-keys). OpenClaw uses
|
||||
the `xiaomi` provider with a Xiaomi MiMo API key.
|
||||
Xiaomi MiMo is the API platform for **MiMo** models. OpenClaw uses the Xiaomi
|
||||
OpenAI-compatible endpoint with API-key authentication. Create your API key in the
|
||||
[Xiaomi MiMo console](https://platform.xiaomimimo.com/#/console/api-keys), then configure the
|
||||
bundled `xiaomi` provider with that key.
|
||||
|
||||
## Model overview
|
||||
|
||||
- **mimo-v2-flash**: 262144-token context window, Anthropic Messages API compatible.
|
||||
- Base URL: `https://api.xiaomimimo.com/anthropic`
|
||||
- **mimo-v2-flash**: default text model, 262144-token context window
|
||||
- **mimo-v2-pro**: reasoning text model, 1048576-token context window
|
||||
- **mimo-v2-omni**: reasoning multimodal model with text and image input, 262144-token context window
|
||||
- Base URL: `https://api.xiaomimimo.com/v1`
|
||||
- API: `openai-completions`
|
||||
- Authorization: `Bearer $XIAOMI_API_KEY`
|
||||
|
||||
## CLI setup
|
||||
@ -37,8 +40,8 @@ openclaw onboard --auth-choice xiaomi-api-key --xiaomi-api-key "$XIAOMI_API_KEY"
|
||||
mode: "merge",
|
||||
providers: {
|
||||
xiaomi: {
|
||||
baseUrl: "https://api.xiaomimimo.com/anthropic",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.xiaomimimo.com/v1",
|
||||
api: "openai-completions",
|
||||
apiKey: "XIAOMI_API_KEY",
|
||||
models: [
|
||||
{
|
||||
@ -50,6 +53,24 @@ openclaw onboard --auth-choice xiaomi-api-key --xiaomi-api-key "$XIAOMI_API_KEY"
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
{
|
||||
id: "mimo-v2-pro",
|
||||
name: "Xiaomi MiMo V2 Pro",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
{
|
||||
id: "mimo-v2-omni",
|
||||
name: "Xiaomi MiMo V2 Omni",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 262144,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -59,6 +80,7 @@ openclaw onboard --auth-choice xiaomi-api-key --xiaomi-api-key "$XIAOMI_API_KEY"
|
||||
|
||||
## Notes
|
||||
|
||||
- Model ref: `xiaomi/mimo-v2-flash`.
|
||||
- Default model ref: `xiaomi/mimo-v2-flash`.
|
||||
- Additional built-in models: `xiaomi/mimo-v2-pro`, `xiaomi/mimo-v2-omni`.
|
||||
- The provider is injected automatically when `XIAOMI_API_KEY` is set (or an auth profile exists).
|
||||
- See [/concepts/model-providers](/concepts/model-providers) for provider rules.
|
||||
|
||||
@ -2,28 +2,31 @@
|
||||
read_when:
|
||||
- 你想在 OpenClaw 中使用 Xiaomi MiMo 模型
|
||||
- 你需要设置 `XIAOMI_API_KEY`
|
||||
summary: 在 OpenClaw 中使用 Xiaomi MiMo(`mimo-v2-flash`)
|
||||
summary: 在 OpenClaw 中使用 Xiaomi MiMo 模型
|
||||
title: Xiaomi MiMo
|
||||
x-i18n:
|
||||
generated_at: "2026-03-16T06:27:26Z"
|
||||
generated_at: "2026-03-20T01:18:00Z"
|
||||
model: gpt-5.4
|
||||
provider: openai
|
||||
source_hash: 366fd2297b2caf8c5ad944d7f1b6d233b248fe43aedd22a28352ae7f370d2435
|
||||
source_hash: e0abfbe49f438807ce1c5cf5d7910e930c0d670f447f6eb53ca4e9af61cc0843
|
||||
source_path: providers/xiaomi.md
|
||||
workflow: 15
|
||||
---
|
||||
|
||||
# Xiaomi MiMo
|
||||
|
||||
Xiaomi MiMo 是 **MiMo** 模型的 API 平台。它提供与
|
||||
OpenAI 和 Anthropic 格式兼容的 REST API,并使用 API key 进行认证。请在
|
||||
[Xiaomi MiMo console](https://platform.xiaomimimo.com/#/console/api-keys) 中创建你的 API key。OpenClaw 使用
|
||||
`xiaomi` 提供商配合 Xiaomi MiMo API key。
|
||||
Xiaomi MiMo 是 **MiMo** 模型的 API 平台。OpenClaw 使用 Xiaomi 提供的
|
||||
OpenAI 兼容端点,并通过 API key 认证。请在
|
||||
[Xiaomi MiMo console](https://platform.xiaomimimo.com/#/console/api-keys) 中创建你的 API key,然后用它配置内置的
|
||||
`xiaomi` 提供商。
|
||||
|
||||
## 模型概览
|
||||
|
||||
- **mimo-v2-flash**:262144-token 上下文窗口,兼容 Anthropic Messages API。
|
||||
- Base URL:`https://api.xiaomimimo.com/anthropic`
|
||||
- **mimo-v2-flash**:默认文本模型,262144-token 上下文窗口
|
||||
- **mimo-v2-pro**:支持推理的文本模型,1048576-token 上下文窗口
|
||||
- **mimo-v2-omni**:支持推理的多模态模型,支持文本和图像输入,262144-token 上下文窗口
|
||||
- Base URL:`https://api.xiaomimimo.com/v1`
|
||||
- API:`openai-completions`
|
||||
- 认证方式:`Bearer $XIAOMI_API_KEY`
|
||||
|
||||
## CLI 设置
|
||||
@ -44,8 +47,8 @@ openclaw onboard --auth-choice xiaomi-api-key --xiaomi-api-key "$XIAOMI_API_KEY"
|
||||
mode: "merge",
|
||||
providers: {
|
||||
xiaomi: {
|
||||
baseUrl: "https://api.xiaomimimo.com/anthropic",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.xiaomimimo.com/v1",
|
||||
api: "openai-completions",
|
||||
apiKey: "XIAOMI_API_KEY",
|
||||
models: [
|
||||
{
|
||||
@ -57,6 +60,24 @@ openclaw onboard --auth-choice xiaomi-api-key --xiaomi-api-key "$XIAOMI_API_KEY"
|
||||
contextWindow: 262144,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
{
|
||||
id: "mimo-v2-pro",
|
||||
name: "Xiaomi MiMo V2 Pro",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
{
|
||||
id: "mimo-v2-omni",
|
||||
name: "Xiaomi MiMo V2 Omni",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 262144,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -66,6 +87,7 @@ openclaw onboard --auth-choice xiaomi-api-key --xiaomi-api-key "$XIAOMI_API_KEY"
|
||||
|
||||
## 说明
|
||||
|
||||
- 模型引用:`xiaomi/mimo-v2-flash`。
|
||||
- 默认模型引用:`xiaomi/mimo-v2-flash`。
|
||||
- 额外内置模型:`xiaomi/mimo-v2-pro`、`xiaomi/mimo-v2-omni`。
|
||||
- 当设置了 `XIAOMI_API_KEY`(或存在凭证配置文件)时,提供商会自动注入。
|
||||
- 有关提供商规则,请参阅 [/concepts/model-providers](/concepts/model-providers)。
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"description": "OpenClaw Discord channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@buape/carbon": "0.0.0-beta-20260216184201",
|
||||
"@buape/carbon": "0.0.0-beta-20260317045421",
|
||||
"@discordjs/voice": "^0.19.2",
|
||||
"discord-api-types": "^0.38.42",
|
||||
"https-proxy-agent": "^8.0.0",
|
||||
|
||||
@ -306,6 +306,7 @@ async function deployDiscordCommands(params: {
|
||||
// errors like Discord 30034 fail fast and don't wedge the provider.
|
||||
restClient.options.queueRequests = false;
|
||||
}
|
||||
params.runtime.log?.("discord: native commands using Carbon reconcile path");
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
try {
|
||||
await params.client.handleDeployRequest();
|
||||
@ -805,7 +806,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
phase: "deploy-commands:start",
|
||||
startAt: startupStartedAt,
|
||||
gateway: lifecycleGateway,
|
||||
details: `native=${nativeEnabled ? "on" : "off"} commandCount=${commands.length}`,
|
||||
details: `native=${nativeEnabled ? "on" : "off"} reconcile=on commandCount=${commands.length}`,
|
||||
});
|
||||
await deployDiscordCommands({
|
||||
client,
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
export * from "openclaw/plugin-sdk/matrix";
|
||||
export * from "../runtime-api.js";
|
||||
// Keep auth-precedence available internally without re-exporting helper-api
|
||||
// twice through both plugin-sdk/matrix and ../runtime-api.js.
|
||||
export * from "./auth-precedence.js";
|
||||
|
||||
@ -1382,14 +1382,14 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.skip("routes plugin-owned callback namespaces before synthetic command fallback", async () => {
|
||||
it("routes plugin-owned callback namespaces before synthetic command fallback", async () => {
|
||||
onSpy.mockClear();
|
||||
replySpy.mockClear();
|
||||
editMessageTextSpy.mockClear();
|
||||
sendMessageSpy.mockClear();
|
||||
registerPluginInteractiveHandler("codex-plugin", {
|
||||
channel: "telegram",
|
||||
namespace: "codex",
|
||||
namespace: "codexapp",
|
||||
handler: async ({ respond, callback }: PluginInteractiveTelegramHandlerContext) => {
|
||||
await respond.editMessage({
|
||||
text: `Handled ${callback.payload}`,
|
||||
@ -1416,7 +1416,7 @@ describe("createTelegramBot", () => {
|
||||
await callbackHandler({
|
||||
callbackQuery: {
|
||||
id: "cbq-codex-1",
|
||||
data: "codex:resume:thread-1",
|
||||
data: "codexapp:resume:thread-1",
|
||||
from: { id: 9, first_name: "Ada", username: "ada_bot" },
|
||||
message: {
|
||||
chat: { id: 1234, type: "private" },
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-models";
|
||||
|
||||
const XIAOMI_BASE_URL = "https://api.xiaomimimo.com/anthropic";
|
||||
const XIAOMI_BASE_URL = "https://api.xiaomimimo.com/v1";
|
||||
export const XIAOMI_DEFAULT_MODEL_ID = "mimo-v2-flash";
|
||||
const XIAOMI_DEFAULT_CONTEXT_WINDOW = 262144;
|
||||
const XIAOMI_DEFAULT_MAX_TOKENS = 8192;
|
||||
@ -14,7 +14,7 @@ const XIAOMI_DEFAULT_COST = {
|
||||
export function buildXiaomiProvider(): ModelProviderConfig {
|
||||
return {
|
||||
baseUrl: XIAOMI_BASE_URL,
|
||||
api: "anthropic-messages",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: XIAOMI_DEFAULT_MODEL_ID,
|
||||
@ -25,6 +25,24 @@ export function buildXiaomiProvider(): ModelProviderConfig {
|
||||
contextWindow: XIAOMI_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: XIAOMI_DEFAULT_MAX_TOKENS,
|
||||
},
|
||||
{
|
||||
id: "mimo-v2-pro",
|
||||
name: "Xiaomi MiMo V2 Pro",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: XIAOMI_DEFAULT_COST,
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
{
|
||||
id: "mimo-v2-omni",
|
||||
name: "Xiaomi MiMo V2 Omni",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: XIAOMI_DEFAULT_COST,
|
||||
contextWindow: XIAOMI_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: 32000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@ -48,5 +48,10 @@ fi
|
||||
|
||||
git add -- "${files[@]}"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
pnpm check
|
||||
# This hook is also exercised from lightweight temp repos in tests, where the
|
||||
# staged-file safety behavior matters but the full OpenClaw workspace does not
|
||||
# exist. Only run the repo-wide gate inside a real checkout.
|
||||
if [[ -f "$ROOT_DIR/package.json" ]] && [[ -f "$ROOT_DIR/pnpm-lock.yaml" ]]; then
|
||||
cd "$ROOT_DIR"
|
||||
pnpm check
|
||||
fi
|
||||
|
||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@ -309,8 +309,8 @@ importers:
|
||||
extensions/discord:
|
||||
dependencies:
|
||||
'@buape/carbon':
|
||||
specifier: 0.0.0-beta-20260216184201
|
||||
version: 0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1)
|
||||
specifier: 0.0.0-beta-20260317045421
|
||||
version: 0.0.0-beta-20260317045421(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1)
|
||||
'@discordjs/voice':
|
||||
specifier: ^0.19.2
|
||||
version: 0.19.2(@discordjs/opus@0.10.0)(opusscript@0.1.1)
|
||||
@ -991,6 +991,9 @@ packages:
|
||||
'@buape/carbon@0.0.0-beta-20260216184201':
|
||||
resolution: {integrity: sha512-u5mgYcigfPVqT7D9gVTGd+3YSflTreQmrWog7ORbb0z5w9eT8ft4rJOdw9fGwr75zMu9kXpSBaAcY2eZoJFSdA==}
|
||||
|
||||
'@buape/carbon@0.0.0-beta-20260317045421':
|
||||
resolution: {integrity: sha512-yM+r5iSxA/iG8CZ2VhK+EkcBQV+y45WLgF7kuczt2Ul1yixjXSCCcM80GppsklfUv7pqM4Dui+7w1WB3f5p7Kg==}
|
||||
|
||||
'@cacheable/memory@2.0.7':
|
||||
resolution: {integrity: sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==}
|
||||
|
||||
@ -7494,7 +7497,27 @@ snapshots:
|
||||
dependencies:
|
||||
css-tree: 3.2.1
|
||||
|
||||
'@buape/carbon@0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1)':
|
||||
'@buape/carbon@0.0.0-beta-20260216184201(hono@4.12.8)(opusscript@0.1.1)':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
discord-api-types: 0.38.37
|
||||
optionalDependencies:
|
||||
'@cloudflare/workers-types': 4.20260120.0
|
||||
'@discordjs/voice': 0.19.0(@discordjs/opus@0.10.0)(opusscript@0.1.1)
|
||||
'@hono/node-server': 1.19.10(hono@4.12.8)
|
||||
'@types/bun': 1.3.9
|
||||
'@types/ws': 8.18.1
|
||||
ws: 8.19.0
|
||||
transitivePeerDependencies:
|
||||
- '@discordjs/opus'
|
||||
- bufferutil
|
||||
- ffmpeg-static
|
||||
- hono
|
||||
- node-opus
|
||||
- opusscript
|
||||
- utf-8-validate
|
||||
|
||||
'@buape/carbon@0.0.0-beta-20260317045421(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1)':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
discord-api-types: 0.38.37
|
||||
@ -12415,7 +12438,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@agentclientprotocol/sdk': 0.16.1(zod@4.3.6)
|
||||
'@aws-sdk/client-bedrock': 3.1009.0
|
||||
'@buape/carbon': 0.0.0-beta-20260216184201(@discordjs/opus@0.10.0)(hono@4.12.8)(opusscript@0.1.1)
|
||||
'@buape/carbon': 0.0.0-beta-20260216184201(hono@4.12.8)(opusscript@0.1.1)
|
||||
'@clack/prompts': 1.1.0
|
||||
'@discordjs/voice': 0.19.2(@discordjs/opus@0.10.0)(opusscript@0.1.1)
|
||||
'@grammyjs/runner': 2.0.3(grammy@1.41.1)
|
||||
|
||||
@ -267,9 +267,6 @@ const parseEnvNumber = (name, fallback) => {
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
};
|
||||
const shardedCi = isCI && shardCount > 1;
|
||||
const shardedCiTopLevelConcurrency = shardedCi
|
||||
? Math.max(1, parseEnvNumber("OPENCLAW_TEST_TOP_LEVEL_CONCURRENCY", 3))
|
||||
: null;
|
||||
const allKnownUnitFiles = allKnownTestFiles.filter((file) => {
|
||||
return isUnitConfigTestFile(file);
|
||||
});
|
||||
@ -590,6 +587,22 @@ const topLevelParallelEnabled =
|
||||
testProfile !== "serial" &&
|
||||
!(!isCI && nodeMajor >= 25) &&
|
||||
!isMacMiniProfile;
|
||||
const defaultTopLevelParallelLimit =
|
||||
testProfile === "serial"
|
||||
? 1
|
||||
: testProfile === "low"
|
||||
? 2
|
||||
: testProfile === "max"
|
||||
? 5
|
||||
: highMemLocalHost
|
||||
? 4
|
||||
: lowMemLocalHost
|
||||
? 2
|
||||
: 3;
|
||||
const topLevelParallelLimit = Math.max(
|
||||
1,
|
||||
parseEnvNumber("OPENCLAW_TEST_TOP_LEVEL_CONCURRENCY", defaultTopLevelParallelLimit),
|
||||
);
|
||||
const overrideWorkers = Number.parseInt(process.env.OPENCLAW_TEST_WORKERS ?? "", 10);
|
||||
const resolvedOverride =
|
||||
Number.isFinite(overrideWorkers) && overrideWorkers > 0 ? overrideWorkers : null;
|
||||
@ -1107,11 +1120,10 @@ const runEntriesWithLimit = async (entries, extraArgs = [], concurrency = 1) =>
|
||||
|
||||
const runEntries = async (entries, extraArgs = []) => {
|
||||
if (topLevelParallelEnabled) {
|
||||
if (shardedCiTopLevelConcurrency !== null) {
|
||||
return runEntriesWithLimit(entries, extraArgs, shardedCiTopLevelConcurrency);
|
||||
}
|
||||
const codes = await Promise.all(entries.map((entry) => run(entry, extraArgs)));
|
||||
return codes.find((code) => code !== 0);
|
||||
// Keep a bounded number of top-level Vitest processes in flight. As the
|
||||
// singleton lane list grows, unbounded Promise.all scheduling turns
|
||||
// isolation into cross-process contention and can reintroduce timeouts.
|
||||
return runEntriesWithLimit(entries, extraArgs, topLevelParallelLimit);
|
||||
}
|
||||
|
||||
return runEntriesWithLimit(entries, extraArgs);
|
||||
|
||||
@ -3,7 +3,6 @@ import { discordOutbound } from "../../../../extensions/discord/src/outbound-ada
|
||||
import { whatsappOutbound } from "../../../../extensions/whatsapp/src/outbound-adapter.js";
|
||||
import { zaloPlugin } from "../../../../extensions/zalo/src/channel.js";
|
||||
import { sendMessageZalo } from "../../../../extensions/zalo/src/send.js";
|
||||
import "./../../../../extensions/zalouser/src/accounts.test-mocks.js";
|
||||
import { zalouserPlugin } from "../../../../extensions/zalouser/src/channel.js";
|
||||
import { setZalouserRuntime } from "../../../../extensions/zalouser/src/runtime.js";
|
||||
import { sendMessageZalouser } from "../../../../extensions/zalouser/src/send.js";
|
||||
@ -19,6 +18,47 @@ vi.mock("../../../../extensions/zalo/src/send.js", () => ({
|
||||
sendMessageZalo: vi.fn().mockResolvedValue({ ok: true, messageId: "zl-1" }),
|
||||
}));
|
||||
|
||||
// This suite only validates payload adaptation. Keep zalouser's runtime-only
|
||||
// ZCA import graph mocked so local contract runs don't depend on native socket
|
||||
// deps being resolved through the extension runtime seam.
|
||||
vi.mock("../../../../extensions/zalouser/src/accounts.js", () => ({
|
||||
listZalouserAccountIds: vi.fn(() => ["default"]),
|
||||
resolveDefaultZalouserAccountId: vi.fn(() => "default"),
|
||||
resolveZalouserAccountSync: vi.fn(() => ({
|
||||
accountId: "default",
|
||||
profile: "default",
|
||||
name: "test",
|
||||
enabled: true,
|
||||
authenticated: true,
|
||||
config: {},
|
||||
})),
|
||||
getZcaUserInfo: vi.fn(async () => null),
|
||||
checkZcaAuthenticated: vi.fn(async () => false),
|
||||
}));
|
||||
|
||||
vi.mock("../../../../extensions/zalouser/src/zalo-js.js", () => ({
|
||||
checkZaloAuthenticated: vi.fn(async () => false),
|
||||
getZaloUserInfo: vi.fn(async () => null),
|
||||
listZaloFriendsMatching: vi.fn(async () => []),
|
||||
listZaloGroupMembers: vi.fn(async () => []),
|
||||
listZaloGroupsMatching: vi.fn(async () => []),
|
||||
logoutZaloProfile: vi.fn(async () => {}),
|
||||
resolveZaloAllowFromEntries: vi.fn(async ({ entries }: { entries: string[] }) =>
|
||||
entries.map((entry) => ({ input: entry, resolved: true, id: entry, note: undefined })),
|
||||
),
|
||||
resolveZaloGroupsByEntries: vi.fn(async ({ entries }: { entries: string[] }) =>
|
||||
entries.map((entry) => ({ input: entry, resolved: true, id: entry, note: undefined })),
|
||||
),
|
||||
startZaloQrLogin: vi.fn(async () => ({
|
||||
message: "qr pending",
|
||||
qrDataUrl: undefined,
|
||||
})),
|
||||
waitForZaloQrLogin: vi.fn(async () => ({
|
||||
connected: false,
|
||||
message: "login pending",
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../../../../extensions/zalouser/src/send.js", () => ({
|
||||
sendMessageZalouser: vi.fn().mockResolvedValue({ ok: true, messageId: "zlu-1" }),
|
||||
sendReactionZalouser: vi.fn().mockResolvedValue({ ok: true }),
|
||||
|
||||
@ -554,9 +554,14 @@ describe("applyXiaomiConfig", () => {
|
||||
it("adds Xiaomi provider with correct settings", () => {
|
||||
const cfg = applyXiaomiConfig({});
|
||||
expect(cfg.models?.providers?.xiaomi).toMatchObject({
|
||||
baseUrl: "https://api.xiaomimimo.com/anthropic",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.xiaomimimo.com/v1",
|
||||
api: "openai-completions",
|
||||
});
|
||||
expect(cfg.models?.providers?.xiaomi?.models.map((m) => m.id)).toEqual([
|
||||
"mimo-v2-flash",
|
||||
"mimo-v2-pro",
|
||||
"mimo-v2-omni",
|
||||
]);
|
||||
expect(resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model)).toBe("xiaomi/mimo-v2-flash");
|
||||
});
|
||||
|
||||
@ -570,12 +575,14 @@ describe("applyXiaomiConfig", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(cfg.models?.providers?.xiaomi?.baseUrl).toBe("https://api.xiaomimimo.com/anthropic");
|
||||
expect(cfg.models?.providers?.xiaomi?.api).toBe("anthropic-messages");
|
||||
expect(cfg.models?.providers?.xiaomi?.baseUrl).toBe("https://api.xiaomimimo.com/v1");
|
||||
expect(cfg.models?.providers?.xiaomi?.api).toBe("openai-completions");
|
||||
expect(cfg.models?.providers?.xiaomi?.apiKey).toBe("old-key");
|
||||
expect(cfg.models?.providers?.xiaomi?.models.map((m) => m.id)).toEqual([
|
||||
"custom-model",
|
||||
"mimo-v2-flash",
|
||||
"mimo-v2-pro",
|
||||
"mimo-v2-omni",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -748,7 +748,9 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
const modelUsed = finalRunResult.meta?.agentMeta?.model ?? fallbackModel ?? model;
|
||||
const providerUsed = finalRunResult.meta?.agentMeta?.provider ?? fallbackProvider ?? provider;
|
||||
const contextTokens =
|
||||
agentCfg?.contextTokens ?? lookupContextTokens(modelUsed) ?? DEFAULT_CONTEXT_TOKENS;
|
||||
agentCfg?.contextTokens ??
|
||||
lookupContextTokens(modelUsed, { allowAsyncLoad: false }) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
setSessionRuntimeModel(cronSession.sessionEntry, {
|
||||
provider: providerUsed,
|
||||
|
||||
@ -162,6 +162,36 @@ describe("device pairing tokens", () => {
|
||||
expect(paired?.scopes).toEqual(["operator.read", "operator.write"]);
|
||||
});
|
||||
|
||||
test("keeps superseded requests interactive when an existing pending request is interactive", async () => {
|
||||
const baseDir = await mkdtemp(join(tmpdir(), "openclaw-device-pairing-"));
|
||||
const first = await requestDevicePairing(
|
||||
{
|
||||
deviceId: "device-1",
|
||||
publicKey: "public-key-1",
|
||||
role: "node",
|
||||
scopes: [],
|
||||
silent: false,
|
||||
},
|
||||
baseDir,
|
||||
);
|
||||
expect(first.request.silent).toBe(false);
|
||||
|
||||
const second = await requestDevicePairing(
|
||||
{
|
||||
deviceId: "device-1",
|
||||
publicKey: "public-key-1",
|
||||
role: "operator",
|
||||
scopes: ["operator.read"],
|
||||
silent: true,
|
||||
},
|
||||
baseDir,
|
||||
);
|
||||
|
||||
expect(second.created).toBe(true);
|
||||
expect(second.request.requestId).not.toBe(first.request.requestId);
|
||||
expect(second.request.silent).toBe(false);
|
||||
});
|
||||
|
||||
test("rejects bootstrap token replay before pending scope escalation can be approved", async () => {
|
||||
const baseDir = await mkdtemp(join(tmpdir(), "openclaw-device-pairing-"));
|
||||
const issued = await issueDeviceBootstrapToken({ baseDir });
|
||||
|
||||
@ -236,6 +236,15 @@ function refreshPendingDevicePairingRequest(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSupersededPendingSilent(params: {
|
||||
existing: readonly DevicePairingPendingRequest[];
|
||||
incomingSilent: boolean | undefined;
|
||||
}): boolean {
|
||||
return Boolean(
|
||||
params.incomingSilent && params.existing.every((pending) => pending.silent === true),
|
||||
);
|
||||
}
|
||||
|
||||
function buildPendingDevicePairingRequest(params: {
|
||||
requestId?: string;
|
||||
deviceId: string;
|
||||
@ -394,7 +403,15 @@ export async function requestDevicePairing(
|
||||
const superseded = buildPendingDevicePairingRequest({
|
||||
deviceId,
|
||||
isRepair,
|
||||
req,
|
||||
req: {
|
||||
...req,
|
||||
// Preserve interactive visibility when superseding pending requests:
|
||||
// if any previous pending request was interactive, keep this one interactive.
|
||||
silent: resolveSupersededPendingSilent({
|
||||
existing: pendingForDevice,
|
||||
incomingSilent: req.silent,
|
||||
}),
|
||||
},
|
||||
});
|
||||
state.pendingById[superseded.requestId] = superseded;
|
||||
await persistState(state, baseDir);
|
||||
|
||||
@ -11,6 +11,23 @@ function getServerArgs(value: unknown): unknown[] | undefined {
|
||||
return isRecord(value) && Array.isArray(value.args) ? value.args : undefined;
|
||||
}
|
||||
|
||||
function normalizePathForAssertion(value: string | undefined): string | undefined {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
return path.normalize(value).replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
async function expectResolvedPathEqual(actual: unknown, expected: string): Promise<void> {
|
||||
expect(typeof actual).toBe("string");
|
||||
if (typeof actual !== "string") {
|
||||
return;
|
||||
}
|
||||
expect(normalizePathForAssertion(await fs.realpath(actual))).toBe(
|
||||
normalizePathForAssertion(await fs.realpath(expected)),
|
||||
);
|
||||
}
|
||||
|
||||
const tempHarness = createBundleMcpTempHarness();
|
||||
|
||||
afterEach(async () => {
|
||||
@ -55,8 +72,10 @@ describe("loadEnabledBundleMcpConfig", () => {
|
||||
if (!loadedServerPath) {
|
||||
throw new Error("expected bundled MCP args to include the server path");
|
||||
}
|
||||
expect(await fs.realpath(loadedServerPath)).toBe(resolvedServerPath);
|
||||
expect(loadedServer.cwd).toBe(resolvedPluginRoot);
|
||||
expect(normalizePathForAssertion(await fs.realpath(loadedServerPath))).toBe(
|
||||
normalizePathForAssertion(resolvedServerPath),
|
||||
);
|
||||
await expectResolvedPathEqual(loadedServer.cwd, resolvedPluginRoot);
|
||||
} finally {
|
||||
env.restore();
|
||||
}
|
||||
@ -178,20 +197,35 @@ describe("loadEnabledBundleMcpConfig", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const resolvedPluginRoot = await fs.realpath(pluginRoot);
|
||||
const loadedServer = loaded.config.mcpServers.inlineProbe;
|
||||
const loadedArgs = getServerArgs(loadedServer);
|
||||
const loadedCommand = isRecord(loadedServer) ? loadedServer.command : undefined;
|
||||
const loadedCwd = isRecord(loadedServer) ? loadedServer.cwd : undefined;
|
||||
const loadedEnv =
|
||||
isRecord(loadedServer) && isRecord(loadedServer.env) ? loadedServer.env : {};
|
||||
|
||||
expect(loaded.diagnostics).toEqual([]);
|
||||
expect(loaded.config.mcpServers.inlineProbe).toEqual({
|
||||
command: path.join(resolvedPluginRoot, "bin", "server.sh"),
|
||||
args: [
|
||||
path.join(resolvedPluginRoot, "servers", "probe.mjs"),
|
||||
path.join(resolvedPluginRoot, "local-probe.mjs"),
|
||||
],
|
||||
cwd: resolvedPluginRoot,
|
||||
env: {
|
||||
PLUGIN_ROOT: resolvedPluginRoot,
|
||||
},
|
||||
});
|
||||
await expectResolvedPathEqual(loadedCwd, pluginRoot);
|
||||
expect(typeof loadedCommand).toBe("string");
|
||||
expect(loadedArgs).toHaveLength(2);
|
||||
expect(typeof loadedEnv.PLUGIN_ROOT).toBe("string");
|
||||
if (typeof loadedCommand !== "string" || typeof loadedCwd !== "string") {
|
||||
throw new Error("expected inline bundled MCP server to expose command and cwd");
|
||||
}
|
||||
expect(normalizePathForAssertion(path.relative(loadedCwd, loadedCommand))).toBe(
|
||||
normalizePathForAssertion(path.join("bin", "server.sh")),
|
||||
);
|
||||
expect(
|
||||
loadedArgs?.map((entry) =>
|
||||
typeof entry === "string"
|
||||
? normalizePathForAssertion(path.relative(loadedCwd, entry))
|
||||
: entry,
|
||||
),
|
||||
).toEqual([
|
||||
normalizePathForAssertion(path.join("servers", "probe.mjs")),
|
||||
normalizePathForAssertion("local-probe.mjs"),
|
||||
]);
|
||||
await expectResolvedPathEqual(loadedEnv.PLUGIN_ROOT, pluginRoot);
|
||||
} finally {
|
||||
env.restore();
|
||||
}
|
||||
|
||||
@ -109,6 +109,17 @@ const { registerSessionBindingAdapter, unregisterSessionBindingAdapter } =
|
||||
await import("../infra/outbound/session-binding-service.js");
|
||||
|
||||
type PluginBindingRequest = Awaited<ReturnType<typeof requestPluginConversationBinding>>;
|
||||
type ConversationBindingModule = typeof import("./conversation-binding.js");
|
||||
|
||||
const conversationBindingModuleUrl = new URL("./conversation-binding.ts", import.meta.url).href;
|
||||
|
||||
async function importConversationBindingModule(
|
||||
cacheBust: string,
|
||||
): Promise<ConversationBindingModule> {
|
||||
return (await import(
|
||||
`${conversationBindingModuleUrl}?t=${cacheBust}`
|
||||
)) as ConversationBindingModule;
|
||||
}
|
||||
|
||||
function createAdapter(channel: string, accountId: string): SessionBindingAdapter {
|
||||
return {
|
||||
@ -290,6 +301,108 @@ describe("plugin conversation binding approvals", () => {
|
||||
expect(differentAccount.status).toBe("pending");
|
||||
});
|
||||
|
||||
it("shares pending bind approvals across duplicate module instances", async () => {
|
||||
const first = await importConversationBindingModule(`first-${Date.now()}`);
|
||||
const second = await importConversationBindingModule(`second-${Date.now()}`);
|
||||
|
||||
first.__testing.reset();
|
||||
|
||||
const request = await first.requestPluginConversationBinding({
|
||||
pluginId: "codex",
|
||||
pluginName: "Codex App Server",
|
||||
pluginRoot: "/plugins/codex-a",
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-10099:topic:77",
|
||||
parentConversationId: "-10099",
|
||||
threadId: "77",
|
||||
},
|
||||
binding: { summary: "Bind this conversation to Codex thread abc." },
|
||||
});
|
||||
|
||||
expect(request.status).toBe("pending");
|
||||
if (request.status !== "pending") {
|
||||
throw new Error("expected pending bind request");
|
||||
}
|
||||
|
||||
await expect(
|
||||
second.resolvePluginConversationBindingApproval({
|
||||
approvalId: request.approvalId,
|
||||
decision: "allow-once",
|
||||
senderId: "user-1",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
status: "approved",
|
||||
binding: expect.objectContaining({
|
||||
pluginId: "codex",
|
||||
pluginRoot: "/plugins/codex-a",
|
||||
conversationId: "-10099:topic:77",
|
||||
}),
|
||||
});
|
||||
|
||||
second.__testing.reset();
|
||||
});
|
||||
|
||||
it("shares persistent approvals across duplicate module instances", async () => {
|
||||
const first = await importConversationBindingModule(`first-${Date.now()}`);
|
||||
const second = await importConversationBindingModule(`second-${Date.now()}`);
|
||||
|
||||
first.__testing.reset();
|
||||
|
||||
const request = await first.requestPluginConversationBinding({
|
||||
pluginId: "codex",
|
||||
pluginName: "Codex App Server",
|
||||
pluginRoot: "/plugins/codex-a",
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-10099:topic:77",
|
||||
parentConversationId: "-10099",
|
||||
threadId: "77",
|
||||
},
|
||||
binding: { summary: "Bind this conversation to Codex thread abc." },
|
||||
});
|
||||
|
||||
expect(request.status).toBe("pending");
|
||||
if (request.status !== "pending") {
|
||||
throw new Error("expected pending bind request");
|
||||
}
|
||||
|
||||
await expect(
|
||||
second.resolvePluginConversationBindingApproval({
|
||||
approvalId: request.approvalId,
|
||||
decision: "allow-always",
|
||||
senderId: "user-1",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
status: "approved",
|
||||
decision: "allow-always",
|
||||
});
|
||||
|
||||
const rebound = await first.requestPluginConversationBinding({
|
||||
pluginId: "codex",
|
||||
pluginName: "Codex App Server",
|
||||
pluginRoot: "/plugins/codex-a",
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-10099:topic:78",
|
||||
parentConversationId: "-10099",
|
||||
threadId: "78",
|
||||
},
|
||||
binding: { summary: "Bind this conversation to Codex thread def." },
|
||||
});
|
||||
|
||||
expect(rebound.status).toBe("bound");
|
||||
|
||||
first.__testing.reset();
|
||||
fs.rmSync(approvalsPath, { force: true });
|
||||
});
|
||||
|
||||
it("does not share persistent approvals across plugin roots even with the same plugin id", async () => {
|
||||
const request = await requestPluginConversationBinding({
|
||||
pluginId: "codex",
|
||||
|
||||
@ -11,6 +11,7 @@ import { expandHomePrefix } from "../infra/home-dir.js";
|
||||
import { writeJsonAtomic } from "../infra/json-files.js";
|
||||
import { type ConversationRef } from "../infra/outbound/session-binding-service.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveGlobalMap, resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||
import { getActivePluginRegistry } from "./runtime.js";
|
||||
import type {
|
||||
PluginConversationBinding,
|
||||
@ -104,24 +105,26 @@ type PluginBindingResolveResult =
|
||||
status: "expired";
|
||||
};
|
||||
|
||||
const pendingRequests = new Map<string, PendingPluginBindingRequest>();
|
||||
const PLUGIN_BINDING_PENDING_REQUESTS_KEY = Symbol.for("openclaw.pluginBindingPendingRequests");
|
||||
|
||||
const pendingRequests = resolveGlobalMap<string, PendingPluginBindingRequest>(
|
||||
PLUGIN_BINDING_PENDING_REQUESTS_KEY,
|
||||
);
|
||||
|
||||
type PluginBindingGlobalState = {
|
||||
fallbackNoticeBindingIds: Set<string>;
|
||||
approvalsCache: PluginBindingApprovalsFile | null;
|
||||
approvalsLoaded: boolean;
|
||||
};
|
||||
|
||||
const pluginBindingGlobalStateKey = Symbol.for("openclaw.plugins.binding.global-state");
|
||||
|
||||
let approvalsCache: PluginBindingApprovalsFile | null = null;
|
||||
let approvalsLoaded = false;
|
||||
|
||||
function getPluginBindingGlobalState(): PluginBindingGlobalState {
|
||||
const globalStore = globalThis as typeof globalThis & {
|
||||
[pluginBindingGlobalStateKey]?: PluginBindingGlobalState;
|
||||
};
|
||||
return (globalStore[pluginBindingGlobalStateKey] ??= {
|
||||
return resolveGlobalSingleton<PluginBindingGlobalState>(pluginBindingGlobalStateKey, () => ({
|
||||
fallbackNoticeBindingIds: new Set<string>(),
|
||||
});
|
||||
approvalsCache: null,
|
||||
approvalsLoaded: false,
|
||||
}));
|
||||
}
|
||||
|
||||
function resolveApprovalsPath(): string {
|
||||
@ -297,8 +300,9 @@ function loadApprovalsFromDisk(): PluginBindingApprovalsFile {
|
||||
async function saveApprovals(file: PluginBindingApprovalsFile): Promise<void> {
|
||||
const filePath = resolveApprovalsPath();
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
approvalsCache = file;
|
||||
approvalsLoaded = true;
|
||||
const state = getPluginBindingGlobalState();
|
||||
state.approvalsCache = file;
|
||||
state.approvalsLoaded = true;
|
||||
await writeJsonAtomic(filePath, file, {
|
||||
mode: 0o600,
|
||||
trailingNewline: true,
|
||||
@ -306,11 +310,12 @@ async function saveApprovals(file: PluginBindingApprovalsFile): Promise<void> {
|
||||
}
|
||||
|
||||
function getApprovals(): PluginBindingApprovalsFile {
|
||||
if (!approvalsLoaded || !approvalsCache) {
|
||||
approvalsCache = loadApprovalsFromDisk();
|
||||
approvalsLoaded = true;
|
||||
const state = getPluginBindingGlobalState();
|
||||
if (!state.approvalsLoaded || !state.approvalsCache) {
|
||||
state.approvalsCache = loadApprovalsFromDisk();
|
||||
state.approvalsLoaded = true;
|
||||
}
|
||||
return approvalsCache;
|
||||
return state.approvalsCache;
|
||||
}
|
||||
|
||||
function hasPersistentApproval(params: {
|
||||
@ -836,8 +841,9 @@ export function buildPluginBindingResolvedText(params: PluginBindingResolveResul
|
||||
export const __testing = {
|
||||
reset() {
|
||||
pendingRequests.clear();
|
||||
approvalsCache = null;
|
||||
approvalsLoaded = false;
|
||||
getPluginBindingGlobalState().fallbackNoticeBindingIds.clear();
|
||||
const state = getPluginBindingGlobalState();
|
||||
state.approvalsCache = null;
|
||||
state.approvalsLoaded = false;
|
||||
state.fallbackNoticeBindingIds.clear();
|
||||
},
|
||||
};
|
||||
|
||||
@ -49,6 +49,14 @@ type InteractiveDispatchParams =
|
||||
respond: PluginInteractiveSlackHandlerContext["respond"];
|
||||
};
|
||||
|
||||
type InteractiveModule = typeof import("./interactive.js");
|
||||
|
||||
const interactiveModuleUrl = new URL("./interactive.ts", import.meta.url).href;
|
||||
|
||||
async function importInteractiveModule(cacheBust: string): Promise<InteractiveModule> {
|
||||
return (await import(`${interactiveModuleUrl}?t=${cacheBust}`)) as InteractiveModule;
|
||||
}
|
||||
|
||||
async function expectDedupedInteractiveDispatch(params: {
|
||||
baseParams: InteractiveDispatchParams;
|
||||
handler: ReturnType<typeof vi.fn>;
|
||||
@ -172,6 +180,66 @@ describe("plugin interactive handlers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shares interactive handlers across duplicate module instances", async () => {
|
||||
const first = await importInteractiveModule(`first-${Date.now()}`);
|
||||
const second = await importInteractiveModule(`second-${Date.now()}`);
|
||||
const handler = vi.fn(async () => ({ handled: true }));
|
||||
|
||||
first.clearPluginInteractiveHandlers();
|
||||
|
||||
expect(
|
||||
first.registerPluginInteractiveHandler("codex-plugin", {
|
||||
channel: "telegram",
|
||||
namespace: "codexapp",
|
||||
handler,
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
|
||||
await expect(
|
||||
second.dispatchPluginInteractiveHandler({
|
||||
channel: "telegram",
|
||||
data: "codexapp:resume:thread-1",
|
||||
callbackId: "cb-shared-1",
|
||||
ctx: {
|
||||
accountId: "default",
|
||||
callbackId: "cb-shared-1",
|
||||
conversationId: "-10099:topic:77",
|
||||
parentConversationId: "-10099",
|
||||
senderId: "user-1",
|
||||
senderUsername: "ada",
|
||||
threadId: 77,
|
||||
isGroup: true,
|
||||
isForum: true,
|
||||
auth: { isAuthorizedSender: true },
|
||||
callbackMessage: {
|
||||
messageId: 55,
|
||||
chatId: "-10099",
|
||||
messageText: "Pick a thread",
|
||||
},
|
||||
},
|
||||
respond: {
|
||||
reply: vi.fn(async () => {}),
|
||||
editMessage: vi.fn(async () => {}),
|
||||
editButtons: vi.fn(async () => {}),
|
||||
clearButtons: vi.fn(async () => {}),
|
||||
deleteMessage: vi.fn(async () => {}),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({ matched: true, handled: true, duplicate: false });
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
callback: expect.objectContaining({
|
||||
namespace: "codexapp",
|
||||
payload: "resume:thread-1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
second.clearPluginInteractiveHandlers();
|
||||
});
|
||||
|
||||
it("rejects duplicate namespace registrations", () => {
|
||||
const first = registerPluginInteractiveHandler("plugin-a", {
|
||||
channel: "telegram",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { createDedupeCache } from "../infra/dedupe.js";
|
||||
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||
import {
|
||||
dispatchDiscordInteractiveHandler,
|
||||
dispatchSlackInteractiveHandler,
|
||||
@ -33,11 +34,23 @@ type InteractiveDispatchResult =
|
||||
| { matched: false; handled: false; duplicate: false }
|
||||
| { matched: true; handled: boolean; duplicate: boolean };
|
||||
|
||||
const interactiveHandlers = new Map<string, RegisteredInteractiveHandler>();
|
||||
const callbackDedupe = createDedupeCache({
|
||||
ttlMs: 5 * 60_000,
|
||||
maxSize: 4096,
|
||||
});
|
||||
type InteractiveState = {
|
||||
interactiveHandlers: Map<string, RegisteredInteractiveHandler>;
|
||||
callbackDedupe: ReturnType<typeof createDedupeCache>;
|
||||
};
|
||||
|
||||
const PLUGIN_INTERACTIVE_STATE_KEY = Symbol.for("openclaw.pluginInteractiveState");
|
||||
|
||||
const state = resolveGlobalSingleton<InteractiveState>(PLUGIN_INTERACTIVE_STATE_KEY, () => ({
|
||||
interactiveHandlers: new Map<string, RegisteredInteractiveHandler>(),
|
||||
callbackDedupe: createDedupeCache({
|
||||
ttlMs: 5 * 60_000,
|
||||
maxSize: 4096,
|
||||
}),
|
||||
}));
|
||||
|
||||
const interactiveHandlers = state.interactiveHandlers;
|
||||
const callbackDedupe = state.callbackDedupe;
|
||||
|
||||
function toRegistryKey(channel: string, namespace: string): string {
|
||||
return `${channel.trim().toLowerCase()}:${namespace.trim()}`;
|
||||
|
||||
120
test/fixtures/test-parallel.behavior.json
vendored
120
test/fixtures/test-parallel.behavior.json
vendored
@ -110,6 +110,126 @@
|
||||
{
|
||||
"file": "src/memory/manager.readonly-recovery.test.ts",
|
||||
"reason": "Readonly recovery coverage exercises sqlite reopen flows and is safer outside shared unit-fast forks."
|
||||
},
|
||||
{
|
||||
"file": "src/acp/persistent-bindings.test.ts",
|
||||
"reason": "Persistent bindings coverage retained a large unit-fast heap spike on Linux CI and is safer outside the shared lane."
|
||||
},
|
||||
{
|
||||
"file": "src/channels/plugins/setup-wizard-helpers.test.ts",
|
||||
"reason": "Setup wizard helper coverage retained the largest shared unit-fast heap spike on Linux Node 24 CI."
|
||||
},
|
||||
{
|
||||
"file": "src/cli/config-cli.integration.test.ts",
|
||||
"reason": "Config CLI integration coverage retained a large shared unit-fast heap spike on Linux CI."
|
||||
},
|
||||
{
|
||||
"file": "src/cli/config-cli.test.ts",
|
||||
"reason": "Config CLI coverage retained a large shared unit-fast heap spike on Linux Node 24 CI."
|
||||
},
|
||||
{
|
||||
"file": "src/cli/plugins-cli.test.ts",
|
||||
"reason": "Plugins CLI coverage retained a broad plugin graph in shared unit-fast forks on Linux CI."
|
||||
},
|
||||
{
|
||||
"file": "src/config/plugin-auto-enable.test.ts",
|
||||
"reason": "Plugin auto-enable coverage retained a large shared unit-fast heap spike on Linux Node 22 CI."
|
||||
},
|
||||
{
|
||||
"file": "src/cron/service.runs-one-shot-main-job-disables-it.test.ts",
|
||||
"reason": "One-shot cron service coverage retained a top shared unit-fast heap spike in the March 19, 2026 Linux Node 22 OOM lane."
|
||||
},
|
||||
{
|
||||
"file": "src/cron/isolated-agent/run.sandbox-config-preserved.test.ts",
|
||||
"reason": "Isolated-agent sandbox config coverage retained a large shared unit-fast heap spike on Linux CI."
|
||||
},
|
||||
{
|
||||
"file": "src/cron/isolated-agent.direct-delivery-core-channels.test.ts",
|
||||
"reason": "Direct-delivery isolated-agent coverage retained a top shared unit-fast heap spike in the March 20, 2026 Linux Node 24 OOM lane."
|
||||
},
|
||||
{
|
||||
"file": "src/cron/service.issue-regressions.test.ts",
|
||||
"reason": "Issue regression cron coverage retained the largest shared unit-fast heap spike in the March 20, 2026 Linux Node 22 and Node 24 OOM lanes."
|
||||
},
|
||||
{
|
||||
"file": "src/cron/store.test.ts",
|
||||
"reason": "Cron store coverage retained a large shared unit-fast heap spike on Linux Node 24 CI."
|
||||
},
|
||||
{
|
||||
"file": "src/context-engine/context-engine.test.ts",
|
||||
"reason": "Context-engine coverage retained the largest shared unit-fast heap spike in the March 20, 2026 Linux Node 24 shard 1 OOM lane."
|
||||
},
|
||||
{
|
||||
"file": "src/acp/control-plane/manager.test.ts",
|
||||
"reason": "ACP control-plane manager coverage retained a top shared unit-fast heap spike in the March 20, 2026 Linux Node 22 and Node 24 OOM lanes."
|
||||
},
|
||||
{
|
||||
"file": "src/acp/translator.stop-reason.test.ts",
|
||||
"reason": "ACP translator stop-reason coverage retained a top shared unit-fast heap spike in the March 20, 2026 Linux Node 24 shard 2 OOM lane."
|
||||
},
|
||||
{
|
||||
"file": "src/infra/exec-approval-forwarder.test.ts",
|
||||
"reason": "Exec approval forwarder coverage retained a top shared unit-fast heap spike in the March 19, 2026 Linux Node 22 OOM lane."
|
||||
},
|
||||
{
|
||||
"file": "src/infra/restart-stale-pids.test.ts",
|
||||
"reason": "Restart-stale-pids coverage retained a top shared unit-fast heap spike in the March 20, 2026 Linux Node 24 shard 1 OOM lane."
|
||||
},
|
||||
{
|
||||
"file": "src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts",
|
||||
"reason": "Heartbeat ack max chars coverage retained a recurring shared unit-fast heap spike across Linux CI lanes."
|
||||
},
|
||||
{
|
||||
"file": "src/infra/heartbeat-runner.returns-default-unset.test.ts",
|
||||
"reason": "Heartbeat default-unset coverage retained a large shared unit-fast heap spike on Linux Node 22 CI."
|
||||
},
|
||||
{
|
||||
"file": "src/infra/outbound/outbound-session.test.ts",
|
||||
"reason": "Outbound session coverage retained a large shared unit-fast heap spike on Linux Node 22 CI."
|
||||
},
|
||||
{
|
||||
"file": "src/infra/outbound/payloads.test.ts",
|
||||
"reason": "Outbound payload coverage retained a large shared unit-fast heap spike on Linux Node 24 CI."
|
||||
},
|
||||
{
|
||||
"file": "src/memory/manager.mistral-provider.test.ts",
|
||||
"reason": "Mistral provider coverage retained a large shared unit-fast heap spike on Linux Node 24 CI."
|
||||
},
|
||||
{
|
||||
"file": "src/memory/manager.batch.test.ts",
|
||||
"reason": "Memory manager batch coverage retained a top shared unit-fast heap spike in the March 20, 2026 Linux Node 22 and Node 24 OOM lanes."
|
||||
},
|
||||
{
|
||||
"file": "src/memory/qmd-manager.test.ts",
|
||||
"reason": "QMD manager coverage retained recurring shared unit-fast heap spikes across Linux CI lanes."
|
||||
},
|
||||
{
|
||||
"file": "src/media-understanding/providers/image.test.ts",
|
||||
"reason": "Image provider coverage retained a top shared unit-fast heap spike in the March 20, 2026 Linux Node 24 shard 2 OOM lane."
|
||||
},
|
||||
{
|
||||
"file": "src/plugins/contracts/auth.contract.test.ts",
|
||||
"reason": "Plugin auth contract coverage retained a large shared unit-fast heap spike on Linux Node 24 CI."
|
||||
},
|
||||
{
|
||||
"file": "src/plugins/contracts/discovery.contract.test.ts",
|
||||
"reason": "Plugin discovery contract coverage retained a top shared unit-fast heap spike in the March 20, 2026 Linux Node 24 shard 1 OOM lane."
|
||||
},
|
||||
{
|
||||
"file": "src/plugins/hooks.phase-hooks.test.ts",
|
||||
"reason": "Phase hooks coverage retained a top shared unit-fast heap spike in the March 20, 2026 Linux Node 24 shard 2 OOM lane."
|
||||
},
|
||||
{
|
||||
"file": "src/channels/plugins/plugins-core.test.ts",
|
||||
"reason": "Core plugin coverage retained a top shared unit-fast heap spike in the March 20, 2026 Linux Node 22 OOM lane."
|
||||
},
|
||||
{
|
||||
"file": "src/secrets/apply.test.ts",
|
||||
"reason": "Secrets apply coverage retained a large shared unit-fast heap spike on Linux Node 22 CI."
|
||||
},
|
||||
{
|
||||
"file": "src/tui/tui-command-handlers.test.ts",
|
||||
"reason": "TUI command handler coverage retained a top shared unit-fast heap spike in the March 20, 2026 Linux Node 24 shard 2 OOM lane."
|
||||
}
|
||||
],
|
||||
"threadSingleton": [
|
||||
|
||||
@ -18,6 +18,13 @@ const run = (cwd: string, cmd: string, args: string[] = [], env?: NodeJS.Process
|
||||
}).trim();
|
||||
};
|
||||
|
||||
function writeExecutable(dir: string, name: string, contents: string): void {
|
||||
writeFileSync(path.join(dir, name), contents, {
|
||||
encoding: "utf8",
|
||||
mode: 0o755,
|
||||
});
|
||||
}
|
||||
|
||||
describe("git-hooks/pre-commit (integration)", () => {
|
||||
it("does not treat staged filenames as git-add flags (e.g. --all)", () => {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "openclaw-pre-commit-"));
|
||||
@ -45,10 +52,10 @@ describe("git-hooks/pre-commit (integration)", () => {
|
||||
);
|
||||
const fakeBinDir = path.join(dir, "bin");
|
||||
mkdirSync(fakeBinDir, { recursive: true });
|
||||
writeFileSync(path.join(fakeBinDir, "node"), "#!/usr/bin/env bash\nexit 0\n", {
|
||||
encoding: "utf8",
|
||||
mode: 0o755,
|
||||
});
|
||||
writeExecutable(fakeBinDir, "node", "#!/usr/bin/env bash\nexit 0\n");
|
||||
// The hook ends with `pnpm check`, but this fixture is only exercising staged-file handling.
|
||||
// Stub pnpm too so Windows CI does not invoke a real package-manager command in the temp repo.
|
||||
writeExecutable(fakeBinDir, "pnpm", "#!/usr/bin/env bash\nexit 0\n");
|
||||
|
||||
// Create an untracked file that should NOT be staged by the hook.
|
||||
writeFileSync(path.join(dir, "secret.txt"), "do-not-stage\n", "utf8");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user