diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f988bc4..bc15ed160 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 8c93476e5..088548c57 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -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, diff --git a/extensions/matrix/src/matrix/monitor/inbound-body.test.ts b/extensions/matrix/src/matrix/monitor/inbound-body.test.ts new file mode 100644 index 000000000..de54e0e65 --- /dev/null +++ b/extensions/matrix/src/matrix/monitor/inbound-body.test.ts @@ -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"); + }); +}); diff --git a/extensions/matrix/src/matrix/monitor/inbound-body.ts b/extensions/matrix/src/matrix/monitor/inbound-body.ts new file mode 100644 index 000000000..65b67417e --- /dev/null +++ b/extensions/matrix/src/matrix/monitor/inbound-body.ts @@ -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}`; +}