refactor(matrix): dedupe sender label resolution for inbound bodies

This commit is contained in:
Peter Steinberger
2026-02-26 20:53:58 +01:00
parent 01b4f42f9a
commit 8483e01a68
4 changed files with 183 additions and 19 deletions

View File

@@ -0,0 +1,142 @@
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
import type { PluginRuntime, RuntimeEnv, RuntimeLogger } from "openclaw/plugin-sdk";
import { describe, expect, it, vi } from "vitest";
import { createMatrixRoomMessageHandler } from "./handler.js";
import { EventType, type MatrixRawEvent } from "./types.js";
describe("createMatrixRoomMessageHandler BodyForAgent sender label", () => {
it("stores sender-labeled BodyForAgent for group thread messages", async () => {
const recordInboundSession = vi.fn().mockResolvedValue(undefined);
const formatInboundEnvelope = vi
.fn()
.mockImplementation((params: { senderLabel?: string; body: string }) => params.body);
const finalizeInboundContext = vi
.fn()
.mockImplementation((ctx: Record<string, unknown>) => ctx);
const core = {
channel: {
pairing: {
readAllowFromStore: vi.fn().mockResolvedValue([]),
},
routing: {
resolveAgentRoute: vi.fn().mockReturnValue({
agentId: "main",
accountId: undefined,
sessionKey: "agent:main:matrix:channel:!room:example.org",
mainSessionKey: "agent:main:main",
}),
},
session: {
resolveStorePath: vi.fn().mockReturnValue("/tmp/openclaw-test-session.json"),
readSessionUpdatedAt: vi.fn().mockReturnValue(123),
recordInboundSession,
},
reply: {
resolveEnvelopeFormatOptions: vi.fn().mockReturnValue({}),
formatInboundEnvelope,
formatAgentEnvelope: vi
.fn()
.mockImplementation((params: { body: string }) => params.body),
finalizeInboundContext,
resolveHumanDelayConfig: vi.fn().mockReturnValue(undefined),
createReplyDispatcherWithTyping: vi.fn().mockReturnValue({
dispatcher: {},
replyOptions: {},
markDispatchIdle: vi.fn(),
}),
withReplyDispatcher: vi
.fn()
.mockResolvedValue({ queuedFinal: false, counts: { final: 0, partial: 0, tool: 0 } }),
},
commands: {
shouldHandleTextCommands: vi.fn().mockReturnValue(true),
},
text: {
hasControlCommand: vi.fn().mockReturnValue(false),
resolveMarkdownTableMode: vi.fn().mockReturnValue("code"),
},
},
system: {
enqueueSystemEvent: vi.fn(),
},
} as unknown as PluginRuntime;
const runtime = {
error: vi.fn(),
} as unknown as RuntimeEnv;
const logger = {
info: vi.fn(),
warn: vi.fn(),
} as unknown as RuntimeLogger;
const logVerboseMessage = vi.fn();
const client = {
getUserId: vi.fn().mockResolvedValue("@bot:matrix.example.org"),
} as unknown as MatrixClient;
const handler = createMatrixRoomMessageHandler({
client,
core,
cfg: {},
runtime,
logger,
logVerboseMessage,
allowFrom: [],
roomsConfig: undefined,
mentionRegexes: [],
groupPolicy: "open",
replyToMode: "first",
threadReplies: "inbound",
dmEnabled: true,
dmPolicy: "open",
textLimit: 4000,
mediaMaxBytes: 5 * 1024 * 1024,
startupMs: Date.now(),
startupGraceMs: 60_000,
directTracker: {
isDirectMessage: vi.fn().mockResolvedValue(false),
},
getRoomInfo: vi.fn().mockResolvedValue({
name: "Dev Room",
canonicalAlias: "#dev:matrix.example.org",
altAliases: [],
}),
getMemberDisplayName: vi.fn().mockResolvedValue("Bu"),
accountId: undefined,
});
const event = {
type: EventType.RoomMessage,
event_id: "$event1",
sender: "@bu:matrix.example.org",
origin_server_ts: Date.now(),
content: {
msgtype: "m.text",
body: "show me my commits",
"m.mentions": { user_ids: ["@bot:matrix.example.org"] },
"m.relates_to": {
rel_type: "m.thread",
event_id: "$thread-root",
},
},
} as unknown as MatrixRawEvent;
await handler("!room:example.org", event);
expect(formatInboundEnvelope).toHaveBeenCalledWith(
expect.objectContaining({
chatType: "channel",
senderLabel: "Bu (bu)",
}),
);
expect(recordInboundSession).toHaveBeenCalledWith(
expect.objectContaining({
ctx: expect.objectContaining({
ChatType: "thread",
BodyForAgent: "Bu (bu): show me my commits",
}),
}),
);
});
});

