Skip to content

Commit 2bc71cd

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents 5734e0e + 9b1b56a commit 2bc71cd

65 files changed

Lines changed: 1955 additions & 85 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
99
- Docs/showcase: add a scannable hero, complete section jump links, and a responsive video grid for community examples. (#48493) Thanks @jchopard69.
1010
- Agents/local models: add `agents.defaults.localModelMode: "lean"` to drop heavyweight default tools like `browser`, `cron`, and `message`, reducing prompt size for weaker local-model setups without changing the normal path. Thanks @ImLukeF.
1111
- QA/Matrix: split Matrix live QA into a source-linked `qa-matrix` runner and keep repo-private `qa-*` surfaces out of packaged and published builds. (#66723) Thanks @gumadeiras.
12+
- Control UI/Overview: add a Model Auth status card showing OAuth token health and provider rate-limit pressure at a glance, with attention callouts when OAuth tokens are expiring or expired. Backed by a new `models.authStatus` gateway method that strips credentials and caches for 60s. (#66211) Thanks @omarshahine.
1213

1314
### Fixes
1415

@@ -43,6 +44,7 @@ Docs: https://docs.openclaw.ai
4344
- Telegram/native commands: keep Telegram command-sync cache process-local so gateway restarts re-register the menu instead of trusting stale on-disk sync state after Telegram cleared commands out-of-band. (#66730) Thanks @nightq.
4445
- Audio/self-hosted STT: restore `models.providers.*.request.allowPrivateNetwork` for audio transcription so private or LAN speech-to-text endpoints stop tripping SSRF blocks after the v2026.4.14 regression. (#66692) Thanks @jhsmith409.
4546
- QQBot/cron: guard against undefined `event.content` in `parseFaceTags` and `filterInternalMarkers` so cron-triggered agent turns with no content payload no longer crash with `TypeError: Cannot read properties of undefined (reading 'startsWith')`. (#66302) Thanks @xinmotlanthua.
47+
- CLI/plugins: stop `--dangerously-force-unsafe-install` plugin installs from falling back to hook-pack installs after security scan failures, while still preserving non-security fallback behavior for real hook packs. (#58909) Thanks @hxy91819.
4648

4749
## 2026.4.14
4850

@@ -1041,6 +1043,8 @@ Docs: https://docs.openclaw.ai
10411043
- ACPX/runtime: repair `queue owner unavailable` session recovery by replacing dead named sessions and resuming the backend session when ACPX exposes a stable session id, so the first ACP prompt no longer inherits a dead handle. (#58669) Thanks @neeravmakwana
10421044
- ACPX/runtime: retry dead-session queue-owner repair without `--resume-session` when the reported ACPX session id is stale, so recovery still creates a fresh named session instead of failing session init. Thanks @obviyus.
10431045
- Tools/web_search (Kimi): replay native Moonshot `$web_search` arguments verbatim, disable thinking for `kimi-k2.5`, and add Moonshot region/model setup prompts so bundled Kimi web search works again. (#59356) Thanks @Innocent-children.
1046+
- Auth/OpenAI Codex: persist plugin-refreshed OAuth credentials to `auth-profiles.json` before returning them, so rotated Codex refresh tokens survive restart and stop falling into `refresh_token_reused` loops. (#53082)
1047+
- Discord/gateway: hand reconnect ownership back to Carbon, keep runtime status aligned with close/reconnect state, and force-stop sockets that open without reaching READY so Discord monitors recover promptly instead of waiting on stale health timeouts. (#59019) Thanks @obviyus
10441048

10451049
## 2026.3.31
10461050

CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ Welcome to the lobster tank! 🦞
8080
- **Sliverp** - Chinese Channel: QQ, WeChat, Wecom, Dingtalk, Feishu
8181
- GitHub: [@sliverp](https://github.com/sliverp) · X: [@sliver01234](https://x.com/sliver01234)
8282

83+
- **Mason Huang** - Stability, Security, Speed
84+
- GitHub: [@hxy91819](https://github.com/hxy91819) · X: [@chenjingtalk](https://x.com/chenjingtalk)
85+
8386
## How to Contribute
8487

8588
1. **Bugs & small fixes** → Open a PR!

extensions/qa-matrix/src/runners/contract/scenario-catalog.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@ export type MatrixQaScenarioDefinition = LiveTransportScenarioDefinition<MatrixQ
3939
topology?: MatrixQaTopologySpec;
4040
};
4141

42+
export const MATRIX_QA_BLOCK_ROOM_KEY = "block";
4243
export const MATRIX_QA_DRIVER_DM_ROOM_KEY = "driver-dm";
4344
export const MATRIX_QA_DRIVER_DM_SHARED_ROOM_KEY = "driver-dm-shared";
45+
export const MATRIX_QA_HOMESERVER_ROOM_KEY = "homeserver";
4446
export const MATRIX_QA_MEMBERSHIP_ROOM_KEY = "membership";
47+
export const MATRIX_QA_RESTART_ROOM_KEY = "restart";
4548
export const MATRIX_QA_SECONDARY_ROOM_KEY = "secondary";
4649

4750
function buildMatrixQaDmTopology(
@@ -104,12 +107,30 @@ const MATRIX_QA_SECONDARY_ROOM_TOPOLOGY = buildMatrixQaSingleGroupTopology({
104107
requireMention: true,
105108
});
106109

110+
const MATRIX_QA_BLOCK_ROOM_TOPOLOGY = buildMatrixQaSingleGroupTopology({
111+
key: MATRIX_QA_BLOCK_ROOM_KEY,
112+
name: "Matrix QA Block Streaming Room",
113+
requireMention: true,
114+
});
115+
107116
const MATRIX_QA_MEMBERSHIP_ROOM_TOPOLOGY = buildMatrixQaSingleGroupTopology({
108117
key: MATRIX_QA_MEMBERSHIP_ROOM_KEY,
109118
name: "Matrix QA Membership Room",
110119
requireMention: true,
111120
});
112121

122+
const MATRIX_QA_RESTART_ROOM_TOPOLOGY = buildMatrixQaSingleGroupTopology({
123+
key: MATRIX_QA_RESTART_ROOM_KEY,
124+
name: "Matrix QA Restart Room",
125+
requireMention: true,
126+
});
127+
128+
const MATRIX_QA_HOMESERVER_ROOM_TOPOLOGY = buildMatrixQaSingleGroupTopology({
129+
key: MATRIX_QA_HOMESERVER_ROOM_KEY,
130+
name: "Matrix QA Homeserver Restart Room",
131+
requireMention: true,
132+
});
133+
113134
export const MATRIX_QA_SCENARIOS: MatrixQaScenarioDefinition[] = [
114135
{
115136
id: "matrix-thread-follow-up",
@@ -149,7 +170,20 @@ export const MATRIX_QA_SCENARIOS: MatrixQaScenarioDefinition[] = [
149170
id: "matrix-room-block-streaming",
150171
timeoutMs: 45_000,
151172
title: "Matrix block streaming preserves completed quiet preview blocks",
173+
topology: MATRIX_QA_BLOCK_ROOM_TOPOLOGY,
152174
configOverrides: {
175+
agentDefaults: {
176+
blockStreamingChunk: {
177+
breakPreference: "newline",
178+
maxChars: 48,
179+
minChars: 1,
180+
},
181+
blockStreamingCoalesce: {
182+
idleMs: 0,
183+
maxChars: 48,
184+
minChars: 1,
185+
},
186+
},
153187
blockStreaming: true,
154188
streaming: "quiet",
155189
},
@@ -228,6 +262,7 @@ export const MATRIX_QA_SCENARIOS: MatrixQaScenarioDefinition[] = [
228262
standardId: "restart-resume",
229263
timeoutMs: 60_000,
230264
title: "Matrix lane resumes cleanly after gateway restart",
265+
topology: MATRIX_QA_RESTART_ROOM_TOPOLOGY,
231266
},
232267
{
233268
id: "matrix-room-membership-loss",
@@ -239,6 +274,7 @@ export const MATRIX_QA_SCENARIOS: MatrixQaScenarioDefinition[] = [
239274
id: "matrix-homeserver-restart-resume",
240275
timeoutMs: 75_000,
241276
title: "Matrix lane resumes after homeserver restart",
277+
topology: MATRIX_QA_HOMESERVER_ROOM_TOPOLOGY,
242278
},
243279
{
244280
id: "matrix-mention-gating",

extensions/qa-matrix/src/runners/contract/scenario-runtime-room.ts

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { randomUUID } from "node:crypto";
22
import type { MatrixQaObservedEvent } from "../../substrate/events.js";
33
import {
4+
MATRIX_QA_BLOCK_ROOM_KEY,
5+
MATRIX_QA_HOMESERVER_ROOM_KEY,
46
MATRIX_QA_MEMBERSHIP_ROOM_KEY,
7+
MATRIX_QA_RESTART_ROOM_KEY,
58
resolveMatrixQaScenarioRoomId,
69
} from "./scenario-catalog.js";
710
import {
@@ -218,30 +221,51 @@ export async function runRoomThreadReplyOverrideScenario(context: MatrixQaScenar
218221
}
219222

220223
export async function runObserverAllowlistOverrideScenario(context: MatrixQaScenarioContext) {
221-
const result = await runTopLevelMentionScenario({
224+
const { client, startSince } = await primeMatrixQaActorCursor({
222225
accessToken: context.observerAccessToken,
223226
actorId: "observer",
224227
baseUrl: context.baseUrl,
228+
syncState: context.syncState,
229+
});
230+
const token = `MATRIX_QA_OBSERVER_ALLOWLIST_${randomUUID().slice(0, 8).toUpperCase()}`;
231+
const body = buildMentionPrompt(context.sutUserId, token);
232+
const driverEventId = await client.sendTextMessage({
233+
body,
234+
mentionUserIds: [context.sutUserId],
235+
roomId: context.roomId,
236+
});
237+
const matched = await client.waitForRoomEvent({
225238
observedEvents: context.observedEvents,
239+
predicate: (event) =>
240+
event.roomId === context.roomId &&
241+
event.sender === context.sutUserId &&
242+
event.type === "m.room.message" &&
243+
event.relatesTo === undefined &&
244+
typeof event.body === "string" &&
245+
event.body.trim().length > 0,
226246
roomId: context.roomId,
227-
syncState: context.syncState,
228-
sutUserId: context.sutUserId,
247+
since: startSince,
229248
timeoutMs: context.timeoutMs,
230-
tokenPrefix: "MATRIX_QA_OBSERVER_ALLOWLIST",
231249
});
232-
assertTopLevelReplyArtifact("observer allowlist override reply", result.reply);
250+
advanceMatrixQaActorCursor({
251+
actorId: "observer",
252+
syncState: context.syncState,
253+
nextSince: matched.since,
254+
startSince,
255+
});
256+
const reply = buildMatrixReplyArtifact(matched.event, token);
233257
return {
234258
artifacts: {
235259
actorUserId: context.observerUserId,
236-
driverEventId: result.driverEventId,
237-
reply: result.reply,
238-
token: result.token,
239-
triggerBody: result.body,
260+
driverEventId,
261+
reply,
262+
token,
263+
triggerBody: body,
240264
},
241265
details: [
242266
`trigger sender: ${context.observerUserId}`,
243-
`driver event: ${result.driverEventId}`,
244-
...buildMatrixReplyDetails("reply", result.reply),
267+
`driver event: ${driverEventId}`,
268+
...buildMatrixReplyDetails("reply", reply),
245269
].join("\n"),
246270
} satisfies MatrixQaScenarioExecution;
247271
}
@@ -312,6 +336,7 @@ export async function runQuietStreamingPreviewScenario(context: MatrixQaScenario
312336
}
313337

314338
export async function runBlockStreamingScenario(context: MatrixQaScenarioContext) {
339+
const roomId = resolveMatrixQaScenarioRoomId(context, MATRIX_QA_BLOCK_ROOM_KEY);
315340
const { client, startSince } = await primeMatrixQaActorCursor({
316341
accessToken: context.driverAccessToken,
317342
actorId: "driver",
@@ -324,27 +349,28 @@ export async function runBlockStreamingScenario(context: MatrixQaScenarioContext
324349
const driverEventId = await client.sendTextMessage({
325350
body: triggerBody,
326351
mentionUserIds: [context.sutUserId],
327-
roomId: context.roomId,
352+
roomId,
328353
});
329354
const firstBlock = await client.waitForRoomEvent({
330355
observedEvents: context.observedEvents,
331356
predicate: (event) =>
332-
event.roomId === context.roomId &&
357+
event.roomId === roomId &&
333358
event.sender === context.sutUserId &&
334359
isMatrixQaMessageLikeKind(event.kind) &&
335-
event.body === firstText,
336-
roomId: context.roomId,
360+
(event.body ?? "").includes(firstText) &&
361+
!(event.body ?? "").includes(secondText),
362+
roomId,
337363
since: startSince,
338364
timeoutMs: context.timeoutMs,
339365
});
340366
const secondBlock = await client.waitForRoomEvent({
341367
observedEvents: context.observedEvents,
342368
predicate: (event) =>
343-
event.roomId === context.roomId &&
369+
event.roomId === roomId &&
344370
event.sender === context.sutUserId &&
345371
isMatrixQaMessageLikeKind(event.kind) &&
346-
event.body === secondText,
347-
roomId: context.roomId,
372+
(event.body ?? "").includes(secondText),
373+
roomId,
348374
since: firstBlock.since,
349375
timeoutMs: context.timeoutMs,
350376
});
@@ -364,10 +390,12 @@ export async function runBlockStreamingScenario(context: MatrixQaScenarioContext
364390
blockEventIds: [firstBlock.event.eventId, secondBlock.event.eventId],
365391
driverEventId,
366392
reply: buildMatrixReplyArtifact(secondBlock.event, secondText),
393+
roomId,
367394
token: secondText,
368395
triggerBody,
369396
},
370397
details: [
398+
`room id: ${roomId}`,
371399
`driver event: ${driverEventId}`,
372400
`block one event: ${firstBlock.event.eventId}`,
373401
`block two event: ${secondBlock.event.eventId}`,
@@ -592,12 +620,13 @@ export async function runHomeserverRestartResumeScenario(context: MatrixQaScenar
592620
if (!context.interruptTransport) {
593621
throw new Error("Matrix homeserver restart scenario requires a transport interruption hook");
594622
}
623+
const roomId = resolveMatrixQaScenarioRoomId(context, MATRIX_QA_HOMESERVER_ROOM_KEY);
595624
await context.interruptTransport();
596625
const resumed = await runDriverTopLevelMentionScenario({
597626
baseUrl: context.baseUrl,
598627
driverAccessToken: context.driverAccessToken,
599628
observedEvents: context.observedEvents,
600-
roomId: context.roomId,
629+
roomId,
601630
syncState: context.syncState,
602631
sutUserId: context.sutUserId,
603632
timeoutMs: context.timeoutMs,
@@ -608,10 +637,12 @@ export async function runHomeserverRestartResumeScenario(context: MatrixQaScenar
608637
artifacts: {
609638
driverEventId: resumed.driverEventId,
610639
reply: resumed.reply,
640+
roomId,
611641
token: resumed.token,
612642
transportInterruption: "homeserver-restart",
613643
},
614644
details: [
645+
`room id: ${roomId}`,
615646
"transport interruption: homeserver-restart",
616647
`driver event: ${resumed.driverEventId}`,
617648
...buildMatrixReplyDetails("reply", resumed.reply),
@@ -623,12 +654,13 @@ export async function runRestartResumeScenario(context: MatrixQaScenarioContext)
623654
if (!context.restartGateway) {
624655
throw new Error("Matrix restart scenario requires a gateway restart callback");
625656
}
657+
const roomId = resolveMatrixQaScenarioRoomId(context, MATRIX_QA_RESTART_ROOM_KEY);
626658
await context.restartGateway();
627659
const result = await runDriverTopLevelMentionScenario({
628660
baseUrl: context.baseUrl,
629661
driverAccessToken: context.driverAccessToken,
630662
observedEvents: context.observedEvents,
631-
roomId: context.roomId,
663+
roomId,
632664
syncState: context.syncState,
633665
sutUserId: context.sutUserId,
634666
timeoutMs: context.timeoutMs,
@@ -640,9 +672,11 @@ export async function runRestartResumeScenario(context: MatrixQaScenarioContext)
640672
driverEventId: result.driverEventId,
641673
reply: result.reply,
642674
restartSignal: "SIGUSR1",
675+
roomId,
643676
token: result.token,
644677
},
645678
details: [
679+
`room id: ${roomId}`,
646680
"restart signal: SIGUSR1",
647681
`post-restart driver event: ${result.driverEventId}`,
648682
...buildMatrixReplyDetails("reply", result.reply),

extensions/qa-matrix/src/runners/contract/scenario-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type MatrixQaScenarioArtifacts = {
2626
recoveredDriverEventId?: string;
2727
recoveredReply?: MatrixQaReplyArtifact;
2828
roomKey?: string;
29+
roomId?: string;
2930
restartSignal?: string;
3031
rootEventId?: string;
3132
threadDriverEventId?: string;

extensions/qa-matrix/src/runners/contract/scenarios.test.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,7 @@ describe("matrix live qa scenarios", () => {
336336
eventId: "$sut-reply",
337337
sender: "@sut:matrix-qa.test",
338338
type: "m.room.message",
339-
body: String(sendTextMessage.mock.calls[0]?.[0]?.body).replace(
340-
"@sut:matrix-qa.test reply with only this exact marker: ",
341-
"",
342-
),
339+
body: "observer sender accepted",
343340
},
344341
since: "observer-sync-next",
345342
}));
@@ -380,6 +377,9 @@ describe("matrix live qa scenarios", () => {
380377
artifacts: {
381378
actorUserId: "@observer:matrix-qa.test",
382379
driverEventId: "$observer-allow-trigger",
380+
reply: {
381+
tokenMatched: false,
382+
},
383383
},
384384
});
385385

@@ -707,7 +707,21 @@ describe("matrix live qa scenarios", () => {
707707
topology: {
708708
defaultRoomId: "!main:matrix-qa.test",
709709
defaultRoomKey: "main",
710-
rooms: [],
710+
rooms: [
711+
{
712+
key: "block",
713+
kind: "group",
714+
memberRoles: ["driver", "observer", "sut"],
715+
memberUserIds: [
716+
"@driver:matrix-qa.test",
717+
"@observer:matrix-qa.test",
718+
"@sut:matrix-qa.test",
719+
],
720+
name: "Block",
721+
requireMention: true,
722+
roomId: "!block:matrix-qa.test",
723+
},
724+
],
711725
},
712726
}),
713727
).resolves.toMatchObject({
@@ -720,7 +734,7 @@ describe("matrix live qa scenarios", () => {
720734
expect(sendTextMessage).toHaveBeenCalledWith({
721735
body: expect.stringContaining("Matrix block streaming QA check"),
722736
mentionUserIds: ["@sut:matrix-qa.test"],
723-
roomId: "!main:matrix-qa.test",
737+
roomId: "!block:matrix-qa.test",
724738
});
725739
expect(waitForRoomEvent).toHaveBeenNthCalledWith(
726740
2,

0 commit comments

Comments
 (0)