@openclaw/matrix 2026.3.12 → 2026.3.13

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.3.13
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
3
9
  ## 2026.3.12
4
10
 
5
11
  ### Changes
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@openclaw/matrix",
3
- "version": "2026.3.12",
3
+ "version": "2026.3.13",
4
4
  "description": "OpenClaw Matrix channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
7
- "@mariozechner/pi-agent-core": "0.57.1",
7
+ "@mariozechner/pi-agent-core": "0.58.0",
8
8
  "@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
9
9
  "@vector-im/matrix-bot-sdk": "0.8.0-element.3",
10
10
  "markdown-it": "14.1.1",
@@ -1,36 +1,17 @@
1
1
  import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/matrix";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { createRuntimeEnv } from "../../test-utils/runtime-env.js";
3
4
  import { matrixPlugin } from "./channel.js";
4
5
  import { setMatrixRuntime } from "./runtime.js";
6
+ import { createMatrixBotSdkMock } from "./test-mocks.js";
5
7
  import type { CoreConfig } from "./types.js";
6
8
 
7
- vi.mock("@vector-im/matrix-bot-sdk", () => ({
8
- ConsoleLogger: class {
9
- trace = vi.fn();
10
- debug = vi.fn();
11
- info = vi.fn();
12
- warn = vi.fn();
13
- error = vi.fn();
14
- },
15
- MatrixClient: class {},
16
- LogService: {
17
- setLogger: vi.fn(),
18
- warn: vi.fn(),
19
- info: vi.fn(),
20
- debug: vi.fn(),
21
- },
22
- SimpleFsStorageProvider: class {},
23
- RustSdkCryptoStorageProvider: class {},
24
- }));
9
+ vi.mock("@vector-im/matrix-bot-sdk", () =>
10
+ createMatrixBotSdkMock({ includeVerboseLogService: true }),
11
+ );
25
12
 
26
13
  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
- };
14
+ const runtimeEnv: RuntimeEnv = createRuntimeEnv();
34
15
 
35
16
  beforeEach(() => {
36
17
  setMatrixRuntime({
package/src/channel.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  PAIRING_APPROVED_MESSAGE,
16
16
  type ChannelPlugin,
17
17
  } from "openclaw/plugin-sdk/matrix";
18
+ import { buildTrafficStatusSummary } from "../../shared/channel-status-summary.js";
18
19
  import { matrixMessageActions } from "./actions.js";
19
20
  import { MatrixConfigSchema } from "./config-schema.js";
20
21
  import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js";
@@ -410,8 +411,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
410
411
  lastError: runtime?.lastError ?? null,
411
412
  probe,
412
413
  lastProbeAt: runtime?.lastProbeAt ?? null,
413
- lastInboundAt: runtime?.lastInboundAt ?? null,
414
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
414
+ ...buildTrafficStatusSummary(runtime),
415
415
  }),
416
416
  },
417
417
  gateway: {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  compileAllowlist,
3
3
  normalizeStringEntries,
4
- resolveAllowlistCandidates,
4
+ resolveCompiledAllowlistMatch,
5
5
  type AllowlistMatch,
6
6
  } from "openclaw/plugin-sdk/matrix";
7
7
 
