diff --git a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts index 1770a88027c..aa06a7df97d 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts @@ -235,7 +235,7 @@ describe("msteams monitor handler authz", () => { }); it("keeps the DM pairing path wired through shared access resolution", async () => { - const { deps, upsertPairingRequest } = createDeps({ + const { conversationStore, deps, upsertPairingRequest, recordInboundSession } = createDeps({ channels: { msteams: { dmPolicy: "pairing", @@ -262,8 +262,18 @@ describe("msteams monitor handler authz", () => { conversation: { id: "a:personal-chat", conversationType: "personal", + tenantId: "tenant-1", }, + channelId: "msteams", + serviceUrl: "https://smba.trafficmanager.net/amer/", + locale: "en-US", channelData: {}, + entities: [ + { + type: "clientInfo", + timezone: "America/New_York", + }, + ], attachments: [], }, sendActivity: vi.fn(async () => undefined), @@ -275,6 +285,33 @@ describe("msteams monitor handler authz", () => { id: "new-user-aad", meta: { name: "New User" }, }); + expect(conversationStore.upsert).toHaveBeenCalledWith("a:personal-chat", { + activityId: "msg-pairing", + user: { + id: "new-user-id", + aadObjectId: "new-user-aad", + name: "New User", + }, + agent: { + id: "bot-id", + name: "Bot", + }, + bot: { + id: "bot-id", + name: "Bot", + }, + conversation: { + id: "a:personal-chat", + conversationType: "personal", + tenantId: "tenant-1", + }, + channelId: "msteams", + serviceUrl: "https://smba.trafficmanager.net/amer/", + locale: "en-US", + timezone: "America/New_York", + }); + expect(recordInboundSession).not.toHaveBeenCalled(); + expect(runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher).not.toHaveBeenCalled(); }); it("logs an info drop reason when dmPolicy allowlist rejects a sender", async () => { diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index 48d8289b5a3..948f7a43357 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -52,6 +52,37 @@ import { recordMSTeamsSentMessage, wasMSTeamsMessageSent } from "../sent-message import { resolveMSTeamsSenderAccess } from "./access.js"; import { resolveMSTeamsInboundMedia } from "./inbound-media.js"; +function buildStoredConversationReference(params: { + activity: MSTeamsTurnContext["activity"]; + conversationId: string; + conversationType: string; + teamId?: string; +}): StoredConversationReference { + const { activity, conversationId, conversationType, teamId } = params; + const from = activity.from; + const conversation = activity.conversation; + const agent = activity.recipient; + const clientInfo = activity.entities?.find((e) => e.type === "clientInfo") as + | { timezone?: string } + | undefined; + return { + activityId: activity.id, + user: from ? { id: from.id, name: from.name, aadObjectId: from.aadObjectId } : undefined, + agent, + bot: agent ? { id: agent.id, name: agent.name } : undefined, + conversation: { + id: conversationId, + conversationType, + tenantId: conversation?.tenantId, + }, + teamId, + channelId: activity.channelId, + serviceUrl: activity.serviceUrl, + locale: activity.locale, + ...(clientInfo?.timezone ? { timezone: clientInfo.timezone } : {}), + }; +} + export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { const { cfg, @@ -140,6 +171,12 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { const conversationMessageId = extractMSTeamsConversationMessageId(rawConversationId); const conversationType = conversation?.conversationType ?? "personal"; const teamId = activity.channelData?.team?.id; + const conversationRef = buildStoredConversationReference({ + activity, + conversationId, + conversationType, + teamId, + }); const { dmPolicy, @@ -177,6 +214,11 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { allowNameMatching, }); if (access.decision === "pairing") { + conversationStore.upsert(conversationId, conversationRef).catch((err) => { + log.debug?.("failed to save conversation reference", { + error: formatUnknownError(err), + }); + }); const request = await pairing.upsertPairingRequest({ id: senderId, meta: { name: senderName }, @@ -306,30 +348,6 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { return; } - // Extract clientInfo entity (Teams sends this on every activity with timezone, locale, etc.) - const clientInfo = activity.entities?.find((e) => e.type === "clientInfo") as - | { timezone?: string; locale?: string; country?: string; platform?: string } - | undefined; - - // Build conversation reference for proactive replies. - const agent = activity.recipient; - const conversationRef: StoredConversationReference = { - activityId: activity.id, - user: { id: from.id, name: from.name, aadObjectId: from.aadObjectId }, - agent, - bot: agent ? { id: agent.id, name: agent.name } : undefined, - conversation: { - id: conversationId, - conversationType, - tenantId: conversation?.tenantId, - }, - teamId, - channelId: activity.channelId, - serviceUrl: activity.serviceUrl, - locale: activity.locale, - // Only set timezone if present (preserve previously stored value on next upsert) - ...(clientInfo?.timezone ? { timezone: clientInfo.timezone } : {}), - }; conversationStore.upsert(conversationId, conversationRef).catch((err) => { log.debug?.("failed to save conversation reference", { error: formatUnknownError(err), @@ -642,8 +660,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { // Use Teams clientInfo timezone if no explicit userTimezone is configured. // This ensures the agent knows the sender's timezone for time-aware responses // and proactive sends within the same session. - // Apply Teams clientInfo timezone if no explicit userTimezone is configured. - const senderTimezone = clientInfo?.timezone || conversationRef.timezone; + const activityClientInfo = activity.entities?.find((e) => e.type === "clientInfo") as + | { timezone?: string } + | undefined; + const senderTimezone = activityClientInfo?.timezone || conversationRef.timezone; const configOverride = senderTimezone && !cfg.agents?.defaults?.userTimezone ? {