diff --git a/extensions/msteams/src/conversation-store-fs.test.ts b/extensions/msteams/src/conversation-store-fs.test.ts index a301e8a4400..6ab0a1386b4 100644 --- a/extensions/msteams/src/conversation-store-fs.test.ts +++ b/extensions/msteams/src/conversation-store-fs.test.ts @@ -123,6 +123,48 @@ describe("msteams conversation store (fs)", () => { expect(retrieved).not.toBeNull(); expect(retrieved!.timezone).toBe("Europe/London"); }); + + it("prefers the freshest personal conversation when a user has multiple references", async () => { + const stateDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-msteams-store-")); + const store = createMSTeamsConversationStoreFs({ + env: { ...process.env, OPENCLAW_STATE_DIR: stateDir }, + ttlMs: 60_000, + }); + + await store.upsert("a:old-personal", { + conversation: { id: "a:old-personal", conversationType: "personal" }, + channelId: "msteams", + serviceUrl: "https://service.example.com", + user: { id: "old-user", aadObjectId: "shared-aad" }, + }); + + await store.upsert("19:group-chat", { + conversation: { id: "19:group-chat", conversationType: "groupChat" }, + channelId: "msteams", + serviceUrl: "https://service.example.com", + user: { id: "group-user", aadObjectId: "shared-aad" }, + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + await store.upsert("a:new-personal", { + conversation: { id: "a:new-personal", conversationType: "personal" }, + channelId: "msteams", + serviceUrl: "https://service.example.com", + user: { id: "new-user", aadObjectId: "shared-aad" }, + }); + + await expect(store.findByUserId("shared-aad")).resolves.toEqual({ + conversationId: "a:new-personal", + reference: expect.objectContaining({ + conversation: expect.objectContaining({ + id: "a:new-personal", + conversationType: "personal", + }), + user: expect.objectContaining({ id: "new-user", aadObjectId: "shared-aad" }), + }), + }); + }); }); describe("msteams conversation store (memory)", () => { diff --git a/extensions/msteams/src/conversation-store-fs.ts b/extensions/msteams/src/conversation-store-fs.ts index 9f49d8eea6a..01baae35056 100644 --- a/extensions/msteams/src/conversation-store-fs.ts +++ b/extensions/msteams/src/conversation-store-fs.ts @@ -118,16 +118,34 @@ export function createMSTeamsConversationStoreFs(params?: { if (!target) { return null; } + + const matches: MSTeamsConversationStoreEntry[] = []; for (const entry of await list()) { const { conversationId, reference } = entry; - if (reference.user?.aadObjectId === target) { - return { conversationId, reference }; - } - if (reference.user?.id === target) { - return { conversationId, reference }; + if (reference.user?.aadObjectId === target || reference.user?.id === target) { + matches.push({ conversationId, reference }); } } - return null; + + if (matches.length === 0) { + return null; + } + + matches.sort((a, b) => { + const aType = a.reference.conversation?.conversationType?.toLowerCase() ?? ""; + const bType = b.reference.conversation?.conversationType?.toLowerCase() ?? ""; + const aPersonal = aType === "personal" ? 1 : 0; + const bPersonal = bType === "personal" ? 1 : 0; + if (aPersonal !== bPersonal) { + return bPersonal - aPersonal; + } + return ( + (parseTimestamp(b.reference.lastSeenAt) ?? 0) - + (parseTimestamp(a.reference.lastSeenAt) ?? 0) + ); + }); + + return matches[0] ?? null; }; const upsert = async ( diff --git a/extensions/msteams/src/conversation-store.ts b/extensions/msteams/src/conversation-store.ts index 8c49e92aed4..a3804c63be6 100644 --- a/extensions/msteams/src/conversation-store.ts +++ b/extensions/msteams/src/conversation-store.ts @@ -7,6 +7,8 @@ /** Minimal ConversationReference shape for proactive messaging */ export type StoredConversationReference = { + /** Timestamp when this reference was last seen/updated. */ + lastSeenAt?: string; /** Activity ID from the last message */ activityId?: string; /** User who sent the message */