@@ -77,19 +77,13 @@ export function resolveMatrixAllowListMatch(params: {
77
77
  userId?: string;
78
78
  }): MatrixAllowListMatch {
79
79
  const compiledAllowList = compileAllowlist(params.allowList);
80
- if (compiledAllowList.set.size === 0) {
81
- return { allowed: false };
82
- }
83
- if (compiledAllowList.wildcard) {
84
- return { allowed: true, matchKey: "*", matchSource: "wildcard" };
85
- }
86
80
  const userId = normalizeMatrixUser(params.userId);
87
81
  const candidates: Array<{ value?: string; source: MatrixAllowListSource }> = [
88
82
  { value: userId, source: "id" },
89
83
  { value: userId ? `matrix:${userId}` : "", source: "prefixed-id" },
90
84
  { value: userId ? `user:${userId}` : "", source: "prefixed-user" },
91
85
  ];
92
- return resolveAllowlistCandidates({
86
+ return resolveCompiledAllowlistMatch({
93
87
  compiledAllowlist: compiledAllowList,
94
88
  candidates,
95
89
  });
@@ -7,6 +7,8 @@ import { createDirectRoomTracker } from "./direct.js";
7
7
 
8
8
  type StateEvent = Record<string, unknown>;
9
9
  type DmMap = Record<string, boolean>;
10
+ const brokenDmRoomId = "!broken-dm:example.org";
11
+ const defaultBrokenDmMembers = ["@alice:example.org", "@bot:example.org"];
10
12
 
11
13
  function createMockClient(opts: {
12
14
  dmRooms?: DmMap;
@@ -50,6 +52,21 @@ function createMockClient(opts: {
50
52
  };
51
53
  }
52
54
 
55
+ function createBrokenDmClient(roomNameEvent?: StateEvent) {
56
+ return createMockClient({
57
+ dmRooms: {},
58
+ membersByRoom: {
59
+ [brokenDmRoomId]: defaultBrokenDmMembers,
60
+ },
61
+ stateEvents: {
62
+ // is_direct not set on either member (e.g. Continuwuity bug)
63
+ [`${brokenDmRoomId}|m.room.member|@alice:example.org`]: {},
64
+ [`${brokenDmRoomId}|m.room.member|@bot:example.org`]: {},
65
+ ...(roomNameEvent ? { [`${brokenDmRoomId}|m.room.name|`]: roomNameEvent } : {}),
66
+ },
67
+ });
68
+ }
69
+
53
70
  // ---------------------------------------------------------------------------
54
71
  // Tests -- isDirectMessage
55
72
  // ---------------------------------------------------------------------------
@@ -131,22 +148,11 @@ describe("createDirectRoomTracker", () => {
131
148
 
132
149
  describe("conservative fallback (memberCount + room name)", () => {
133
150
  it("returns true for 2-member room WITHOUT a room name (broken flags)", async () => {
134
- const client = createMockClient({
135
- dmRooms: {},
136
- membersByRoom: {
137
- "!broken-dm:example.org": ["@alice:example.org", "@bot:example.org"],
138
- },
139
- stateEvents: {
140
- // is_direct not set on either member (e.g. Continuwuity bug)
141
- "!broken-dm:example.org|m.room.member|@alice:example.org": {},
142
- "!broken-dm:example.org|m.room.member|@bot:example.org": {},
143
- // No m.room.name -> getRoomStateEvent will throw (event not found)
144
- },
145
- });
151
+ const client = createBrokenDmClient();
146
152
  const tracker = createDirectRoomTracker(client as never);
147
153
 
148
154
  const result = await tracker.isDirectMessage({
149
- roomId: "!broken-dm:example.org",
155
+ roomId: brokenDmRoomId,
150
156
  senderId: "@alice:example.org",
151
157
  });
152
158
 
@@ -154,21 +160,11 @@ describe("createDirectRoomTracker", () => {
154
160
  });
155
161
 
156
162
  it("returns true for 2-member room with empty room name", async () => {
157
- const client = createMockClient({
158
- dmRooms: {},
159
- membersByRoom: {
160
- "!broken-dm:example.org": ["@alice:example.org", "@bot:example.org"],
161
- },
162
- stateEvents: {
163
- "!broken-dm:example.org|m.room.member|@alice:example.org": {},
164
- "!broken-dm:example.org|m.room.member|@bot:example.org": {},
165
- "!broken-dm:example.org|m.room.name|": { name: "" },
166
- },
167
- });
163
+ const client = createBrokenDmClient({ name: "" });
168
164
  const tracker = createDirectRoomTracker(client as never);
169
165
 
170
166
  const result = await tracker.isDirectMessage({
171
- roomId: "!broken-dm:example.org",
167
+ roomId: brokenDmRoomId,
172
168
  senderId: "@alice:example.org",
173
169
  });
174
170
 
@@ -12,6 +12,19 @@ vi.mock("../send.js", () => ({
12
12
  }));
13
13
 
14
14
  describe("registerMatrixMonitorEvents", () => {
15
+ const roomId = "!room:example.org";
16
+
17
+ function makeEvent(overrides: Partial<MatrixRawEvent>): MatrixRawEvent {
18
+ return {
19
+ event_id: "$event",
20
+ sender: "@alice:example.org",
21
+ type: "m.room.message",
22
+ origin_server_ts: 0,
23
+ content: {},
24
+ ...overrides,
25
+ };
26
+ }
27
+
15
28
  beforeEach(() => {
16
29
  sendReadReceiptMatrixMock.mockClear();
17
30
  });
@@ -53,12 +66,22 @@ describe("registerMatrixMonitorEvents", () => {
53
66
  return { client, getUserId, onRoomMessage, roomMessageHandler, logVerboseMessage };
54
67
  }
55
68
 
69
+ async function expectForwardedWithoutReadReceipt(event: MatrixRawEvent) {
70
+ const { onRoomMessage, roomMessageHandler } = createHarness();
71
+
72
+ roomMessageHandler(roomId, event);
73
+ await vi.waitFor(() => {
74
+ expect(onRoomMessage).toHaveBeenCalledWith(roomId, event);
75
+ });
76
+ expect(sendReadReceiptMatrixMock).not.toHaveBeenCalled();
77
+ }
78
+
56
79
  it("sends read receipt immediately for non-self messages", async () => {
57
80
  const { client, onRoomMessage, roomMessageHandler } = createHarness();
58
- const event = {
81
+ const event = makeEvent({
59
82
  event_id: "$e1",
60
83
  sender: "@alice:example.org",
61
- } as MatrixRawEvent;
84
+ });
62
85
 
63
86
  roomMessageHandler("!room:example.org", event);
64
87
 
@@ -69,36 +92,27 @@ describe("registerMatrixMonitorEvents", () => {
69
92
  });
70
93
 
71
94
  it("does not send read receipts for self messages", async () => {
72
- const { onRoomMessage, roomMessageHandler } = createHarness();
73
- const event = {
74
- event_id: "$e2",
75
- sender: "@bot:example.org",
76
- } as MatrixRawEvent;
77
-
78
- roomMessageHandler("!room:example.org", event);
79
- await vi.waitFor(() => {
80
- expect(onRoomMessage).toHaveBeenCalledWith("!room:example.org", event);
81
- });
82
- expect(sendReadReceiptMatrixMock).not.toHaveBeenCalled();
95
+ await expectForwardedWithoutReadReceipt(
96
+ makeEvent({
97
+ event_id: "$e2",
98
+ sender: "@bot:example.org",
99
+ }),
100
+ );
83
101
  });
84
102
 
85
103
  it("skips receipt when message lacks sender or event id", async () => {
86
- const { onRoomMessage, roomMessageHandler } = createHarness();
87
- const event = {
88
- sender: "@alice:example.org",
89
- } as MatrixRawEvent;
90
-
91
- roomMessageHandler("!room:example.org", event);
92
- await vi.waitFor(() => {
93
- expect(onRoomMessage).toHaveBeenCalledWith("!room:example.org", event);
94
- });
95
- expect(sendReadReceiptMatrixMock).not.toHaveBeenCalled();
104
+ await expectForwardedWithoutReadReceipt(
105
+ makeEvent({
106
+ sender: "@alice:example.org",
107
+ event_id: "",
108
+ }),
109
+ );
96
110
  });
97
111
 
98
112
  it("caches self user id across messages", async () => {
99
113
  const { getUserId, roomMessageHandler } = createHarness();
100
- const first = { event_id: "$e3", sender: "@alice:example.org" } as MatrixRawEvent;
101
- const second = { event_id: "$e4", sender: "@bob:example.org" } as MatrixRawEvent;
114
+ const first = makeEvent({ event_id: "$e3", sender: "@alice:example.org" });
115
+ const second = makeEvent({ event_id: "$e4", sender: "@bob:example.org" });
102
116
 
103
117
  roomMessageHandler("!room:example.org", first);
104
118
  roomMessageHandler("!room:example.org", second);
@@ -112,7 +126,7 @@ describe("registerMatrixMonitorEvents", () => {
112
126
  it("logs and continues when sending read receipt fails", async () => {
113
127
  sendReadReceiptMatrixMock.mockRejectedValueOnce(new Error("network boom"));
114
128
  const { roomMessageHandler, onRoomMessage, logVerboseMessage } = createHarness();
115
- const event = { event_id: "$e5", sender: "@alice:example.org" } as MatrixRawEvent;
129
+ const event = makeEvent({ event_id: "$e5", sender: "@alice:example.org" });
116
130
 
117
131
  roomMessageHandler("!room:example.org", event);
118
132
 
@@ -128,7 +142,7 @@ describe("registerMatrixMonitorEvents", () => {
128
142
  const { roomMessageHandler, onRoomMessage, getUserId } = createHarness({
129
143
  getUserId: vi.fn().mockRejectedValue(new Error("cannot resolve self")),
130
144
  });
131
- const event = { event_id: "$e6", sender: "@alice:example.org" } as MatrixRawEvent;
145
+ const event = makeEvent({ event_id: "$e6", sender: "@alice:example.org" });
132
146
 
133
147
  roomMessageHandler("!room:example.org", event);
134
148
 
@@ -686,6 +686,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
686
686
  channel: "matrix",
687
687
  accountId: route.accountId,
688
688
  });
689
+ const humanDelay = core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId);
689
690
  const typingCallbacks = createTypingCallbacks({
690
691
  start: () => sendTypingMatrix(roomId, true, undefined, client),
691
692
  stop: () => sendTypingMatrix(roomId, false, undefined, client),
@@ -711,7 +712,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
711
712
  const { dispatcher, replyOptions, markDispatchIdle } =
712
713
  core.channel.reply.createReplyDispatcherWithTyping({
713
714
  ...prefixOptions,
714
- humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
715
+ humanDelay,
715
716
  typingCallbacks,
716
717
  deliver: async (payload) => {
717
718
  await deliverMatrixReplies({
@@ -1,16 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { createDeferred } from "../../../shared/deferred.js";
2
3
  import { DEFAULT_SEND_GAP_MS, enqueueSend } from "./send-queue.js";
3
4
 
4
- function deferred<T>() {
5
- let resolve!: (value: T | PromiseLike<T>) => void;
6
- let reject!: (reason?: unknown) => void;
7
- const promise = new Promise<T>((res, rej) => {
8
- resolve = res;
9
- reject = rej;
10
- });
11
- return { promise, resolve, reject };
12
- }
13
-
14
5
  describe("enqueueSend", () => {
15
6
  beforeEach(() => {
16
7
  vi.useFakeTimers();
@@ -21,7 +12,7 @@ describe("enqueueSend", () => {
21
12
  });
22
13
 
23
14
  it("serializes sends per room", async () => {
24
- const gate = deferred<void>();
15
+ const gate = createDeferred<void>();
25
16
  const events: string[] = [];
26
17
 
27
18
  const first = enqueueSend("!room:example.org", async () => {
@@ -91,7 +82,7 @@ describe("enqueueSend", () => {
91
82
  });
92
83
 
93
84
  it("continues queued work when the head task fails", async () => {
94
- const gate = deferred<void>();
85
+ const gate = createDeferred<void>();
95
86
  const events: string[] = [];
96
87
 
97
88
  const first = enqueueSend("!room:example.org", async () => {
@@ -1,6 +1,7 @@
1
1
  import type { PluginRuntime } from "openclaw/plugin-sdk/matrix";
2
2
  import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { setMatrixRuntime } from "../runtime.js";
4
+ import { createMatrixBotSdkMock } from "../test-mocks.js";
4
5
 
5
6
  vi.mock("music-metadata", () => ({
6
7
  // `resolveMediaDurationMs` lazily imports `music-metadata`; in tests we don't
@@ -8,21 +9,13 @@ vi.mock("music-metadata", () => ({
8
9
  parseBuffer: vi.fn().mockResolvedValue({ format: {} }),
9
10
  }));
10
11
 
11
- vi.mock("@vector-im/matrix-bot-sdk", () => ({
12
- ConsoleLogger: class {
13
- trace = vi.fn();
14
- debug = vi.fn();
15
- info = vi.fn();
16
- warn = vi.fn();
17
- error = vi.fn();
18
- },
19
- LogService: {
20
- setLogger: vi.fn(),
21
- },
22
- MatrixClient: vi.fn(),
23
- SimpleFsStorageProvider: vi.fn(),
24
- RustSdkCryptoStorageProvider: vi.fn(),
25
- }));
12
+ vi.mock("@vector-im/matrix-bot-sdk", () =>
13
+ createMatrixBotSdkMock({
14
+ matrixClient: vi.fn(),
15
+ simpleFsStorageProvider: vi.fn(),
16
+ rustSdkCryptoStorageProvider: vi.fn(),
17
+ }),
18
+ );
26
19
 
27
20
  vi.mock("./send-queue.js", () => ({
28
21
  enqueueSend: async <T>(_roomId: string, fn: () => Promise<T>) => await fn(),
@@ -8,6 +8,15 @@ vi.mock("./directory-live.js", () => ({
8
8
  listMatrixDirectoryGroupsLive: vi.fn(),
9
9
  }));
10
10
 
11
+ async function resolveUserTarget(input = "Alice") {
12
+ const [result] = await resolveMatrixTargets({
13
+ cfg: {},
14
+ inputs: [input],
15
+ kind: "user",
16
+ });
17
+ return result;
18
+ }
19
+
11
20
  describe("resolveMatrixTargets (users)", () => {
12
21
  beforeEach(() => {
13
22
  vi.mocked(listMatrixDirectoryPeersLive).mockReset();
@@ -20,11 +29,7 @@ describe("resolveMatrixTargets (users)", () => {
20
29
  ];
21
30
  vi.mocked(listMatrixDirectoryPeersLive).mockResolvedValue(matches);
22
31
 
23
- const [result] = await resolveMatrixTargets({
24
- cfg: {},
25
- inputs: ["Alice"],
26
- kind: "user",
27
- });
32
+ const result = await resolveUserTarget();
28
33
 
29
34
  expect(result?.resolved).toBe(true);
30
35
  expect(result?.id).toBe("@alice:example.org");
@@ -37,11 +42,7 @@ describe("resolveMatrixTargets (users)", () => {
37
42
  ];
38
43
  vi.mocked(listMatrixDirectoryPeersLive).mockResolvedValue(matches);
39
44
 
40
- const [result] = await resolveMatrixTargets({
41
- cfg: {},
42
- inputs: ["Alice"],
43
- kind: "user",
44
- });
45
+ const result = await resolveUserTarget();
45
46
 
46
47
  expect(result?.resolved).toBe(false);
47
48
  expect(result?.note).toMatch(/use full Matrix ID/i);
@@ -0,0 +1,53 @@
1
+ import type { Mock } from "vitest";
2
+ import { vi } from "vitest";
3
+
4
+ type MatrixBotSdkMockParams = {
5
+ matrixClient?: unknown;
6
+ simpleFsStorageProvider?: unknown;
7
+ rustSdkCryptoStorageProvider?: unknown;
8
+ includeVerboseLogService?: boolean;
9
+ };
10
+
11
+ type MatrixBotSdkMock = {
12
+ ConsoleLogger: new () => {
13
+ trace: Mock<() => void>;
14
+ debug: Mock<() => void>;
15
+ info: Mock<() => void>;
16
+ warn: Mock<() => void>;
17
+ error: Mock<() => void>;
18
+ };
19
+ MatrixClient: unknown;
20
+ LogService: {
21
+ setLogger: Mock<() => void>;
22
+ warn?: Mock<() => void>;
23
+ info?: Mock<() => void>;
24
+ debug?: Mock<() => void>;
25
+ };
26
+ SimpleFsStorageProvider: unknown;
27
+ RustSdkCryptoStorageProvider: unknown;
28
+ };
29
+
30
+ export function createMatrixBotSdkMock(params: MatrixBotSdkMockParams = {}): MatrixBotSdkMock {
31
+ return {
32
+ ConsoleLogger: class {
33
+ trace = vi.fn();
34
+ debug = vi.fn();
35
+ info = vi.fn();
36
+ warn = vi.fn();
37
+ error = vi.fn();
38
+ },
39
+ MatrixClient: params.matrixClient ?? class {},
40
+ LogService: {
41
+ setLogger: vi.fn(),
42
+ ...(params.includeVerboseLogService
43
+ ? {
44
+ warn: vi.fn(),
45
+ info: vi.fn(),
46
+ debug: vi.fn(),
47
+ }
48
+ : {}),
49
+ },
50
+ SimpleFsStorageProvider: params.simpleFsStorageProvider ?? class {},
51
+ RustSdkCryptoStorageProvider: params.rustSdkCryptoStorageProvider ?? class {},
52
+ };
53
+ }