@openclaw/matrix 2026.2.21 → 2026.2.23
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 +6 -0
- package/package.json +1 -1
- package/src/channel.ts +8 -2
- package/src/matrix/deps.ts +2 -82
- package/src/matrix/monitor/handler.ts +4 -3
- package/src/matrix/monitor/index.ts +29 -16
- package/src/matrix/monitor/replies.test.ts +52 -0
- package/src/matrix/monitor/replies.ts +24 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
formatPairingApproveHint,
|
|
7
7
|
normalizeAccountId,
|
|
8
8
|
PAIRING_APPROVED_MESSAGE,
|
|
9
|
+
resolveAllowlistProviderRuntimeGroupPolicy,
|
|
10
|
+
resolveDefaultGroupPolicy,
|
|
9
11
|
setAccountEnabledInConfigSection,
|
|
10
12
|
type ChannelPlugin,
|
|
11
13
|
} from "openclaw/plugin-sdk";
|
|
@@ -169,8 +171,12 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|
|
169
171
|
};
|
|
170
172
|
},
|
|
171
173
|
collectWarnings: ({ account, cfg }) => {
|
|
172
|
-
const defaultGroupPolicy = (cfg as CoreConfig)
|
|
173
|
-
const
|
|
174
|
+
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg as CoreConfig);
|
|
175
|
+
const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
|
|
176
|
+
providerConfigPresent: (cfg as CoreConfig).channels?.matrix !== undefined,
|
|
177
|
+
groupPolicy: account.config.groupPolicy,
|
|
178
|
+
defaultGroupPolicy,
|
|
179
|
+
});
|
|
174
180
|
if (groupPolicy !== "open") {
|
|
175
181
|
return [];
|
|
176
182
|
}
|
package/src/matrix/deps.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
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";
|
|
6
|
-
import type
|
|
5
|
+
import { runPluginCommandWithTimeout, type RuntimeEnv } from "openclaw/plugin-sdk";
|
|
7
6
|
|
|
8
7
|
const MATRIX_SDK_PACKAGE = "@vector-im/matrix-bot-sdk";
|
|
9
8
|
|
|
@@ -22,85 +21,6 @@ function resolvePluginRoot(): string {
|
|
|
22
21
|
return path.resolve(currentDir, "..", "..");
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
type CommandResult = {
|
|
26
|
-
code: number;
|
|
27
|
-
stdout: string;
|
|
28
|
-
stderr: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
async function runFixedCommandWithTimeout(params: {
|
|
32
|
-
argv: string[];
|
|
33
|
-
cwd: string;
|
|
34
|
-
timeoutMs: number;
|
|
35
|
-
env?: NodeJS.ProcessEnv;
|
|
36
|
-
}): Promise<CommandResult> {
|
|
37
|
-
return await new Promise((resolve) => {
|
|
38
|
-
const [command, ...args] = params.argv;
|
|
39
|
-
if (!command) {
|
|
40
|
-
resolve({
|
|
41
|
-
code: 1,
|
|
42
|
-
stdout: "",
|
|
43
|
-
stderr: "command is required",
|
|
44
|
-
});
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const proc = spawn(command, args, {
|
|
49
|
-
cwd: params.cwd,
|
|
50
|
-
env: { ...process.env, ...params.env },
|
|
51
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
let stdout = "";
|
|
55
|
-
let stderr = "";
|
|
56
|
-
let settled = false;
|
|
57
|
-
let timer: NodeJS.Timeout | null = null;
|
|
58
|
-
|
|
59
|
-
const finalize = (result: CommandResult) => {
|
|
60
|
-
if (settled) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
settled = true;
|
|
64
|
-
if (timer) {
|
|
65
|
-
clearTimeout(timer);
|
|
66
|
-
}
|
|
67
|
-
resolve(result);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
proc.stdout?.on("data", (chunk: Buffer | string) => {
|
|
71
|
-
stdout += chunk.toString();
|
|
72
|
-
});
|
|
73
|
-
proc.stderr?.on("data", (chunk: Buffer | string) => {
|
|
74
|
-
stderr += chunk.toString();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
timer = setTimeout(() => {
|
|
78
|
-
proc.kill("SIGKILL");
|
|
79
|
-
finalize({
|
|
80
|
-
code: 124,
|
|
81
|
-
stdout,
|
|
82
|
-
stderr: stderr || `command timed out after ${params.timeoutMs}ms`,
|
|
83
|
-
});
|
|
84
|
-
}, params.timeoutMs);
|
|
85
|
-
|
|
86
|
-
proc.on("error", (err) => {
|
|
87
|
-
finalize({
|
|
88
|
-
code: 1,
|
|
89
|
-
stdout,
|
|
90
|
-
stderr: err.message,
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
proc.on("close", (code) => {
|
|
95
|
-
finalize({
|
|
96
|
-
code: code ?? 1,
|
|
97
|
-
stdout,
|
|
98
|
-
stderr,
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
24
|
export async function ensureMatrixSdkInstalled(params: {
|
|
105
25
|
runtime: RuntimeEnv;
|
|
106
26
|
confirm?: (message: string) => Promise<boolean>;
|
|
@@ -121,7 +41,7 @@ export async function ensureMatrixSdkInstalled(params: {
|
|
|
121
41
|
? ["pnpm", "install"]
|
|
122
42
|
: ["npm", "install", "--omit=dev", "--silent"];
|
|
123
43
|
params.runtime.log?.(`matrix: installing dependencies via ${command[0]} (${root})…`);
|
|
124
|
-
const result = await
|
|
44
|
+
const result = await runPluginCommandWithTimeout({
|
|
125
45
|
argv: command,
|
|
126
46
|
cwd: root,
|
|
127
47
|
timeoutMs: 300_000,
|
|
@@ -218,9 +218,10 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
const senderName = await getMemberDisplayName(roomId, senderId);
|
|
221
|
-
const storeAllowFrom =
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
const storeAllowFrom =
|
|
222
|
+
dmPolicy === "allowlist"
|
|
223
|
+
? []
|
|
224
|
+
: await core.channel.pairing.readAllowFromStore("matrix").catch(() => []);
|
|
224
225
|
const effectiveAllowFrom = normalizeMatrixAllowList([...allowFrom, ...storeAllowFrom]);
|
|
225
226
|
const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
|
|
226
227
|
const effectiveGroupAllowFrom = normalizeMatrixAllowList(groupAllowFrom);
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createLoggerBackedRuntime,
|
|
3
|
+
GROUP_POLICY_BLOCKED_LABEL,
|
|
4
|
+
mergeAllowlist,
|
|
5
|
+
resolveAllowlistProviderRuntimeGroupPolicy,
|
|
6
|
+
resolveDefaultGroupPolicy,
|
|
7
|
+
summarizeMapping,
|
|
8
|
+
warnMissingProviderGroupPolicyFallbackOnce,
|
|
9
|
+
type RuntimeEnv,
|
|
10
|
+
} from "openclaw/plugin-sdk";
|
|
3
11
|
import { resolveMatrixTargets } from "../../resolve-targets.js";
|
|
4
12
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
5
13
|
import type { CoreConfig, ReplyToMode } from "../../types.js";
|
|
@@ -40,18 +48,11 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
const logger = core.logging.getChildLogger({ module: "matrix-auto-reply" });
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
logger
|
|
47
|
-
}
|
|
48
|
-
error: (...args) => {
|
|
49
|
-
logger.error(formatRuntimeMessage(...args));
|
|
50
|
-
},
|
|
51
|
-
exit: (code: number): never => {
|
|
52
|
-
throw new Error(`exit ${code}`);
|
|
53
|
-
},
|
|
54
|
-
};
|
|
51
|
+
const runtime: RuntimeEnv =
|
|
52
|
+
opts.runtime ??
|
|
53
|
+
createLoggerBackedRuntime({
|
|
54
|
+
logger,
|
|
55
|
+
});
|
|
55
56
|
const logVerboseMessage = (message: string) => {
|
|
56
57
|
if (!core.logging.shouldLogVerbose()) {
|
|
57
58
|
return;
|
|
@@ -242,8 +243,20 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
242
243
|
setActiveMatrixClient(client, opts.accountId);
|
|
243
244
|
|
|
244
245
|
const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg);
|
|
245
|
-
const defaultGroupPolicy = cfg
|
|
246
|
-
const
|
|
246
|
+
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
|
247
|
+
const { groupPolicy: groupPolicyRaw, providerMissingFallbackApplied } =
|
|
248
|
+
resolveAllowlistProviderRuntimeGroupPolicy({
|
|
249
|
+
providerConfigPresent: cfg.channels?.matrix !== undefined,
|
|
250
|
+
groupPolicy: accountConfig.groupPolicy,
|
|
251
|
+
defaultGroupPolicy,
|
|
252
|
+
});
|
|
253
|
+
warnMissingProviderGroupPolicyFallbackOnce({
|
|
254
|
+
providerMissingFallbackApplied,
|
|
255
|
+
providerKey: "matrix",
|
|
256
|
+
accountId: account.accountId,
|
|
257
|
+
blockedLabel: GROUP_POLICY_BLOCKED_LABEL.room,
|
|
258
|
+
log: (message) => logVerboseMessage(message),
|
|
259
|
+
});
|
|
247
260
|
const groupPolicy = allowlistOnly && groupPolicyRaw === "open" ? "allowlist" : groupPolicyRaw;
|
|
248
261
|
const replyToMode = opts.replyToMode ?? accountConfig.replyToMode ?? "off";
|
|
249
262
|
const threadReplies = accountConfig.threadReplies ?? "inbound";
|
|
@@ -108,6 +108,58 @@ describe("deliverMatrixReplies", () => {
|
|
|
108
108
|
);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
it("skips reasoning-only replies with Reasoning prefix", async () => {
|
|
112
|
+
await deliverMatrixReplies({
|
|
113
|
+
replies: [
|
|
114
|
+
{ text: "Reasoning:\nThe user wants X because Y.", replyToId: "r1" },
|
|
115
|
+
{ text: "Here is the answer.", replyToId: "r2" },
|
|
116
|
+
],
|
|
117
|
+
roomId: "room:reason",
|
|
118
|
+
client: {} as MatrixClient,
|
|
119
|
+
runtime: runtimeEnv,
|
|
120
|
+
textLimit: 4000,
|
|
121
|
+
replyToMode: "first",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(1);
|
|
125
|
+
expect(sendMessageMatrixMock.mock.calls[0]?.[1]).toBe("Here is the answer.");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("skips reasoning-only replies with thinking tags", async () => {
|
|
129
|
+
await deliverMatrixReplies({
|
|
130
|
+
replies: [
|
|
131
|
+
{ text: "<thinking>internal chain of thought</thinking>", replyToId: "r1" },
|
|
132
|
+
{ text: " <think>more reasoning</think> ", replyToId: "r2" },
|
|
133
|
+
{ text: "<antthinking>hidden</antthinking>", replyToId: "r3" },
|
|
134
|
+
{ text: "Visible reply", replyToId: "r4" },
|
|
135
|
+
],
|
|
136
|
+
roomId: "room:tags",
|
|
137
|
+
client: {} as MatrixClient,
|
|
138
|
+
runtime: runtimeEnv,
|
|
139
|
+
textLimit: 4000,
|
|
140
|
+
replyToMode: "all",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(1);
|
|
144
|
+
expect(sendMessageMatrixMock.mock.calls[0]?.[1]).toBe("Visible reply");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("delivers all replies when none are reasoning-only", async () => {
|
|
148
|
+
await deliverMatrixReplies({
|
|
149
|
+
replies: [
|
|
150
|
+
{ text: "First answer", replyToId: "r1" },
|
|
151
|
+
{ text: "Second answer", replyToId: "r2" },
|
|
152
|
+
],
|
|
153
|
+
roomId: "room:normal",
|
|
154
|
+
client: {} as MatrixClient,
|
|
155
|
+
runtime: runtimeEnv,
|
|
156
|
+
textLimit: 4000,
|
|
157
|
+
replyToMode: "all",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(2);
|
|
161
|
+
});
|
|
162
|
+
|
|
111
163
|
it("suppresses replyToId when threadId is set", async () => {
|
|
112
164
|
chunkMarkdownTextWithModeMock.mockImplementation((text: string) => text.split("|"));
|
|
113
165
|
|
|
@@ -41,6 +41,11 @@ export async function deliverMatrixReplies(params: {
|
|
|
41
41
|
params.runtime.error?.("matrix reply missing text/media");
|
|
42
42
|
continue;
|
|
43
43
|
}
|
|
44
|
+
// Skip pure reasoning messages so internal thinking traces are never delivered.
|
|
45
|
+
if (reply.text && isReasoningOnlyMessage(reply.text)) {
|
|
46
|
+
logVerbose("matrix reply is reasoning-only; skipping");
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
44
49
|
const replyToIdRaw = reply.replyToId?.trim();
|
|
45
50
|
const replyToId = params.threadId || params.replyToMode === "off" ? undefined : replyToIdRaw;
|
|
46
51
|
const rawText = reply.text ?? "";
|
|
@@ -98,3 +103,22 @@ export async function deliverMatrixReplies(params: {
|
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
}
|
|
106
|
+
|
|
107
|
+
const REASONING_PREFIX = "Reasoning:\n";
|
|
108
|
+
const THINKING_TAG_RE = /^\s*<\s*(?:think(?:ing)?|thought|antthinking)\b/i;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Detect messages that contain only reasoning/thinking content and no user-facing answer.
|
|
112
|
+
* These are emitted by the agent when `includeReasoning` is active but should not
|
|
113
|
+
* be forwarded to channels that do not support a dedicated reasoning lane.
|
|
114
|
+
*/
|
|
115
|
+
function isReasoningOnlyMessage(text: string): boolean {
|
|
116
|
+
const trimmed = text.trim();
|
|
117
|
+
if (trimmed.startsWith(REASONING_PREFIX)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (THINKING_TAG_RE.test(trimmed)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|