From 2e2fbfe8ed9617dd75b6effd06e4032f362ca3f0 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Fri, 27 Feb 2026 13:56:20 +0100
Subject: [PATCH] Chat history: extract Teams timestamp, classify pre-join
messages as history, no AI trigger
Made-with: Cursor
---
src/bot/chatProcedure.ts | 65 +++++++++++++++++++++++++++++++++-------
src/bot/orchestrator.ts | 7 +++--
src/types/index.ts | 1 +
3 files changed, 59 insertions(+), 14 deletions(-)
diff --git a/src/bot/chatProcedure.ts b/src/bot/chatProcedure.ts
index 20ff466..580a870 100644
--- a/src/bot/chatProcedure.ts
+++ b/src/bot/chatProcedure.ts
@@ -6,6 +6,7 @@ export interface ChatMessageEntry {
text: string;
timestamp: Date;
source: 'chat';
+ isHistory: boolean;
}
/**
@@ -20,7 +21,7 @@ export class ChatProcedure {
private _onChatMessage: (entry: ChatMessageEntry) => void;
private _isSubscribed: boolean = false;
private _lastMessageText: string = '';
- private _chatReadyAtMs: number = 0;
+ private _botJoinedAtMs: number = 0;
private _recentMessageKeyTimestamps: Map = new Map();
constructor(
@@ -33,6 +34,14 @@ export class ChatProcedure {
this._onChatMessage = onChatMessage;
}
+ /**
+ * Set the bot's join timestamp so we can classify messages as history vs live.
+ */
+ setBotJoinedAt(ms: number): void {
+ this._botJoinedAtMs = ms;
+ this._logger.info(`Chat history cutoff set: messages before ${new Date(ms).toISOString()} are history`);
+ }
+
/**
* Open the chat panel and start monitoring messages.
*/
@@ -121,6 +130,7 @@ export class ChatProcedure {
speaker: string;
text: string;
timestamp: string;
+ teamsTimestamp?: string;
messageKey?: string;
}) => {
this._handleChatMessage(msg);
@@ -156,8 +166,26 @@ export class ChatProcedure {
return noisePatterns.some(p => lower.includes(p));
}
+ function _extractTeamsTimestamp(el: HTMLElement): string | undefined {
+ const timeSelectors = [
+ 'time[datetime]',
+ '[data-tid="message-timestamp"] time',
+ '[data-tid*="timestamp"] time',
+ '[data-tid="message-timestamp"]',
+ ];
+ for (const sel of timeSelectors) {
+ const timeEl = el.querySelector(sel) || el.closest?.('[data-tid*="message"]')?.querySelector(sel);
+ if (timeEl) {
+ const dt = timeEl.getAttribute('datetime');
+ if (dt) return dt;
+ const title = timeEl.getAttribute('title');
+ if (title) return title;
+ }
+ }
+ return undefined;
+ }
+
function _extractChatMessage(el: HTMLElement): boolean {
- // Strategy 1: Standard selectors
const messageSelectors = [
'[data-tid="chat-message"]',
'.fui-ChatMessage',
@@ -203,10 +231,12 @@ export class ChatProcedure {
}
if (text && text.length > 0) {
+ const teamsTs = _extractTeamsTimestamp(messageEl) || _extractTeamsTimestamp(el);
(window as any).__onChatMessageEvent({
speaker: author,
text,
- timestamp: new Date().toISOString(),
+ timestamp: teamsTs || new Date().toISOString(),
+ teamsTimestamp: teamsTs,
messageKey: `${author}::${text}`,
});
return true;
@@ -239,10 +269,12 @@ export class ChatProcedure {
// Check if this looks like a time-stamped message (not just any two children)
const hasTid = children[i].getAttribute('data-tid') || '';
if (hasTid.includes('author') || hasTid.includes('name') || hasTid.includes('sender')) {
+ const teamsTs = _extractTeamsTimestamp(el);
(window as any).__onChatMessageEvent({
speaker: candidateName,
text: candidateBody,
- timestamp: new Date().toISOString(),
+ timestamp: teamsTs || new Date().toISOString(),
+ teamsTimestamp: teamsTs,
messageKey: `${candidateName}::${candidateBody}`,
});
return true;
@@ -325,19 +357,21 @@ 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; messageKey?: string }): void {
+ private _handleChatMessage(msg: {
+ speaker: string;
+ text: string;
+ timestamp: string;
+ teamsTimestamp?: 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;
@@ -353,13 +387,22 @@ export class ChatProcedure {
}
this._lastMessageText = msg.text;
- this._logger.info(`Chat: [${msg.speaker}] ${msg.text}`);
+ // Classify as history (sent before bot joined) vs live
+ const messageTimestamp = new Date(msg.teamsTimestamp || msg.timestamp);
+ const messageMs = messageTimestamp.getTime();
+ const isHistory = this._botJoinedAtMs > 0 && messageMs < this._botJoinedAtMs;
+
+ this._logger.info(
+ `Chat${isHistory ? ' [HISTORY]' : ''}: [${msg.speaker}] ${msg.text}` +
+ (msg.teamsTimestamp ? ` (teams_ts=${msg.teamsTimestamp})` : ''),
+ );
this._onChatMessage({
speaker: msg.speaker,
text: msg.text,
- timestamp: new Date(msg.timestamp),
+ timestamp: messageTimestamp,
source: 'chat',
+ isHistory,
});
}
diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts
index 28dc1ae..ee1cee2 100644
--- a/src/bot/orchestrator.ts
+++ b/src/bot/orchestrator.ts
@@ -1025,8 +1025,7 @@ export class BotOrchestrator {
this._page,
this._logger,
(entry: ChatMessageEntry) => {
- // Send chat message to Gateway as a special transcript
- this._sendChatMessage(entry.speaker, entry.text);
+ this._sendChatMessage(entry.speaker, entry.text, entry.isHistory);
this._callbacks.onTranscript({
speaker: entry.speaker,
text: entry.text,
@@ -1203,6 +1202,7 @@ export class BotOrchestrator {
*/
private async _enableChat(): Promise {
try {
+ this._chatProcedure!.setBotJoinedAt(Date.now());
await this._chatProcedure!.enableChatMonitoring();
await this._chatProcedure!.subscribeToChatMessages();
this._logger.info('Chat monitoring enabled and subscribed');
@@ -1252,7 +1252,7 @@ export class BotOrchestrator {
/**
* Send a chat message event to the Gateway.
*/
- private _sendChatMessage(speaker: string, text: string): void {
+ private _sendChatMessage(speaker: string, text: string, isHistory: boolean = false): void {
const message: ChatMessage = {
type: 'chatMessage',
sessionId: this._sessionId,
@@ -1260,6 +1260,7 @@ export class BotOrchestrator {
speaker,
text,
timestamp: new Date().toISOString(),
+ isHistory,
},
};
this._sendToGateway(message);
diff --git a/src/types/index.ts b/src/types/index.ts
index 3d71dfb..c97e461 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -47,6 +47,7 @@ export interface ChatMessage {
speaker: string;
text: string;
timestamp: string;
+ isHistory?: boolean;
};
}