View File

@@ -26,7 +26,11 @@ import {
resolveMatrixAllowListMatch,
resolveMatrixAllowListMatches,
} from "./allowlist.js";
import { resolveMatrixBodyForAgent } from "./inbound-body.js";
import {
resolveMatrixBodyForAgent,
resolveMatrixInboundSenderLabel,
resolveMatrixSenderUsername,
} from "./inbound-body.js";
import { resolveMatrixLocation, type MatrixLocationPayload } from "./location.js";
import { downloadMatrixMedia } from "./media.js";
import { resolveMentions } from "./mentions.js";
@@ -216,7 +220,12 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
}
const senderName = await getMemberDisplayName(roomId, senderId);
const senderUsername = senderId.split(":")[0]?.replace(/^@/, "");
const senderUsername = resolveMatrixSenderUsername(senderId);
const senderLabel = resolveMatrixInboundSenderLabel({
senderName,
senderId,
senderUsername,
});
const storeAllowFrom = isDirectMessage
? await readStoreAllowFromForDmPolicy({
provider: "matrix",
@@ -531,7 +540,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
envelope: envelopeOptions,
body: textWithId,
chatType: isDirectMessage ? "direct" : "channel",
sender: { name: senderName, username: senderUsername },
senderLabel,
});
const groupSystemPrompt = roomConfig?.systemPrompt?.trim() || undefined;
@@ -540,8 +549,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
BodyForAgent: resolveMatrixBodyForAgent({
isDirectMessage,
bodyText,
senderName,
senderId,
senderLabel,
}),
RawBody: bodyText,
CommandBody: bodyText,

View File

@@ -1,7 +1,27 @@
import { describe, expect, it } from "vitest";
import { resolveMatrixBodyForAgent, resolveMatrixInboundSenderLabel } from "./inbound-body.js";
import {
resolveMatrixBodyForAgent,
resolveMatrixInboundSenderLabel,
resolveMatrixSenderUsername,
} from "./inbound-body.js";
describe("resolveMatrixSenderUsername", () => {
it("extracts localpart without leading @", () => {
expect(resolveMatrixSenderUsername("@bu:matrix.example.org")).toBe("bu");
});
});
describe("resolveMatrixInboundSenderLabel", () => {
it("uses provided senderUsername when present", () => {
expect(
resolveMatrixInboundSenderLabel({
senderName: "Bu",
senderId: "@bu:matrix.example.org",
senderUsername: "BU_CUSTOM",
}),
).toBe("Bu (BU_CUSTOM)");
});
it("includes sender username when it differs from display name", () => {
expect(
resolveMatrixInboundSenderLabel({
@@ -36,8 +56,7 @@ describe("resolveMatrixBodyForAgent", () => {
resolveMatrixBodyForAgent({
isDirectMessage: true,
bodyText: "show me my commits",
senderName: "Bu",
senderId: "@bu:matrix.example.org",
senderLabel: "Bu (bu)",
}),
).toBe("show me my commits");
});
@@ -47,8 +66,7 @@ describe("resolveMatrixBodyForAgent", () => {
resolveMatrixBodyForAgent({
isDirectMessage: false,
bodyText: "show me my commits",
senderName: "Bu",
senderId: "@bu:matrix.example.org",
senderLabel: "Bu (bu)",
}),
).toBe("Bu (bu): show me my commits");
});

View File

@@ -1,4 +1,4 @@
function resolveMatrixSenderUsername(senderId: string): string | undefined {
export function resolveMatrixSenderUsername(senderId: string): string | undefined {
const username = senderId.split(":")[0]?.replace(/^@/, "").trim();
return username ? username : undefined;
}
@@ -6,9 +6,10 @@ function resolveMatrixSenderUsername(senderId: string): string | undefined {
export function resolveMatrixInboundSenderLabel(params: {
senderName: string;
senderId: string;
senderUsername?: string;
}): string {
const senderName = params.senderName.trim();
const senderUsername = resolveMatrixSenderUsername(params.senderId);
const senderUsername = params.senderUsername ?? resolveMatrixSenderUsername(params.senderId);
if (senderName && senderUsername && senderName !== senderUsername) {
return `${senderName} (${senderUsername})`;
}
@@ -18,15 +19,10 @@ export function resolveMatrixInboundSenderLabel(params: {
export function resolveMatrixBodyForAgent(params: {
isDirectMessage: boolean;
bodyText: string;
senderName: string;
senderId: string;
senderLabel: string;
}): string {
if (params.isDirectMessage) {
return params.bodyText;
}
const senderLabel = resolveMatrixInboundSenderLabel({
senderName: params.senderName,
senderId: params.senderId,
});
return `${senderLabel}: ${params.bodyText}`;
return `${params.senderLabel}: ${params.bodyText}`;
}