fix(matrix): preserve sender labels in Matrix BodyForAgent

This commit is contained in:
Peter Steinberger
2026-02-26 20:49:30 +01:00
parent 4cb4053993
commit 01b4f42f9a
4 changed files with 100 additions and 3 deletions

View File

@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
- Queue/Drain/Cron reliability: harden lane draining with guaranteed `draining` flag reset on synchronous pump failures, reject new queue enqueues during gateway restart drain windows (instead of silently killing accepted tasks), add `/stop` queued-backlog cutoff metadata with stale-message skipping (while avoiding cross-session native-stop cutoff bleed), and raise isolated cron `agentTurn` outer safety timeout to avoid false 10-minute timeout races against longer agent session timeouts. (#27407, #27332, #27427)
- Gateway shared-auth scopes: preserve requested operator scopes for shared-token clients when device identity is unavailable, instead of clearing scopes during auth handling. Landed from contributor PR #27498 by @kevinWangSheng. (#27494)
- NO_REPLY suppression: suppress `NO_REPLY` before Slack API send and in sub-agent announce completion flow so sentinel text no longer leaks into user channels. Landed from contributor PRs #27529 (by @Sid-Qin) and #27535 (rewritten minimal landing by maintainers). (#27387, #27531)
- Matrix/Group sender identity: preserve sender labels in Matrix group inbound prompt text (`BodyForAgent`) for both channel and threaded messages, and align group envelopes with shared inbound sender-prefix formatting so first-person requests resolve against the current sender. (#27401) thanks @koushikxd.
- Auto-reply/Streaming: suppress only exact `NO_REPLY` final replies while still filtering streaming partial sentinel fragments (`NO_`, `NO_RE`, `HEARTBEAT_...`) so substantive replies ending with `NO_REPLY` are delivered and partial silent tokens do not leak during streaming. (#19576) Thanks @aldoeliacim.
- Auto-reply/Inbound metadata: add a readable `timestamp` field to conversation info and ignore invalid/out-of-range timestamp values so prompt assembly never crashes on malformed timestamp inputs. (#17017) thanks @liuy.
- Typing/Main reply pipeline: always mark dispatch idle in `agent-runner` finalization so typing cleanup runs even when dispatcher `onIdle` does not fire, preventing stuck typing indicators after run completion. (#27250) Thanks @Sid-Qin.

View File

@@ -26,6 +26,7 @@ import {
resolveMatrixAllowListMatch,
resolveMatrixAllowListMatches,
} from "./allowlist.js";
import { resolveMatrixBodyForAgent } from "./inbound-body.js";
import { resolveMatrixLocation, type MatrixLocationPayload } from "./location.js";
import { downloadMatrixMedia } from "./media.js";
import { resolveMentions } from "./mentions.js";
@@ -215,6 +216,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
}
const senderName = await getMemberDisplayName(roomId, senderId);
const senderUsername = senderId.split(":")[0]?.replace(/^@/, "");
const storeAllowFrom = isDirectMessage
? await readStoreAllowFromForDmPolicy({
provider: "matrix",
@@ -521,19 +523,26 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
storePath,
sessionKey: route.sessionKey,
});
const body = core.channel.reply.formatAgentEnvelope({
const body = core.channel.reply.formatInboundEnvelope({
channel: "Matrix",
from: envelopeFrom,
timestamp: eventTs ?? undefined,
previousTimestamp,
envelope: envelopeOptions,
body: textWithId,
chatType: isDirectMessage ? "direct" : "channel",
sender: { name: senderName, username: senderUsername },
});
const groupSystemPrompt = roomConfig?.systemPrompt?.trim() || undefined;
const ctxPayload = core.channel.reply.finalizeInboundContext({
Body: body,
BodyForAgent: bodyText,
BodyForAgent: resolveMatrixBodyForAgent({
isDirectMessage,
bodyText,
senderName,
senderId,
}),
RawBody: bodyText,
CommandBody: bodyText,
From: isDirectMessage ? `matrix:${senderId}` : `matrix:channel:${roomId}`,
@@ -544,7 +553,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
ConversationLabel: envelopeFrom,
SenderName: senderName,
SenderId: senderId,
SenderUsername: senderId.split(":")[0]?.replace(/^@/, ""),
SenderUsername: senderUsername,
GroupSubject: isRoom ? (roomName ?? roomId) : undefined,
GroupChannel: isRoom ? (roomInfo.canonicalAlias ?? roomId) : undefined,
GroupSystemPrompt: isRoom ? groupSystemPrompt : undefined,

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { resolveMatrixBodyForAgent, resolveMatrixInboundSenderLabel } from "./inbound-body.js";
describe("resolveMatrixInboundSenderLabel", () => {
it("includes sender username when it differs from display name", () => {
expect(
resolveMatrixInboundSenderLabel({
senderName: "Bu",
senderId: "@bu:matrix.example.org",
}),
).toBe("Bu (bu)");
});
it("falls back to sender username when display name is blank", () => {
expect(
resolveMatrixInboundSenderLabel({
senderName: " ",
senderId: "@zhang:matrix.example.org",
}),
).toBe("zhang");
});
it("falls back to sender id when username cannot be parsed", () => {
expect(
resolveMatrixInboundSenderLabel({
senderName: "",
senderId: "matrix-user-without-colon",
}),
).toBe("matrix-user-without-colon");
});
});
describe("resolveMatrixBodyForAgent", () => {
it("keeps direct message body unchanged", () => {
expect(
resolveMatrixBodyForAgent({
isDirectMessage: true,
bodyText: "show me my commits",
senderName: "Bu",
senderId: "@bu:matrix.example.org",
}),
).toBe("show me my commits");
});
it("prefixes non-direct message body with sender label", () => {
expect(
resolveMatrixBodyForAgent({
isDirectMessage: false,
bodyText: "show me my commits",
senderName: "Bu",
senderId: "@bu:matrix.example.org",
}),
).toBe("Bu (bu): show me my commits");
});
});

View File

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