diff --git a/CHANGELOG.md b/CHANGELOG.md index fbe24cc2366..8d1c4c0ccc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai - Daemon/Linux: stop flagging non-gateway systemd services as duplicate gateways just because their unit files mention OpenClaw, reducing false-positive doctor/log noise. (#45328) Thanks @gregretkowski. - Feishu: close WebSocket connections on monitor stop/abort so ghost connections no longer persist, preventing duplicate event processing and resource leaks across restart cycles. (#52844) Thanks @schumilin. - Feishu: use the original message `create_time` instead of `Date.now()` for inbound timestamps so offline-retried messages carry the correct authoring time, preventing mis-targeted agent actions on stale instructions. (#52809) Thanks @schumilin. +- Matrix/replies: include quoted poll question/options in inbound reply context so the agent sees the original poll content when users reply to Matrix poll messages. (#55056) Thanks @alberthild. - Agents/sandbox: honor `tools.sandbox.tools.alsoAllow`, let explicit sandbox re-allows remove matching built-in default-deny tools, and keep sandbox explain/error guidance aligned with the effective sandbox tool policy. (#54492) Thanks @ngutman. - Agents/sandbox: make blocked-tool guidance glob-aware again, redact/sanitize session-specific explain hints for safer copy-paste, and avoid leaking control-character session keys in those hints. (#54684) Thanks @ngutman. - Agents/compaction: trigger timeout recovery compaction before retrying high-context LLM timeouts so embedded runs stop repeating oversized requests. (#46417) thanks @joeykrug. diff --git a/extensions/matrix/src/matrix/monitor/context-summary.ts b/extensions/matrix/src/matrix/monitor/context-summary.ts new file mode 100644 index 00000000000..22f98dd010a --- /dev/null +++ b/extensions/matrix/src/matrix/monitor/context-summary.ts @@ -0,0 +1,43 @@ +import { + formatMatrixMessageText, + resolveMatrixMessageAttachment, + resolveMatrixMessageBody, +} from "../media-text.js"; +import { + formatPollAsText, + isPollStartType, + parsePollStartContent, + type PollStartContent, +} from "../poll-types.js"; +import type { MatrixRawEvent } from "./types.js"; + +export function trimMatrixMaybeString(value: unknown): string | undefined { + if (typeof value !== "string") { + return undefined; + } + const trimmed = value.trim(); + return trimmed || undefined; +} + +export function summarizeMatrixMessageContextEvent(event: MatrixRawEvent): string | undefined { + if (isPollStartType(event.type)) { + const pollSummary = parsePollStartContent(event.content as PollStartContent); + if (pollSummary) { + return formatPollAsText(pollSummary); + } + } + + const content = event.content as { body?: unknown; filename?: unknown; msgtype?: unknown }; + return formatMatrixMessageText({ + body: resolveMatrixMessageBody({ + body: trimMatrixMaybeString(content.body), + filename: trimMatrixMaybeString(content.filename), + msgtype: trimMatrixMaybeString(content.msgtype), + }), + attachment: resolveMatrixMessageAttachment({ + body: trimMatrixMaybeString(content.body), + filename: trimMatrixMaybeString(content.filename), + msgtype: trimMatrixMaybeString(content.msgtype), + }), + }); +} diff --git a/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts b/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts index 8d6b050ece6..cbfb5eb1c7d 100644 --- a/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts @@ -123,4 +123,118 @@ describe("createMatrixRoomMessageHandler inbound body formatting", () => { }), ); }); + + it("records reply context for quoted poll start events inside always-threaded replies", async () => { + const { handler, finalizeInboundContext } = createMatrixHandlerTestHarness({ + client: { + getEvent: async (_roomId: string, eventId: string) => { + if (eventId === "$thread-root") { + return createMatrixTextMessageEvent({ + eventId: "$thread-root", + sender: "@bob:example.org", + body: "Root topic", + }); + } + + return { + event_id: "$poll", + sender: "@alice:example.org", + type: "m.poll.start", + origin_server_ts: 1, + content: { + "m.poll.start": { + question: { "m.text": "Lunch?" }, + kind: "m.poll.disclosed", + max_selections: 1, + answers: [ + { id: "a1", "m.text": "Pizza" }, + { id: "a2", "m.text": "Sushi" }, + ], + }, + }, + } satisfies MatrixRawEvent; + }, + } as unknown as Partial, + isDirectMessage: false, + threadReplies: "always", + getMemberDisplayName: async (_roomId, userId) => { + if (userId === "@alice:example.org") { + return "Alice"; + } + if (userId === "@bob:example.org") { + return "Bob"; + } + return "sender"; + }, + }); + + await handler( + "!room:example.org", + createMatrixTextMessageEvent({ + eventId: "$reply1", + body: "@room follow up", + relatesTo: { + rel_type: "m.thread", + event_id: "$thread-root", + "m.in_reply_to": { event_id: "$poll" }, + }, + mentions: { room: true }, + }), + ); + + expect(finalizeInboundContext).toHaveBeenCalledWith( + expect.objectContaining({ + MessageThreadId: "$thread-root", + ReplyToId: undefined, + ReplyToSender: "Alice", + ReplyToBody: "[Poll]\nLunch?\n\n1. Pizza\n2. Sushi", + ThreadStarterBody: "Matrix thread root $thread-root from Bob:\nRoot topic", + }), + ); + }); + + it("reuses the fetched thread root when reply context points at the same event", async () => { + const getEvent = vi.fn(async () => + createMatrixTextMessageEvent({ + eventId: "$thread-root", + sender: "@alice:example.org", + body: "Root topic", + }), + ); + const getMemberDisplayName = vi.fn(async (_roomId: string, userId: string) => + userId === "@alice:example.org" ? "Alice" : "sender", + ); + const { handler, finalizeInboundContext } = createMatrixHandlerTestHarness({ + client: { getEvent }, + isDirectMessage: false, + threadReplies: "always", + getMemberDisplayName, + }); + + await handler( + "!room:example.org", + createMatrixTextMessageEvent({ + eventId: "$reply1", + body: "@room follow up", + relatesTo: { + rel_type: "m.thread", + event_id: "$thread-root", + "m.in_reply_to": { event_id: "$thread-root" }, + }, + mentions: { room: true }, + }), + ); + + expect(finalizeInboundContext).toHaveBeenCalledWith( + expect.objectContaining({ + MessageThreadId: "$thread-root", + ReplyToId: undefined, + ReplyToSender: "Alice", + ReplyToBody: "Root topic", + ThreadStarterBody: "Matrix thread root $thread-root from Alice:\nRoot topic", + }), + ); + expect(getEvent).toHaveBeenCalledTimes(1); + expect(getMemberDisplayName).toHaveBeenCalledTimes(2); + }); }); diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 3df9f90c859..7bd0626ee93 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -37,6 +37,7 @@ import { downloadMatrixMedia } from "./media.js"; import { resolveMentions } from "./mentions.js"; import { handleInboundMatrixReaction } from "./reaction-events.js"; import { deliverMatrixReplies } from "./replies.js"; +import { createMatrixReplyContextResolver } from "./reply-context.js"; import { resolveMatrixRoomConfig } from "./rooms.js"; import { resolveMatrixInboundRoute } from "./route.js"; import { createMatrixThreadContextResolver } from "./thread-context.js"; @@ -182,6 +183,11 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam getMemberDisplayName, logVerboseMessage, }); + const resolveReplyContext = createMatrixReplyContextResolver({ + client, + getMemberDisplayName, + logVerboseMessage, + }); const readStoreAllowFrom = async (): Promise => { const now = Date.now(); @@ -707,6 +713,20 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam ? await resolveThreadContext({ roomId, threadRootId: _threadRootId }) : undefined; + // Resolve the body and sender of the replied-to message so the agent + // can see what is being replied to, not just the event ID. + // Note: resolve even when threadTarget is set (e.g. threadReplies: "always") + // because the user may still be quoting a specific message within the thread. + const replyContext = + replyToEventId && replyToEventId === _threadRootId && threadContext?.summary + ? { + replyToBody: threadContext.summary, + replyToSender: threadContext.senderLabel, + } + : replyToEventId + ? await resolveReplyContext({ roomId, eventId: replyToEventId }) + : undefined; + if (_configuredBinding) { const ensured = await ensureConfiguredAcpBindingReady({ cfg, @@ -766,6 +786,8 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam WasMentioned: isRoom ? wasMentioned : undefined, MessageSid: _messageId, ReplyToId: threadTarget ? undefined : (replyToEventId ?? undefined), + ReplyToBody: replyContext?.replyToBody, + ReplyToSender: replyContext?.replyToSender, MessageThreadId: threadTarget, ThreadStarterBody: threadContext?.threadStarterBody, Timestamp: eventTs ?? undefined, diff --git a/extensions/matrix/src/matrix/monitor/reply-context.test.ts b/extensions/matrix/src/matrix/monitor/reply-context.test.ts new file mode 100644 index 00000000000..9c5b4a69eff --- /dev/null +++ b/extensions/matrix/src/matrix/monitor/reply-context.test.ts @@ -0,0 +1,273 @@ +import { describe, expect, it, vi } from "vitest"; +import { createMatrixReplyContextResolver, summarizeMatrixReplyEvent } from "./reply-context.js"; +import type { MatrixRawEvent } from "./types.js"; + +describe("matrix reply context", () => { + it("summarizes reply events from body text", () => { + expect( + summarizeMatrixReplyEvent({ + event_id: "$original", + sender: "@alice:example.org", + type: "m.room.message", + origin_server_ts: Date.now(), + content: { + msgtype: "m.text", + body: " Some quoted message ", + }, + } as MatrixRawEvent), + ).toBe("Some quoted message"); + }); + + it("truncates long reply bodies", () => { + const longBody = "x".repeat(600); + const result = summarizeMatrixReplyEvent({ + event_id: "$original", + sender: "@alice:example.org", + type: "m.room.message", + origin_server_ts: Date.now(), + content: { + msgtype: "m.text", + body: longBody, + }, + } as MatrixRawEvent); + expect(result).toBeDefined(); + expect(result!.length).toBeLessThanOrEqual(500); + expect(result!.endsWith("...")).toBe(true); + }); + + it("handles media-only reply events", () => { + expect( + summarizeMatrixReplyEvent({ + event_id: "$original", + sender: "@alice:example.org", + type: "m.room.message", + origin_server_ts: Date.now(), + content: { + msgtype: "m.image", + body: "photo.jpg", + }, + } as MatrixRawEvent), + ).toBe("[matrix image attachment]"); + }); + + it("summarizes poll start events from poll content", () => { + expect( + summarizeMatrixReplyEvent({ + event_id: "$poll", + sender: "@alice:example.org", + type: "m.poll.start", + origin_server_ts: Date.now(), + content: { + "m.poll.start": { + question: { "m.text": "Lunch?" }, + kind: "m.poll.disclosed", + max_selections: 1, + answers: [ + { id: "a1", "m.text": "Pizza" }, + { id: "a2", "m.text": "Sushi" }, + ], + }, + }, + } as MatrixRawEvent), + ).toBe("[Poll]\nLunch?\n\n1. Pizza\n2. Sushi"); + }); + + it("resolves and caches reply context", async () => { + const getEvent = vi.fn(async () => ({ + event_id: "$original", + sender: "@alice:example.org", + type: "m.room.message", + origin_server_ts: Date.now(), + content: { + msgtype: "m.text", + body: "This is the original message", + }, + })); + const getMemberDisplayName = vi.fn(async () => "Alice"); + const resolveReplyContext = createMatrixReplyContextResolver({ + client: { + getEvent, + } as never, + getMemberDisplayName, + logVerboseMessage: () => {}, + }); + + const result = await resolveReplyContext({ + roomId: "!room:example.org", + eventId: "$original", + }); + + expect(result).toEqual({ + replyToBody: "This is the original message", + replyToSender: "Alice", + }); + + // Second call should use cache + await resolveReplyContext({ + roomId: "!room:example.org", + eventId: "$original", + }); + + expect(getEvent).toHaveBeenCalledTimes(1); + expect(getMemberDisplayName).toHaveBeenCalledTimes(1); + }); + + it("returns empty context when event fetch fails", async () => { + const getEvent = vi.fn().mockRejectedValueOnce(new Error("not found")); + const getMemberDisplayName = vi.fn(async () => "Alice"); + const resolveReplyContext = createMatrixReplyContextResolver({ + client: { + getEvent, + } as never, + getMemberDisplayName, + logVerboseMessage: () => {}, + }); + + const result = await resolveReplyContext({ + roomId: "!room:example.org", + eventId: "$missing", + }); + + expect(result).toEqual({}); + }); + + it("returns empty context for redacted events", async () => { + const getEvent = vi.fn(async () => ({ + event_id: "$redacted", + sender: "@alice:example.org", + type: "m.room.message", + origin_server_ts: Date.now(), + unsigned: { + redacted_because: { type: "m.room.redaction" }, + }, + content: {}, + })); + const getMemberDisplayName = vi.fn(async () => "Alice"); + const resolveReplyContext = createMatrixReplyContextResolver({ + client: { + getEvent, + } as never, + getMemberDisplayName, + logVerboseMessage: () => {}, + }); + + const result = await resolveReplyContext({ + roomId: "!room:example.org", + eventId: "$redacted", + }); + + expect(result).toEqual({}); + expect(getMemberDisplayName).not.toHaveBeenCalled(); + }); + + it("does not cache fetch failures so retries can succeed", async () => { + const getEvent = vi + .fn() + .mockRejectedValueOnce(new Error("temporary failure")) + .mockResolvedValueOnce({ + event_id: "$original", + sender: "@bob:example.org", + type: "m.room.message", + origin_server_ts: Date.now(), + content: { + msgtype: "m.text", + body: "Recovered message", + }, + }); + const getMemberDisplayName = vi.fn(async () => "Bob"); + const resolveReplyContext = createMatrixReplyContextResolver({ + client: { + getEvent, + } as never, + getMemberDisplayName, + logVerboseMessage: () => {}, + }); + + // First call fails + const first = await resolveReplyContext({ + roomId: "!room:example.org", + eventId: "$original", + }); + expect(first).toEqual({}); + + // Second call succeeds (should retry, not use cached failure) + const second = await resolveReplyContext({ + roomId: "!room:example.org", + eventId: "$original", + }); + expect(second).toEqual({ + replyToBody: "Recovered message", + replyToSender: "Bob", + }); + + expect(getEvent).toHaveBeenCalledTimes(2); + }); + + it("falls back to senderId when display name resolution fails", async () => { + const getEvent = vi.fn(async () => ({ + event_id: "$original", + sender: "@charlie:example.org", + type: "m.room.message", + origin_server_ts: Date.now(), + content: { + msgtype: "m.text", + body: "Hello", + }, + })); + const getMemberDisplayName = vi.fn().mockRejectedValueOnce(new Error("unknown member")); + const resolveReplyContext = createMatrixReplyContextResolver({ + client: { + getEvent, + } as never, + getMemberDisplayName, + logVerboseMessage: () => {}, + }); + + const result = await resolveReplyContext({ + roomId: "!room:example.org", + eventId: "$original", + }); + + expect(result).toEqual({ + replyToBody: "Hello", + replyToSender: "@charlie:example.org", + }); + }); + + it("uses LRU eviction — recently accessed entries survive over older ones", async () => { + let callCount = 0; + const getEvent = vi.fn().mockImplementation((_roomId: string, eventId: string) => { + callCount++; + return Promise.resolve({ + event_id: eventId, + sender: `@user${callCount}:example.org`, + type: "m.room.message", + origin_server_ts: Date.now(), + content: { msgtype: "m.text", body: `msg-${eventId}` }, + }); + }); + const getMemberDisplayName = vi + .fn() + .mockImplementation((_r: string, userId: string) => Promise.resolve(userId)); + + // Use a small cache by testing the eviction pattern: + // The actual MAX_CACHED_REPLY_CONTEXTS is 256. We cannot override it easily, + // but we can verify that a cache hit reorders entries (delete + re-insert). + const resolveReplyContext = createMatrixReplyContextResolver({ + client: { getEvent } as never, + getMemberDisplayName, + logVerboseMessage: () => {}, + }); + + // Populate cache with two entries + await resolveReplyContext({ roomId: "!r:e", eventId: "$A" }); + await resolveReplyContext({ roomId: "!r:e", eventId: "$B" }); + expect(getEvent).toHaveBeenCalledTimes(2); + + // Access $A again — should be a cache hit (no new getEvent call) + // and should move $A to the end of the Map for LRU. + const hitResult = await resolveReplyContext({ roomId: "!r:e", eventId: "$A" }); + expect(getEvent).toHaveBeenCalledTimes(2); // Still 2 — cache hit + expect(hitResult.replyToBody).toBe("msg-$A"); + }); +}); diff --git a/extensions/matrix/src/matrix/monitor/reply-context.ts b/extensions/matrix/src/matrix/monitor/reply-context.ts new file mode 100644 index 00000000000..5ae84c1965d --- /dev/null +++ b/extensions/matrix/src/matrix/monitor/reply-context.ts @@ -0,0 +1,90 @@ +import type { MatrixClient } from "../sdk.js"; +import { summarizeMatrixMessageContextEvent, trimMatrixMaybeString } from "./context-summary.js"; +import type { MatrixRawEvent } from "./types.js"; + +const MAX_CACHED_REPLY_CONTEXTS = 256; +const MAX_REPLY_BODY_LENGTH = 500; + +export type MatrixReplyContext = { + replyToBody?: string; + replyToSender?: string; +}; + +function truncateReplyBody(value: string): string { + if (value.length <= MAX_REPLY_BODY_LENGTH) { + return value; + } + return `${value.slice(0, MAX_REPLY_BODY_LENGTH - 3)}...`; +} + +export function summarizeMatrixReplyEvent(event: MatrixRawEvent): string | undefined { + const body = summarizeMatrixMessageContextEvent(event); + return body ? truncateReplyBody(body) : undefined; +} + +/** + * Creates a cached resolver that fetches the body and sender of a replied-to + * Matrix event. This allows the agent to see the content of the message being + * replied to, not just its event ID. + */ +export function createMatrixReplyContextResolver(params: { + client: MatrixClient; + getMemberDisplayName: (roomId: string, userId: string) => Promise; + logVerboseMessage: (message: string) => void; +}) { + const cache = new Map(); + + const remember = (key: string, value: MatrixReplyContext): MatrixReplyContext => { + cache.set(key, value); + if (cache.size > MAX_CACHED_REPLY_CONTEXTS) { + const oldest = cache.keys().next().value; + if (typeof oldest === "string") { + cache.delete(oldest); + } + } + return value; + }; + + return async (input: { roomId: string; eventId: string }): Promise => { + const cacheKey = `${input.roomId}:${input.eventId}`; + const cached = cache.get(cacheKey); + if (cached) { + // Move to end for LRU semantics so frequently accessed entries survive eviction. + cache.delete(cacheKey); + cache.set(cacheKey, cached); + return cached; + } + + const event = await params.client.getEvent(input.roomId, input.eventId).catch((err) => { + params.logVerboseMessage( + `matrix: failed resolving reply context room=${input.roomId} id=${input.eventId}: ${String(err)}`, + ); + return null; + }); + if (!event) { + // Do not cache failures so transient errors can be retried on the next + // message that references the same event. + return {}; + } + + const rawEvent = event as MatrixRawEvent; + if (rawEvent.unsigned?.redacted_because) { + return remember(cacheKey, {}); + } + + const replyToBody = summarizeMatrixReplyEvent(rawEvent); + if (!replyToBody) { + return remember(cacheKey, {}); + } + + const senderId = trimMatrixMaybeString(rawEvent.sender); + const senderName = + senderId && + (await params.getMemberDisplayName(input.roomId, senderId).catch(() => undefined)); + + return remember(cacheKey, { + replyToBody, + replyToSender: senderName ?? senderId, + }); + }; +} diff --git a/extensions/matrix/src/matrix/monitor/thread-context.test.ts b/extensions/matrix/src/matrix/monitor/thread-context.test.ts index 2e1dd16c833..bfd8e8178bd 100644 --- a/extensions/matrix/src/matrix/monitor/thread-context.test.ts +++ b/extensions/matrix/src/matrix/monitor/thread-context.test.ts @@ -63,6 +63,8 @@ describe("matrix thread context", () => { }), ).resolves.toEqual({ threadStarterBody: "Matrix thread root $root from Alice:\nRoot topic", + senderLabel: "Alice", + summary: "Root topic", }); await resolveThreadContext({ @@ -113,9 +115,33 @@ describe("matrix thread context", () => { }), ).resolves.toEqual({ threadStarterBody: "Matrix thread root $root from Alice:\nRecovered topic", + senderLabel: "Alice", + summary: "Recovered topic", }); expect(getEvent).toHaveBeenCalledTimes(2); expect(getMemberDisplayName).toHaveBeenCalledTimes(1); }); + + it("summarizes poll start thread roots from poll content", () => { + expect( + summarizeMatrixThreadStarterEvent({ + event_id: "$root", + sender: "@alice:example.org", + type: "m.poll.start", + origin_server_ts: Date.now(), + content: { + "m.poll.start": { + question: { "m.text": "Lunch?" }, + kind: "m.poll.disclosed", + max_selections: 1, + answers: [ + { id: "a1", "m.text": "Pizza" }, + { id: "a2", "m.text": "Sushi" }, + ], + }, + }, + } as MatrixRawEvent), + ).toBe("[Poll]\nLunch?\n\n1. Pizza\n2. Sushi"); + }); }); diff --git a/extensions/matrix/src/matrix/monitor/thread-context.ts b/extensions/matrix/src/matrix/monitor/thread-context.ts index 9a9fc3a29cc..df4a5a89b0c 100644 --- a/extensions/matrix/src/matrix/monitor/thread-context.ts +++ b/extensions/matrix/src/matrix/monitor/thread-context.ts @@ -1,9 +1,5 @@ -import { - formatMatrixMessageText, - resolveMatrixMessageAttachment, - resolveMatrixMessageBody, -} from "../media-text.js"; import type { MatrixClient } from "../sdk.js"; +import { summarizeMatrixMessageContextEvent, trimMatrixMaybeString } from "./context-summary.js"; import type { MatrixRawEvent } from "./types.js"; const MAX_TRACKED_THREAD_STARTERS = 256; @@ -11,16 +7,10 @@ const MAX_THREAD_STARTER_BODY_LENGTH = 500; type MatrixThreadContext = { threadStarterBody?: string; + senderLabel?: string; + summary?: string; }; -function trimMaybeString(value: unknown): string | undefined { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - return trimmed || undefined; -} - function truncateThreadStarterBody(value: string): string { if (value.length <= MAX_THREAD_STARTER_BODY_LENGTH) { return value; @@ -29,27 +19,16 @@ function truncateThreadStarterBody(value: string): string { } export function summarizeMatrixThreadStarterEvent(event: MatrixRawEvent): string | undefined { - const content = event.content as { body?: unknown; filename?: unknown; msgtype?: unknown }; - const body = formatMatrixMessageText({ - body: resolveMatrixMessageBody({ - body: trimMaybeString(content.body), - filename: trimMaybeString(content.filename), - msgtype: trimMaybeString(content.msgtype), - }), - attachment: resolveMatrixMessageAttachment({ - body: trimMaybeString(content.body), - filename: trimMaybeString(content.filename), - msgtype: trimMaybeString(content.msgtype), - }), - }); + const body = summarizeMatrixMessageContextEvent(event); if (body) { return truncateThreadStarterBody(body); } - const msgtype = trimMaybeString(content.msgtype); + const content = event.content as { msgtype?: unknown }; + const msgtype = trimMatrixMaybeString(content.msgtype); if (msgtype) { return `Matrix ${msgtype} message`; } - const eventType = trimMaybeString(event.type); + const eventType = trimMatrixMaybeString(event.type); return eventType ? `Matrix ${eventType} event` : undefined; } @@ -107,17 +86,21 @@ export function createMatrixThreadContextResolver(params: { } const rawEvent = rootEvent as MatrixRawEvent; - const senderId = trimMaybeString(rawEvent.sender); + const senderId = trimMatrixMaybeString(rawEvent.sender); const senderName = senderId && (await params.getMemberDisplayName(input.roomId, senderId).catch(() => undefined)); + const senderLabel = senderName ?? senderId; + const summary = summarizeMatrixThreadStarterEvent(rawEvent); return remember(cacheKey, { threadStarterBody: formatMatrixThreadStarterBody({ threadRootId: input.threadRootId, senderId, senderName, - summary: summarizeMatrixThreadStarterEvent(rawEvent), + summary, }), + senderLabel, + summary, }); }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fc7de7ae8b..378177b2dd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -480,8 +480,6 @@ importers: specifier: ^1.2.10 version: 1.2.10 - extensions/microsoft-foundry: {} - extensions/minimax: {} extensions/mistral: {}