@openclaw/matrix 2026.2.15 → 2026.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.2.17
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
9
+ ## 2026.2.16
10
+
11
+ ### Changes
12
+
13
+ - Version alignment with core OpenClaw release numbers.
14
+
3
15
  ## 2026.2.15
4
16
 
5
17
  ### Changes
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@openclaw/matrix",
3
- "version": "2026.2.15",
3
+ "version": "2026.2.17",
4
4
  "description": "OpenClaw Matrix channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
8
8
  "@vector-im/matrix-bot-sdk": "0.8.0-element.3",
9
9
  "markdown-it": "14.1.1",
10
- "music-metadata": "^11.12.0",
10
+ "music-metadata": "^11.12.1",
11
11
  "zod": "^4.3.6"
12
12
  },
13
13
  "devDependencies": {
package/src/actions.ts CHANGED
@@ -7,9 +7,9 @@ import {
7
7
  type ChannelMessageActionName,
8
8
  type ChannelToolSend,
9
9
  } from "openclaw/plugin-sdk";
10
- import type { CoreConfig } from "./types.js";
11
10
  import { resolveMatrixAccount } from "./matrix/accounts.js";
12
11
  import { handleMatrixAction } from "./tool-actions.js";
12
+ import type { CoreConfig } from "./types.js";
13
13
 
14
14
  export const matrixMessageActions: ChannelMessageActionAdapter = {
15
15
  listActions: ({ cfg }) => {
@@ -1,8 +1,8 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
1
+ import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
- import type { CoreConfig } from "./types.js";
4
3
  import { matrixPlugin } from "./channel.js";
5
4
  import { setMatrixRuntime } from "./runtime.js";
5
+ import type { CoreConfig } from "./types.js";
6
6
 
7
7
  vi.mock("@vector-im/matrix-bot-sdk", () => ({
8
8
  ConsoleLogger: class {
@@ -24,10 +24,18 @@ vi.mock("@vector-im/matrix-bot-sdk", () => ({
24
24
  }));
25
25
 
26
26
  describe("matrix directory", () => {
27
+ const runtimeEnv: RuntimeEnv = {
28
+ log: vi.fn(),
29
+ error: vi.fn(),
30
+ exit: vi.fn((code: number): never => {
31
+ throw new Error(`exit ${code}`);
32
+ }),
33
+ };
34
+
27
35
  beforeEach(() => {
28
36
  setMatrixRuntime({
29
37
  state: {
30
- resolveStateDir: (_env, homeDir) => homeDir(),
38
+ resolveStateDir: (_env, homeDir) => (homeDir ?? (() => "/tmp"))(),
31
39
  },
32
40
  } as PluginRuntime);
33
41
  });
@@ -51,11 +59,12 @@ describe("matrix directory", () => {
51
59
  expect(matrixPlugin.directory?.listGroups).toBeTruthy();
52
60
 
53
61
  await expect(
54
- matrixPlugin.directory!.listPeers({
62
+ matrixPlugin.directory!.listPeers!({
55
63
  cfg,
56
64
  accountId: undefined,
57
65
  query: undefined,
58
66
  limit: undefined,
67
+ runtime: runtimeEnv,
59
68
  }),
60
69
  ).resolves.toEqual(
61
70
  expect.arrayContaining([
@@ -67,11 +76,12 @@ describe("matrix directory", () => {
67
76
  );
68
77
 
69
78
  await expect(
70
- matrixPlugin.directory!.listGroups({
79
+ matrixPlugin.directory!.listGroups!({
71
80
  cfg,
72
81
  accountId: undefined,
73
82
  query: undefined,
74
83
  limit: undefined,
84
+ runtime: runtimeEnv,
75
85
  }),
76
86
  ).resolves.toEqual(
77
87
  expect.arrayContaining([
@@ -130,11 +140,11 @@ describe("matrix directory", () => {
130
140
  },
131
141
  } as unknown as CoreConfig;
132
142
 
133
- expect(matrixPlugin.groups.resolveRequireMention({ cfg, groupId: "!room:example.org" })).toBe(
143
+ expect(matrixPlugin.groups!.resolveRequireMention!({ cfg, groupId: "!room:example.org" })).toBe(
134
144
  true,
135
145
  );
136
146
  expect(
137
- matrixPlugin.groups.resolveRequireMention({
147
+ matrixPlugin.groups!.resolveRequireMention!({
138
148
  cfg,
139
149
  accountId: "assistant",
140
150
  groupId: "!room:example.org",
package/src/channel.ts CHANGED
@@ -9,7 +9,6 @@ import {
9
9
  setAccountEnabledInConfigSection,
10
10
  type ChannelPlugin,
11
11
  } from "openclaw/plugin-sdk";
12
- import type { CoreConfig } from "./types.js";
13
12
  import { matrixMessageActions } from "./actions.js";
14
13
  import { MatrixConfigSchema } from "./config-schema.js";
15
14
  import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js";
@@ -31,6 +30,7 @@ import { sendMessageMatrix } from "./matrix/send.js";
31
30
  import { matrixOnboardingAdapter } from "./onboarding.js";
32
31
  import { matrixOutbound } from "./outbound.js";
33
32
  import { resolveMatrixTargets } from "./resolve-targets.js";
33
+ import type { CoreConfig } from "./types.js";
34
34
 
35
35
  // Mutex for serializing account startup (workaround for concurrent dynamic import race condition)
36
36
  let matrixStartupLock: Promise<void> = Promise.resolve();
@@ -1,7 +1,7 @@
1
1
  import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
2
- import type { CoreConfig } from "./types.js";
3
2
  import { resolveMatrixAccountConfig } from "./matrix/accounts.js";
4
3
  import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
4
+ import type { CoreConfig } from "./types.js";
5
5
 
6
6
  function stripLeadingPrefixCaseInsensitive(value: string, prefix: string): string {
7
7
  return value.toLowerCase().startsWith(prefix.toLowerCase())
@@ -1,14 +1,10 @@
1
1
  import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
- import type { CoreConfig } from "../../types.js";
3
- import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js";
4
2
  import { getMatrixRuntime } from "../../runtime.js";
3
+ import type { CoreConfig } from "../../types.js";
5
4
  import { getActiveMatrixClient } from "../active-client.js";
6
- import {
7
- createMatrixClient,
8
- isBunRuntime,
9
- resolveMatrixAuth,
10
- resolveSharedMatrixClient,
11
- } from "../client.js";
5
+ import { createPreparedMatrixClient } from "../client-bootstrap.js";
6
+ import { isBunRuntime, resolveMatrixAuth, resolveSharedMatrixClient } from "../client.js";
7
+ import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js";
12
8
 
13
9
  export function ensureNodeRuntime() {
14
10
  if (isBunRuntime()) {
@@ -42,24 +38,10 @@ export async function resolveActionClient(
42
38
  cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
43
39
  accountId,
44
40
  });
45
- const client = await createMatrixClient({
46
- homeserver: auth.homeserver,
47
- userId: auth.userId,
48
- accessToken: auth.accessToken,
49
- encryption: auth.encryption,
50
- localTimeoutMs: opts.timeoutMs,
41
+ const client = await createPreparedMatrixClient({
42
+ auth,
43
+ timeoutMs: opts.timeoutMs,
51
44
  accountId,
52
45
  });
53
- if (auth.encryption && client.crypto) {
54
- try {
55
- const joinedRooms = await client.getJoinedRooms();
56
- await (client.crypto as { prepare: (rooms?: string[]) => Promise<void> }).prepare(
57
- joinedRooms,
58
- );
59
- } catch {
60
- // Ignore crypto prep failures for one-off actions.
61
- }
62
- }
63
- await client.start();
64
46
  return { client, stopOnDone: true };
65
47
  }
@@ -1,9 +1,9 @@
1
1
  import { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
2
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
- import type { CoreConfig } from "../../types.js";
4
- import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
5
3
  import { getMatrixRuntime } from "../../runtime.js";
4
+ import type { CoreConfig } from "../../types.js";
6
5
  import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
6
+ import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
7
7
 
8
8
  function clean(value?: string): string {
9
9
  return value?.trim() ?? "";
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import type { IStorageProvider, ICryptoStorageProvider } from "@vector-im/matrix-bot-sdk";
2
3
  import {
3
4
  LogService,
@@ -5,7 +6,6 @@ import {
5
6
  SimpleFsStorageProvider,
6
7
  RustSdkCryptoStorageProvider,
7
8
  } from "@vector-im/matrix-bot-sdk";
8
- import fs from "node:fs";
9
9
  import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
10
10
  import {
11
11
  maybeMigrateLegacyStorage,
@@ -2,10 +2,10 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
2
  import { LogService } from "@vector-im/matrix-bot-sdk";
3
3
  import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
4
4
  import type { CoreConfig } from "../../types.js";
5
- import type { MatrixAuth } from "./types.js";
6
5
  import { resolveMatrixAuth } from "./config.js";
7
6
  import { createMatrixClient } from "./create-client.js";
8
7
  import { DEFAULT_ACCOUNT_KEY } from "./storage.js";
8
+ import type { MatrixAuth } from "./types.js";
9
9
 
10
10
  type SharedMatrixClientState = {
11
11
  client: MatrixClient;
@@ -2,8 +2,8 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import type { MatrixStoragePaths } from "./types.js";
6
5
  import { getMatrixRuntime } from "../../runtime.js";
6
+ import type { MatrixStoragePaths } from "./types.js";
7
7
 
8
8
  export const DEFAULT_ACCOUNT_KEY = "default";
9
9
  const STORAGE_META_FILENAME = "storage-meta.json";
@@ -0,0 +1,39 @@
1
+ import { createMatrixClient } from "./client.js";
2
+
3
+ type MatrixClientBootstrapAuth = {
4
+ homeserver: string;
5
+ userId: string;
6
+ accessToken: string;
7
+ encryption?: boolean;
8
+ };
9
+
10
+ type MatrixCryptoPrepare = {
11
+ prepare: (rooms?: string[]) => Promise<void>;
12
+ };
13
+
14
+ type MatrixBootstrapClient = Awaited<ReturnType<typeof createMatrixClient>>;
15
+
16
+ export async function createPreparedMatrixClient(opts: {
17
+ auth: MatrixClientBootstrapAuth;
18
+ timeoutMs?: number;
19
+ accountId?: string;
20
+ }): Promise<MatrixBootstrapClient> {
21
+ const client = await createMatrixClient({
22
+ homeserver: opts.auth.homeserver,
23
+ userId: opts.auth.userId,
24
+ accessToken: opts.auth.accessToken,
25
+ encryption: opts.auth.encryption,
26
+ localTimeoutMs: opts.timeoutMs,
27
+ accountId: opts.accountId,
28
+ });
29
+ if (opts.auth.encryption && client.crypto) {
30
+ try {
31
+ const joinedRooms = await client.getJoinedRooms();
32
+ await (client.crypto as MatrixCryptoPrepare).prepare(joinedRooms);
33
+ } catch {
34
+ // Ignore crypto prep failures for one-off requests.
35
+ }
36
+ }
37
+ await client.start();
38
+ return client;
39
+ }
@@ -1,8 +1,8 @@
1
- import type { RuntimeEnv } from "openclaw/plugin-sdk";
2
1
  import fs from "node:fs";
3
2
  import { createRequire } from "node:module";
4
3
  import path from "node:path";
5
4
  import { fileURLToPath } from "node:url";
5
+ import type { RuntimeEnv } from "openclaw/plugin-sdk";
6
6
  import { getMatrixRuntime } from "../runtime.js";
7
7
 
8
8
  const MATRIX_SDK_PACKAGE = "@vector-im/matrix-bot-sdk";
@@ -1,8 +1,8 @@
1
1
  import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
- import type { RuntimeEnv } from "openclaw/plugin-sdk";
3
2
  import { AutojoinRoomsMixin } from "@vector-im/matrix-bot-sdk";
4
- import type { CoreConfig } from "../../types.js";
3
+ import type { RuntimeEnv } from "openclaw/plugin-sdk";
5
4
  import { getMatrixRuntime } from "../../runtime.js";
5
+ import type { CoreConfig } from "../../types.js";
6
6
 
7
7
  export function registerMatrixAutoJoin(params: {
8
8
  client: MatrixClient;
@@ -11,7 +11,6 @@ import {
11
11
  type RuntimeLogger,
12
12
  } from "openclaw/plugin-sdk";
13
13
  import type { CoreConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js";
14
- import type { MatrixRawEvent, RoomMessageEventContent } from "./types.js";
15
14
  import { fetchEventSummary } from "../actions/summary.js";
16
15
  import {
17
16
  formatPollAsText,
@@ -36,6 +35,7 @@ import { resolveMentions } from "./mentions.js";
36
35
  import { deliverMatrixReplies } from "./replies.js";
37
36
  import { resolveMatrixRoomConfig } from "./rooms.js";
38
37
  import { resolveMatrixThreadRootId, resolveMatrixThreadTarget } from "./threads.js";
38
+ import type { MatrixRawEvent, RoomMessageEventContent } from "./types.js";
39
39
  import { EventType, RelationType } from "./types.js";
40
40
 
41
41
  export type MatrixMonitorHandlerParams = {
@@ -1,8 +1,8 @@
1
1
  import { format } from "node:util";
2
2
  import { mergeAllowlist, summarizeMapping, type RuntimeEnv } from "openclaw/plugin-sdk";
3
- import type { CoreConfig, ReplyToMode } from "../../types.js";
4
3
  import { resolveMatrixTargets } from "../../resolve-targets.js";
5
4
  import { getMatrixRuntime } from "../../runtime.js";
5
+ import type { CoreConfig, ReplyToMode } from "../../types.js";
6
6
  import { resolveMatrixAccount } from "../accounts.js";
7
7
  import { setActiveMatrixClient } from "../active-client.js";
8
8
  import {
@@ -22,14 +22,12 @@ describe("downloadMatrixMedia", () => {
22
22
  setMatrixRuntime(runtimeStub);
23
23
  });
24
24
 
25
- it("decrypts encrypted media when file payloads are present", async () => {
25
+ function makeEncryptedMediaFixture() {
26
26
  const decryptMedia = vi.fn().mockResolvedValue(Buffer.from("decrypted"));
27
-
28
27
  const client = {
29
28
  crypto: { decryptMedia },
30
29
  mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"),
31
30
  } as unknown as import("@vector-im/matrix-bot-sdk").MatrixClient;
32
-
33
31
  const file = {
34
32
  url: "mxc://example/file",
35
33
  key: {
@@ -43,6 +41,11 @@ describe("downloadMatrixMedia", () => {
43
41
  hashes: { sha256: "hash" },
44
42
  v: "v2",
45
43
  };
44
+ return { decryptMedia, client, file };
45
+ }
46
+
47
+ it("decrypts encrypted media when file payloads are present", async () => {
48
+ const { decryptMedia, client, file } = makeEncryptedMediaFixture();
46
49
 
47
50
  const result = await downloadMatrixMedia({
48
51
  client,
@@ -64,26 +67,7 @@ describe("downloadMatrixMedia", () => {
64
67
  });
65
68
 
66
69
  it("rejects encrypted media that exceeds maxBytes before decrypting", async () => {
67
- const decryptMedia = vi.fn().mockResolvedValue(Buffer.from("decrypted"));
68
-
69
- const client = {
70
- crypto: { decryptMedia },
71
- mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"),
72
- } as unknown as import("@vector-im/matrix-bot-sdk").MatrixClient;
73
-
74
- const file = {
75
- url: "mxc://example/file",
76
- key: {
77
- kty: "oct",
78
- key_ops: ["encrypt", "decrypt"],
79
- alg: "A256CTR",
80
- k: "secret",
81
- ext: true,
82
- },
83
- iv: "iv",
84
- hashes: { sha256: "hash" },
85
- v: "v2",
86
- };
70
+ const { decryptMedia, client, file } = makeEncryptedMediaFixture();
87
71
 
88
72
  await expect(
89
73
  downloadMatrixMedia({
@@ -0,0 +1,132 @@
1
+ import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
+ import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ const sendMessageMatrixMock = vi.hoisted(() => vi.fn().mockResolvedValue({ messageId: "mx-1" }));
6
+
7
+ vi.mock("../send.js", () => ({
8
+ sendMessageMatrix: (to: string, message: string, opts?: unknown) =>
9
+ sendMessageMatrixMock(to, message, opts),
10
+ }));
11
+
12
+ import { setMatrixRuntime } from "../../runtime.js";
13
+ import { deliverMatrixReplies } from "./replies.js";
14
+
15
+ describe("deliverMatrixReplies", () => {
16
+ const loadConfigMock = vi.fn(() => ({}));
17
+ const resolveMarkdownTableModeMock = vi.fn(() => "code");
18
+ const convertMarkdownTablesMock = vi.fn((text: string) => text);
19
+ const resolveChunkModeMock = vi.fn(() => "length");
20
+ const chunkMarkdownTextWithModeMock = vi.fn((text: string) => [text]);
21
+
22
+ const runtimeStub = {
23
+ config: {
24
+ loadConfig: () => loadConfigMock(),
25
+ },
26
+ channel: {
27
+ text: {
28
+ resolveMarkdownTableMode: () => resolveMarkdownTableModeMock(),
29
+ convertMarkdownTables: (text: string) => convertMarkdownTablesMock(text),
30
+ resolveChunkMode: () => resolveChunkModeMock(),
31
+ chunkMarkdownTextWithMode: (text: string) => chunkMarkdownTextWithModeMock(text),
32
+ },
33
+ },
34
+ logging: {
35
+ shouldLogVerbose: () => false,
36
+ },
37
+ } as unknown as PluginRuntime;
38
+
39
+ const runtimeEnv: RuntimeEnv = {
40
+ log: vi.fn(),
41
+ error: vi.fn(),
42
+ } as unknown as RuntimeEnv;
43
+
44
+ beforeEach(() => {
45
+ vi.clearAllMocks();
46
+ setMatrixRuntime(runtimeStub);
47
+ chunkMarkdownTextWithModeMock.mockImplementation((text: string) => [text]);
48
+ });
49
+
50
+ it("keeps replyToId on first reply only when replyToMode=first", async () => {
51
+ chunkMarkdownTextWithModeMock.mockImplementation((text: string) => text.split("|"));
52
+
53
+ await deliverMatrixReplies({
54
+ replies: [
55
+ { text: "first-a|first-b", replyToId: "reply-1" },
56
+ { text: "second", replyToId: "reply-2" },
57
+ ],
58
+ roomId: "room:1",
59
+ client: {} as MatrixClient,
60
+ runtime: runtimeEnv,
61
+ textLimit: 4000,
62
+ replyToMode: "first",
63
+ });
64
+
65
+ expect(sendMessageMatrixMock).toHaveBeenCalledTimes(3);
66
+ expect(sendMessageMatrixMock.mock.calls[0]?.[2]).toEqual(
67
+ expect.objectContaining({ replyToId: "reply-1", threadId: undefined }),
68
+ );
69
+ expect(sendMessageMatrixMock.mock.calls[1]?.[2]).toEqual(
70
+ expect.objectContaining({ replyToId: "reply-1", threadId: undefined }),
71
+ );
72
+ expect(sendMessageMatrixMock.mock.calls[2]?.[2]).toEqual(
73
+ expect.objectContaining({ replyToId: undefined, threadId: undefined }),
74
+ );
75
+ });
76
+
77
+ it("keeps replyToId on every reply when replyToMode=all", async () => {
78
+ await deliverMatrixReplies({
79
+ replies: [
80
+ {
81
+ text: "caption",
82
+ mediaUrls: ["https://example.com/a.jpg", "https://example.com/b.jpg"],
83
+ replyToId: "reply-media",
84
+ audioAsVoice: true,
85
+ },
86
+ { text: "plain", replyToId: "reply-text" },
87
+ ],
88
+ roomId: "room:2",
89
+ client: {} as MatrixClient,
90
+ runtime: runtimeEnv,
91
+ textLimit: 4000,
92
+ replyToMode: "all",
93
+ });
94
+
95
+ expect(sendMessageMatrixMock).toHaveBeenCalledTimes(3);
96
+ expect(sendMessageMatrixMock.mock.calls[0]).toEqual([
97
+ "room:2",
98
+ "caption",
99
+ expect.objectContaining({ mediaUrl: "https://example.com/a.jpg", replyToId: "reply-media" }),
100
+ ]);
101
+ expect(sendMessageMatrixMock.mock.calls[1]).toEqual([
102
+ "room:2",
103
+ "",
104
+ expect.objectContaining({ mediaUrl: "https://example.com/b.jpg", replyToId: "reply-media" }),
105
+ ]);
106
+ expect(sendMessageMatrixMock.mock.calls[2]?.[2]).toEqual(
107
+ expect.objectContaining({ replyToId: "reply-text" }),
108
+ );
109
+ });
110
+
111
+ it("suppresses replyToId when threadId is set", async () => {
112
+ chunkMarkdownTextWithModeMock.mockImplementation((text: string) => text.split("|"));
113
+
114
+ await deliverMatrixReplies({
115
+ replies: [{ text: "hello|thread", replyToId: "reply-thread" }],
116
+ roomId: "room:3",
117
+ client: {} as MatrixClient,
118
+ runtime: runtimeEnv,
119
+ textLimit: 4000,
120
+ replyToMode: "all",
121
+ threadId: "thread-77",
122
+ });
123
+
124
+ expect(sendMessageMatrixMock).toHaveBeenCalledTimes(2);
125
+ expect(sendMessageMatrixMock.mock.calls[0]?.[2]).toEqual(
126
+ expect.objectContaining({ replyToId: undefined, threadId: "thread-77" }),
127
+ );
128
+ expect(sendMessageMatrixMock.mock.calls[1]?.[2]).toEqual(
129
+ expect.objectContaining({ replyToId: undefined, threadId: "thread-77" }),
130
+ );
131
+ });
132
+ });
@@ -53,8 +53,10 @@ export async function deliverMatrixReplies(params: {
53
53
 
54
54
  const shouldIncludeReply = (id?: string) =>
55
55
  Boolean(id) && (params.replyToMode === "all" || !hasReplied);
56
+ const replyToIdForReply = shouldIncludeReply(replyToId) ? replyToId : undefined;
56
57
 
57
58
  if (mediaList.length === 0) {
59
+ let sentTextChunk = false;
58
60
  for (const chunk of core.channel.text.chunkMarkdownTextWithMode(
59
61
  text,
60
62
  chunkLimit,
@@ -66,13 +68,14 @@ export async function deliverMatrixReplies(params: {
66
68
  }
67
69
  await sendMessageMatrix(params.roomId, trimmed, {
68
70
  client: params.client,
69
- replyToId: shouldIncludeReply(replyToId) ? replyToId : undefined,
71
+ replyToId: replyToIdForReply,
70
72
  threadId: params.threadId,
71
73
  accountId: params.accountId,
72
74
  });
73
- if (shouldIncludeReply(replyToId)) {
74
- hasReplied = true;
75
- }
75
+ sentTextChunk = true;
76
+ }
77
+ if (replyToIdForReply && !hasReplied && sentTextChunk) {
78
+ hasReplied = true;
76
79
  }
77
80
  continue;
78
81
  }
@@ -83,15 +86,15 @@ export async function deliverMatrixReplies(params: {
83
86
  await sendMessageMatrix(params.roomId, caption, {
84
87
  client: params.client,
85
88
  mediaUrl,
86
- replyToId: shouldIncludeReply(replyToId) ? replyToId : undefined,
89
+ replyToId: replyToIdForReply,
87
90
  threadId: params.threadId,
88
91
  audioAsVoice: reply.audioAsVoice,
89
92
  accountId: params.accountId,
90
93
  });
91
- if (shouldIncludeReply(replyToId)) {
92
- hasReplied = true;
93
- }
94
94
  first = false;
95
95
  }
96
+ if (replyToIdForReply && !hasReplied) {
97
+ hasReplied = true;
98
+ }
96
99
  }
97
100
  }
@@ -1,14 +1,10 @@
1
1
  import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
2
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
- import type { CoreConfig } from "../../types.js";
4
3
  import { getMatrixRuntime } from "../../runtime.js";
4
+ import type { CoreConfig } from "../../types.js";
5
5
  import { getActiveMatrixClient, getAnyActiveMatrixClient } from "../active-client.js";
6
- import {
7
- createMatrixClient,
8
- isBunRuntime,
9
- resolveMatrixAuth,
10
- resolveSharedMatrixClient,
11
- } from "../client.js";
6
+ import { createPreparedMatrixClient } from "../client-bootstrap.js";
7
+ import { isBunRuntime, resolveMatrixAuth, resolveSharedMatrixClient } from "../client.js";
12
8
 
13
9
  const getCore = () => getMatrixRuntime();
14
10
 
@@ -92,25 +88,10 @@ export async function resolveMatrixClient(opts: {
92
88
  return { client, stopOnDone: false };
93
89
  }
94
90
  const auth = await resolveMatrixAuth({ accountId });
95
- const client = await createMatrixClient({
96
- homeserver: auth.homeserver,
97
- userId: auth.userId,
98
- accessToken: auth.accessToken,
99
- encryption: auth.encryption,
100
- localTimeoutMs: opts.timeoutMs,
91
+ const client = await createPreparedMatrixClient({
92
+ auth,
93
+ timeoutMs: opts.timeoutMs,
101
94
  accountId,
102
95
  });
103
- if (auth.encryption && client.crypto) {
104
- try {
105
- const joinedRooms = await client.getJoinedRooms();
106
- await (client.crypto as { prepare: (rooms?: string[]) => Promise<void> }).prepare(
107
- joinedRooms,
108
- );
109
- } catch {
110
- // Ignore crypto prep failures for one-off sends; normal sync will retry.
111
- }
112
- }
113
- // @vector-im/matrix-bot-sdk uses start() instead of startClient()
114
- await client.start();
115
96
  return { client, stopOnDone: true };
116
97
  }
@@ -40,11 +40,13 @@ const runtimeStub = {
40
40
  loadConfig: () => ({}),
41
41
  },
42
42
  media: {
43
- loadWebMedia: (...args: unknown[]) => loadWebMediaMock(...args),
44
- mediaKindFromMime: (...args: unknown[]) => mediaKindFromMimeMock(...args),
45
- isVoiceCompatibleAudio: (...args: unknown[]) => isVoiceCompatibleAudioMock(...args),
46
- getImageMetadata: (...args: unknown[]) => getImageMetadataMock(...args),
47
- resizeToJpeg: (...args: unknown[]) => resizeToJpegMock(...args),
43
+ loadWebMedia: loadWebMediaMock as unknown as PluginRuntime["media"]["loadWebMedia"],
44
+ mediaKindFromMime:
45
+ mediaKindFromMimeMock as unknown as PluginRuntime["media"]["mediaKindFromMime"],
46
+ isVoiceCompatibleAudio:
47
+ isVoiceCompatibleAudioMock as unknown as PluginRuntime["media"]["isVoiceCompatibleAudio"],
48
+ getImageMetadata: getImageMetadataMock as unknown as PluginRuntime["media"]["getImageMetadata"],
49
+ resizeToJpeg: resizeToJpegMock as unknown as PluginRuntime["media"]["resizeToJpeg"],
48
50
  },
49
51
  channel: {
50
52
  text: {
package/src/onboarding.ts CHANGED
@@ -2,16 +2,17 @@ import type { DmPolicy } from "openclaw/plugin-sdk";
2
2
  import {
3
3
  addWildcardAllowFrom,
4
4
  formatDocsLink,
5
+ mergeAllowFromEntries,
5
6
  promptChannelAccessConfig,
6
7
  type ChannelOnboardingAdapter,
7
8
  type ChannelOnboardingDmPolicy,
8
9
  type WizardPrompter,
9
10
  } from "openclaw/plugin-sdk";
10
- import type { CoreConfig } from "./types.js";
11
11
  import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
12
12
  import { resolveMatrixAccount } from "./matrix/accounts.js";
13
13
  import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
14
14
  import { resolveMatrixTargets } from "./resolve-targets.js";
15
+ import type { CoreConfig } from "./types.js";
15
16
 
16
17
  const channel = "matrix" as const;
17
18
 
@@ -118,12 +119,7 @@ async function promptMatrixAllowFrom(params: {
118
119
  continue;
119
120
  }
120
121
 
121
- const unique = [
122
- ...new Set([
123
- ...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
124
- ...resolvedIds,
125
- ]),
126
- ];
122
+ const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds);
127
123
  return {
128
124
  ...cfg,
129
125
  channels: {
@@ -6,7 +6,6 @@ import {
6
6
  readReactionParams,
7
7
  readStringParam,
8
8
  } from "openclaw/plugin-sdk";
9
- import type { CoreConfig } from "./types.js";
10
9
  import {
11
10
  deleteMatrixMessage,
12
11
  editMatrixMessage,
@@ -21,6 +20,7 @@ import {
21
20
  unpinMatrixMessage,
22
21
  } from "./matrix/actions.js";
23
22
  import { reactMatrixMessage } from "./matrix/send.js";
23
+ import type { CoreConfig } from "./types.js";
24
24
 
25
25
  const messageActions = new Set(["sendMessage", "editMessage", "deleteMessage", "readMessages"]);
26
26
  const reactionActions = new Set(["react", "reactions"]);