Skip to content

Commit dd2faa3

Browse files
authored
fix(msteams): persist conversation reference during DM pairing (#60432)
* fix(msteams): persist conversation reference during DM pairing (#43323) * ci: retrigger checks --------- Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
1 parent 06c6ff6 commit dd2faa3

2 files changed

Lines changed: 84 additions & 27 deletions

File tree

extensions/msteams/src/monitor-handler/message-handler.authz.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ describe("msteams monitor handler authz", () => {
235235
});
236236

237237
it("keeps the DM pairing path wired through shared access resolution", async () => {
238-
const { deps, upsertPairingRequest } = createDeps({
238+
const { conversationStore, deps, upsertPairingRequest, recordInboundSession } = createDeps({
239239
channels: {
240240
msteams: {
241241
dmPolicy: "pairing",
@@ -262,8 +262,18 @@ describe("msteams monitor handler authz", () => {
262262
conversation: {
263263
id: "a:personal-chat",
264264
conversationType: "personal",
265+
tenantId: "tenant-1",
265266
},
267+
channelId: "msteams",
268+
serviceUrl: "https://smba.trafficmanager.net/amer/",
269+
locale: "en-US",
266270
channelData: {},
271+
entities: [
272+
{
273+
type: "clientInfo",
274+
timezone: "America/New_York",
275+
},
276+
],
267277
attachments: [],
268278
},
269279
sendActivity: vi.fn(async () => undefined),
@@ -275,6 +285,33 @@ describe("msteams monitor handler authz", () => {
275285
id: "new-user-aad",
276286
meta: { name: "New User" },
277287
});
288+
expect(conversationStore.upsert).toHaveBeenCalledWith("a:personal-chat", {
289+
activityId: "msg-pairing",
290+
user: {
291+
id: "new-user-id",
292+
aadObjectId: "new-user-aad",
293+
name: "New User",
294+
},
295+
agent: {
296+
id: "bot-id",
297+
name: "Bot",
298+
},
299+
bot: {
300+
id: "bot-id",
301+
name: "Bot",
302+
},
303+
conversation: {
304+
id: "a:personal-chat",
305+
conversationType: "personal",
306+
tenantId: "tenant-1",
307+
},
308+
channelId: "msteams",
309+
serviceUrl: "https://smba.trafficmanager.net/amer/",
310+
locale: "en-US",
311+
timezone: "America/New_York",
312+
});
313+
expect(recordInboundSession).not.toHaveBeenCalled();
314+
expect(runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher).not.toHaveBeenCalled();
278315
});
279316

280317
it("logs an info drop reason when dmPolicy allowlist rejects a sender", async () => {

extensions/msteams/src/monitor-handler/message-handler.ts

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,37 @@ import { recordMSTeamsSentMessage, wasMSTeamsMessageSent } from "../sent-message
5252
import { resolveMSTeamsSenderAccess } from "./access.js";
5353
import { resolveMSTeamsInboundMedia } from "./inbound-media.js";
5454

55+
function buildStoredConversationReference(params: {
56+
activity: MSTeamsTurnContext["activity"];
57+
conversationId: string;
58+
conversationType: string;
59+
teamId?: string;
60+
}): StoredConversationReference {
61+
const { activity, conversationId, conversationType, teamId } = params;
62+
const from = activity.from;
63+
const conversation = activity.conversation;
64+
const agent = activity.recipient;
65+
const clientInfo = activity.entities?.find((e) => e.type === "clientInfo") as
66+
| { timezone?: string }
67+
| undefined;
68+
return {
69+
activityId: activity.id,
70+
user: from ? { id: from.id, name: from.name, aadObjectId: from.aadObjectId } : undefined,
71+
agent,
72+
bot: agent ? { id: agent.id, name: agent.name } : undefined,
73+
conversation: {
74+
id: conversationId,
75+
conversationType,
76+
tenantId: conversation?.tenantId,
77+
},
78+
teamId,
79+
channelId: activity.channelId,
80+
serviceUrl: activity.serviceUrl,
81+
locale: activity.locale,
82+
...(clientInfo?.timezone ? { timezone: clientInfo.timezone } : {}),
83+
};
84+
}
85+
5586
export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
5687
const {
5788
cfg,
@@ -140,6 +171,12 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
140171
const conversationMessageId = extractMSTeamsConversationMessageId(rawConversationId);
141172
const conversationType = conversation?.conversationType ?? "personal";
142173
const teamId = activity.channelData?.team?.id;
174+
const conversationRef = buildStoredConversationReference({
175+
activity,
176+
conversationId,
177+
conversationType,
178+
teamId,
179+
});
143180

144181
const {
145182
dmPolicy,
@@ -177,6 +214,11 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
177214
allowNameMatching,
178215
});
179216
if (access.decision === "pairing") {
217+
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
218+
log.debug?.("failed to save conversation reference", {
219+
error: formatUnknownError(err),
220+
});
221+
});
180222
const request = await pairing.upsertPairingRequest({
181223
id: senderId,
182224
meta: { name: senderName },
@@ -306,30 +348,6 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
306348
return;
307349
}
308350

309-
// Extract clientInfo entity (Teams sends this on every activity with timezone, locale, etc.)
310-
const clientInfo = activity.entities?.find((e) => e.type === "clientInfo") as
311-
| { timezone?: string; locale?: string; country?: string; platform?: string }
312-
| undefined;
313-
314-
// Build conversation reference for proactive replies.
315-
const agent = activity.recipient;
316-
const conversationRef: StoredConversationReference = {
317-
activityId: activity.id,
318-
user: { id: from.id, name: from.name, aadObjectId: from.aadObjectId },
319-
agent,
320-
bot: agent ? { id: agent.id, name: agent.name } : undefined,
321-
conversation: {
322-
id: conversationId,
323-
conversationType,
324-
tenantId: conversation?.tenantId,
325-
},
326-
teamId,
327-
channelId: activity.channelId,
328-
serviceUrl: activity.serviceUrl,
329-
locale: activity.locale,
330-
// Only set timezone if present (preserve previously stored value on next upsert)
331-
...(clientInfo?.timezone ? { timezone: clientInfo.timezone } : {}),
332-
};
333351
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
334352
log.debug?.("failed to save conversation reference", {
335353
error: formatUnknownError(err),
@@ -642,8 +660,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
642660
// Use Teams clientInfo timezone if no explicit userTimezone is configured.
643661
// This ensures the agent knows the sender's timezone for time-aware responses
644662
// and proactive sends within the same session.
645-
// Apply Teams clientInfo timezone if no explicit userTimezone is configured.
646-
const senderTimezone = clientInfo?.timezone || conversationRef.timezone;
663+
const activityClientInfo = activity.entities?.find((e) => e.type === "clientInfo") as
664+
| { timezone?: string }
665+
| undefined;
666+
const senderTimezone = activityClientInfo?.timezone || conversationRef.timezone;
647667
const configOverride =
648668
senderTimezone && !cfg.agents?.defaults?.userTimezone
649669
? {

0 commit comments

Comments
 (0)