diff --git a/CHANGELOG.md b/CHANGELOG.md index 68fe5413541..3815eb18821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -436,6 +436,7 @@ Docs: https://docs.openclaw.ai - Google/models: resolve Gemini 3.1 pro, flash, and flash-lite for all Google provider aliases by passing the actual runtime provider ID and adding a template-provider fallback; fix flash-lite prefix ordering. (#56567) - OpenAI Codex/image tools: register Codex for media understanding and route image prompts through Codex instructions so image analysis no longer fails on missing provider registration or missing `instructions`. (#54829) Thanks @neeravmakwana. - Agents/image tool: restore the generic image-runtime fallback when no provider-specific media-understanding provider is registered, so image analysis works again for providers like `openrouter` and `minimax-portal`. (#54858) Thanks @MonkeyLeeT. +- Matrix/multi-account: keep room-level `account` scoping and implicit default-account selection consistent when the default Matrix account is configured directly on top-level `channels.matrix.*` alongside named accounts. (#58449) thanks @Daanvdplas. - WhatsApp: fix infinite echo loop in self-chat DM mode where the bot's own outbound replies were re-processed as new inbound user messages. (#54570) Thanks @joelnishanth - Telegram/splitting: replace proportional text estimate with verified HTML-length search so long messages split at word boundaries instead of mid-word; gracefully degrade when tag overhead exceeds the limit. (#56595) - Telegram/delivery: skip whitespace-only and hook-blanked text replies in bot delivery to prevent GrammyError 400 empty-text crashes. (#56620) diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index badc4d2ce49..fd5f99f4945 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -656,6 +656,8 @@ See [Pairing](/channels/pairing) for the shared DM pairing flow and storage layo ``` Top-level `channels.matrix` values act as defaults for named accounts unless an account overrides them. +You can scope inherited room entries to one Matrix account with `groups..account` (or legacy `rooms..account`). +Entries without `account` stay shared across all Matrix accounts, and entries with `account: "default"` still work when the default account is configured directly on top-level `channels.matrix.*`. Set `defaultAccount` when you want OpenClaw to prefer one named Matrix account for implicit routing, probing, and CLI operations. If you configure multiple named accounts, set `defaultAccount` or pass `--account ` for CLI commands that rely on implicit account selection. Pass `--account ` to `openclaw matrix verify ...` and `openclaw matrix devices ...` when you want to override that implicit selection for one command. diff --git a/extensions/matrix/src/account-selection.ts b/extensions/matrix/src/account-selection.ts index 33b1f5ac69e..f1bea6fcf45 100644 --- a/extensions/matrix/src/account-selection.ts +++ b/extensions/matrix/src/account-selection.ts @@ -11,6 +11,8 @@ import { } from "openclaw/plugin-sdk/account-id"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { listMatrixEnvAccountIds } from "./env-vars.js"; +import { hasExplicitMatrixAccountConfig } from "./matrix/account-config.js"; +import type { CoreConfig } from "./types.js"; function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); @@ -42,11 +44,15 @@ export function resolveConfiguredMatrixAccountIds( env: NodeJS.ProcessEnv = process.env, ): string[] { const channel = resolveMatrixChannelConfig(cfg); + const configuredAccountIds = listConfiguredAccountIds({ + accounts: channel && isRecord(channel.accounts) ? channel.accounts : undefined, + normalizeAccountId, + }); + if (channel && hasExplicitMatrixAccountConfig(cfg as CoreConfig, DEFAULT_ACCOUNT_ID)) { + configuredAccountIds.push(DEFAULT_ACCOUNT_ID); + } return listCombinedAccountIds({ - configuredAccountIds: listConfiguredAccountIds({ - accounts: channel && isRecord(channel.accounts) ? channel.accounts : undefined, - normalizeAccountId, - }), + configuredAccountIds, additionalAccountIds: listMatrixEnvAccountIds(env), fallbackAccountIdWhenEmpty: channel ? DEFAULT_ACCOUNT_ID : undefined, }); diff --git a/extensions/matrix/src/matrix/accounts.test.ts b/extensions/matrix/src/matrix/accounts.test.ts index 858dc0cbf7c..c2a35c6f591 100644 --- a/extensions/matrix/src/matrix/accounts.test.ts +++ b/extensions/matrix/src/matrix/accounts.test.ts @@ -260,6 +260,26 @@ describe("resolveMatrixAccount", () => { expect(resolveDefaultMatrixAccountId(cfg)).toBe("default"); }); + it("includes a top-level configured default account alongside named accounts", () => { + const cfg: CoreConfig = { + channels: { + matrix: { + homeserver: "https://matrix.example.org", + accessToken: "default-token", + accounts: { + ops: { + homeserver: "https://matrix.example.org", + accessToken: "ops-token", + }, + }, + }, + }, + }; + + expect(listMatrixAccountIds(cfg)).toEqual(["default", "ops"]); + expect(resolveDefaultMatrixAccountId(cfg)).toBe("default"); + }); + it('uses the synthetic "default" account when multiple named accounts need explicit selection', () => { const cfg: CoreConfig = { channels: { @@ -458,6 +478,55 @@ describe("resolveMatrixAccount", () => { }); }); + it("filters channel-level groups when the default account is configured at the top level", () => { + const cfg = { + channels: { + matrix: { + homeserver: "https://matrix.example.org", + accessToken: "default-token", + groups: { + "!default-room:example.org": { + allow: true, + account: "default", + }, + "!ops-room:example.org": { + allow: true, + account: "ops", + }, + "!shared-room:example.org": { + allow: true, + }, + }, + accounts: { + ops: { + homeserver: "https://matrix.example.org", + accessToken: "ops-token", + }, + }, + }, + }, + } as unknown as CoreConfig; + + expect(resolveMatrixAccount({ cfg, accountId: "default" }).config.groups).toEqual({ + "!default-room:example.org": { + allow: true, + account: "default", + }, + "!shared-room:example.org": { + allow: true, + }, + }); + expect(resolveMatrixAccount({ cfg, accountId: "ops" }).config.groups).toEqual({ + "!ops-room:example.org": { + allow: true, + account: "ops", + }, + "!shared-room:example.org": { + allow: true, + }, + }); + }); + it("filters legacy channel-level rooms by room account in multi-account setups", () => { const cfg = { channels: { @@ -509,6 +578,55 @@ describe("resolveMatrixAccount", () => { }); }); + it("filters legacy channel-level rooms when the default account is configured at the top level", () => { + const cfg = { + channels: { + matrix: { + homeserver: "https://matrix.example.org", + accessToken: "default-token", + rooms: { + "!default-room:example.org": { + allow: true, + account: "default", + }, + "!ops-room:example.org": { + allow: true, + account: "ops", + }, + "!shared-room:example.org": { + allow: true, + }, + }, + accounts: { + ops: { + homeserver: "https://matrix.example.org", + accessToken: "ops-token", + }, + }, + }, + }, + } as unknown as CoreConfig; + + expect(resolveMatrixAccount({ cfg, accountId: "default" }).config.rooms).toEqual({ + "!default-room:example.org": { + allow: true, + account: "default", + }, + "!shared-room:example.org": { + allow: true, + }, + }); + expect(resolveMatrixAccount({ cfg, accountId: "ops" }).config.rooms).toEqual({ + "!ops-room:example.org": { + allow: true, + account: "ops", + }, + "!shared-room:example.org": { + allow: true, + }, + }); + }); + it("honors injected env when scoping room entries in multi-account setups", () => { const env = { MATRIX_HOMESERVER: "https://matrix.example.org",