matrix-js: improve thread context and auto-threading

This commit is contained in:
Gustavo Madeira Santana
2026-03-08 16:36:14 -04:00
parent a670c21ab4
commit 3eb6c4c8ec
11 changed files with 610 additions and 2 deletions

View File

@@ -273,6 +273,12 @@ export type ChannelThreadingToolContext = {
currentChannelProvider?: ChannelId;
currentThreadTs?: string;
currentMessageId?: string | number;
/**
* Optional direct-chat participant identifier for channels whose outbound
* tool targets can address either the backing conversation id or the direct
* participant id.
*/
currentDirectUserId?: string;
replyToMode?: "off" | "first" | "all";
hasRepliedRef?: { value: boolean };
/**

View File

@@ -71,6 +71,49 @@ export function resolveTelegramAutoThreadId(params: {
return context.currentThreadTs;
}
function normalizeMatrixThreadTarget(raw: string): string | undefined {
let normalized = raw.trim();
if (!normalized) {
return undefined;
}
if (normalized.toLowerCase().startsWith("matrix:")) {
normalized = normalized.slice("matrix:".length).trim();
}
normalized = normalized.replace(/^(room|channel|user):/i, "").trim();
return normalized || undefined;
}
function normalizeMatrixDirectUserTarget(raw: string): string | undefined {
const normalized = normalizeMatrixThreadTarget(raw);
return normalized?.startsWith("@") ? normalized : undefined;
}
export function resolveMatrixAutoThreadId(params: {
to: string;
toolContext?: ChannelThreadingToolContext;
}): string | undefined {
const context = params.toolContext;
if (!context?.currentThreadTs || !context.currentChannelId) {
return undefined;
}
const target = normalizeMatrixThreadTarget(params.to);
const currentChannel = normalizeMatrixThreadTarget(context.currentChannelId);
if (!target || !currentChannel) {
return undefined;
}
if (target.toLowerCase() !== currentChannel.toLowerCase()) {
const directTarget = normalizeMatrixDirectUserTarget(params.to);
const currentDirectUserId = normalizeMatrixDirectUserTarget(context.currentDirectUserId ?? "");
if (!directTarget || !currentDirectUserId) {
return undefined;
}
if (directTarget.toLowerCase() !== currentDirectUserId.toLowerCase()) {
return undefined;
}
}
return context.currentThreadTs;
}
function resolveAttachmentMaxBytes(params: {
cfg: OpenClawConfig;
channel: ChannelId;

View File

@@ -1,4 +1,5 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { matrixPlugin } from "../../../extensions/matrix-js/src/channel.js";
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
import type { OpenClawConfig } from "../../config/config.js";
@@ -49,6 +50,15 @@ const telegramConfig = {
},
} as OpenClawConfig;
const matrixConfig = {
channels: {
"matrix-js": {
homeserver: "https://matrix.example.org",
accessToken: "matrix-test",
},
},
} as OpenClawConfig;
async function runThreadingAction(params: {
cfg: OpenClawConfig;
actionParams: Record<string, unknown>;
@@ -80,23 +90,42 @@ const defaultTelegramToolContext = {
currentThreadTs: "42",
} as const;
const defaultMatrixToolContext = {
currentChannelId: "room:!room:example.org",
currentThreadTs: "$thread",
} as const;
const defaultMatrixDmToolContext = {
currentChannelId: "room:!dm:example.org",
currentThreadTs: "$thread",
currentDirectUserId: "@alice:example.org",
} as const;
let createPluginRuntime: typeof import("../../plugins/runtime/index.js").createPluginRuntime;
let setMatrixRuntime: typeof import("../../../extensions/matrix-js/src/runtime.js").setMatrixRuntime;
let setSlackRuntime: typeof import("../../../extensions/slack/src/runtime.js").setSlackRuntime;
let setTelegramRuntime: typeof import("../../../extensions/telegram/src/runtime.js").setTelegramRuntime;
describe("runMessageAction threading auto-injection", () => {
beforeAll(async () => {
({ createPluginRuntime } = await import("../../plugins/runtime/index.js"));
({ setMatrixRuntime } = await import("../../../extensions/matrix-js/src/runtime.js"));
({ setSlackRuntime } = await import("../../../extensions/slack/src/runtime.js"));
({ setTelegramRuntime } = await import("../../../extensions/telegram/src/runtime.js"));
});
beforeEach(() => {
const runtime = createPluginRuntime();
setMatrixRuntime(runtime);
setSlackRuntime(runtime);
setTelegramRuntime(runtime);
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "matrix-js",
source: "test",
plugin: matrixPlugin,
},
{
pluginId: "slack",
source: "test",
@@ -221,4 +250,96 @@ describe("runMessageAction threading auto-injection", () => {
expect(call?.replyToId).toBe("777");
expect(call?.ctx?.params?.replyTo).toBe("777");
});
it.each([
{
name: "injects threadId for bare room id",
target: "!room:example.org",
expectedThreadId: "$thread",
},
{
name: "injects threadId for room target prefix",
target: "room:!room:example.org",
expectedThreadId: "$thread",
},
{
name: "injects threadId for matrix room target",
target: "matrix:room:!room:example.org",
expectedThreadId: "$thread",
},
{
name: "skips threadId when target room differs",
target: "!other:example.org",
expectedThreadId: undefined,
},
] as const)("matrix auto-threading: $name", async (testCase) => {
mockHandledSendAction();
const call = await runThreadingAction({
cfg: matrixConfig,
actionParams: {
channel: "matrix-js",
target: testCase.target,
message: "hi",
},
toolContext: defaultMatrixToolContext,
});
expect(call?.ctx?.params?.threadId).toBe(testCase.expectedThreadId);
if (testCase.expectedThreadId !== undefined) {
expect(call?.threadId).toBe(testCase.expectedThreadId);
}
});
it("uses explicit matrix threadId when provided", async () => {
mockHandledSendAction();
const call = await runThreadingAction({
cfg: matrixConfig,
actionParams: {
channel: "matrix-js",
target: "room:!room:example.org",
message: "hi",
threadId: "$explicit",
},
toolContext: defaultMatrixToolContext,
});
expect(call?.threadId).toBe("$explicit");
expect(call?.ctx?.params?.threadId).toBe("$explicit");
});
it("injects threadId for matching Matrix dm user target", async () => {
mockHandledSendAction();
const call = await runThreadingAction({
cfg: matrixConfig,
actionParams: {
channel: "matrix-js",
target: "user:@alice:example.org",
message: "hi",
},
toolContext: defaultMatrixDmToolContext,
});
expect(call?.threadId).toBe("$thread");
expect(call?.ctx?.params?.threadId).toBe("$thread");
});
it("skips threadId for different Matrix dm user target", async () => {
mockHandledSendAction();
const call = await runThreadingAction({
cfg: matrixConfig,
actionParams: {
channel: "matrix-js",
target: "user:@bob:example.org",
message: "hi",
},
toolContext: defaultMatrixDmToolContext,
});
expect(call?.threadId).toBeUndefined();
expect(call?.ctx?.params?.threadId).toBeUndefined();
});
});

View File

@@ -35,6 +35,7 @@ import {
parseComponentsParam,
readBooleanParam,
resolveAttachmentMediaPolicy,
resolveMatrixAutoThreadId,
resolveSlackAutoThreadId,
resolveTelegramAutoThreadId,
} from "./message-action-params.js";
@@ -78,7 +79,11 @@ function resolveAndApplyOutboundThreadId(
ctx.channel === "telegram" && !threadId
? resolveTelegramAutoThreadId({ to: ctx.to, toolContext: ctx.toolContext })
: undefined;
const resolved = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
const matrixAutoThreadId =
ctx.channel === "matrix-js" && !threadId
? resolveMatrixAutoThreadId({ to: ctx.to, toolContext: ctx.toolContext })
: undefined;
const resolved = threadId ?? slackAutoThreadId ?? telegramAutoThreadId ?? matrixAutoThreadId;
// Write auto-resolved threadId back into params so downstream dispatch
// (plugin `readStringParam(params, "threadId")`) picks it up.
if (resolved && !params.threadId) {