From 5caa5a047e3f98e1a87ca083a499c2f92dcbc1ac Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Thu, 26 Feb 2026 21:41:56 +0100 Subject: [PATCH] Prevent historical Teams chat replay as fresh transcript input. Add chat warmup guard and key-based deduplication so old thread messages are not re-emitted with new timestamps after join. Made-with: Cursor --- src/bot/chatProcedure.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/bot/chatProcedure.ts b/src/bot/chatProcedure.ts index 44c44f5..20ff466 100644 --- a/src/bot/chatProcedure.ts +++ b/src/bot/chatProcedure.ts @@ -20,6 +20,8 @@ export class ChatProcedure { private _onChatMessage: (entry: ChatMessageEntry) => void; private _isSubscribed: boolean = false; private _lastMessageText: string = ''; + private _chatReadyAtMs: number = 0; + private _recentMessageKeyTimestamps: Map = new Map(); constructor( page: Page, @@ -119,6 +121,7 @@ export class ChatProcedure { speaker: string; text: string; timestamp: string; + messageKey?: string; }) => { this._handleChatMessage(msg); }); @@ -204,6 +207,7 @@ export class ChatProcedure { speaker: author, text, timestamp: new Date().toISOString(), + messageKey: `${author}::${text}`, }); return true; } @@ -239,6 +243,7 @@ export class ChatProcedure { speaker: candidateName, text: candidateBody, timestamp: new Date().toISOString(), + messageKey: `${candidateName}::${candidateBody}`, }); return true; } @@ -320,16 +325,32 @@ export class ChatProcedure { }); this._logger.info(`Chat MutationObserver set up (target: ${chatObserverTarget})`); + // Guard against Teams replaying historical thread entries as fresh mutations. + this._chatReadyAtMs = Date.now() + 12000; + this._logger.info('Chat monitor warmup active for 12s'); } /** * Handle a chat message event from the browser. */ - private _handleChatMessage(msg: { speaker: string; text: string; timestamp: string }): void { + private _handleChatMessage(msg: { speaker: string; text: string; timestamp: string; messageKey?: string }): void { if (!this._isSubscribed || !msg.text) return; + const nowMs = Date.now(); + if (this._chatReadyAtMs > 0 && nowMs < this._chatReadyAtMs) return; + // Dedup if (msg.text === this._lastMessageText) return; + const key = msg.messageKey || `${msg.speaker}::${msg.text}`; + const lastSeen = this._recentMessageKeyTimestamps.get(key); + if (lastSeen && (nowMs - lastSeen) < 120000) return; + this._recentMessageKeyTimestamps.set(key, nowMs); + if (this._recentMessageKeyTimestamps.size > 400) { + const cutoff = nowMs - 10 * 60 * 1000; + for (const [k, ts] of this._recentMessageKeyTimestamps.entries()) { + if (ts < cutoff) this._recentMessageKeyTimestamps.delete(k); + } + } this._lastMessageText = msg.text; this._logger.info(`Chat: [${msg.speaker}] ${msg.text}`);