From dbcc53bed85b63385b0713b45f1d99d28dcb3ca5 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sat, 28 Feb 2026 16:12:56 +0100
Subject: [PATCH] fix: chat queue to prevent interleaved text, retry+selectors
for greeting
Made-with: Cursor
---
src/bot/chatProcedure.ts | 91 ++++++++++++++++++++++++----------------
src/bot/orchestrator.ts | 51 +++++++++++++++++-----
2 files changed, 94 insertions(+), 48 deletions(-)
diff --git a/src/bot/chatProcedure.ts b/src/bot/chatProcedure.ts
index 580a870..4302eba 100644
--- a/src/bot/chatProcedure.ts
+++ b/src/bot/chatProcedure.ts
@@ -408,50 +408,67 @@ export class ChatProcedure {
/**
* Send a chat message in the meeting.
- * Finds the chat input, types the message, and sends it.
+ * Finds the chat input (with retry), types the message, and sends it.
*/
async sendChatMessage(text: string): Promise {
this._logger.info(`Sending chat message: ${text.substring(0, 60)}...`);
- try {
- // Find chat input
- const inputSelectors = [
- '[data-tid="ckeditor-replyConversation"]',
- 'div[role="textbox"][data-tid*="chat"]',
- 'div[role="textbox"][aria-label*="message" i]',
- 'div[role="textbox"][aria-label*="Nachricht" i]',
- 'div[contenteditable="true"][data-tid*="message"]',
- 'div[contenteditable="true"][aria-label*="message" i]',
- ];
+ const inputSelectors = [
+ '[data-tid="ckeditor-replyConversation"]',
+ 'div[role="textbox"][data-tid*="chat"]',
+ 'div[role="textbox"][data-tid*="message"]',
+ 'div[role="textbox"][aria-label*="message" i]',
+ 'div[role="textbox"][aria-label*="Nachricht" i]',
+ 'div[contenteditable="true"][data-tid*="message"]',
+ 'div[contenteditable="true"][data-tid*="chat"]',
+ 'div[contenteditable="true"][aria-label*="message" i]',
+ '[aria-label*="Type a new message" i]',
+ '[aria-label*="Neue Nachricht eingeben" i]',
+ ];
- let input: any = null;
- for (const selector of inputSelectors) {
- input = await this._page.$(selector);
- if (input) break;
+ const maxAttempts = 5;
+ const retryDelayMs = 600;
+
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+ try {
+ let input: any = null;
+ for (const selector of inputSelectors) {
+ input = await this._page.$(selector);
+ if (input) {
+ const isVisible = await input.isVisible().catch(() => false);
+ if (isVisible) break;
+ await input.dispose();
+ input = null;
+ }
+ }
+
+ if (input) {
+ await input.click();
+ await this._page.waitForTimeout(200);
+
+ await this._page.keyboard.type(text, { delay: 10 });
+ await this._page.waitForTimeout(200);
+ await this._page.keyboard.press('Enter');
+ this._logger.info('Chat message sent');
+ return true;
+ }
+
+ if (attempt < maxAttempts) {
+ this._logger.info(`Chat input not found, retry ${attempt}/${maxAttempts} in ${retryDelayMs}ms`);
+ await this._page.waitForTimeout(retryDelayMs);
+ }
+ } catch (error) {
+ this._logger.error(`Error sending chat message (attempt ${attempt}):`, error);
+ if (attempt < maxAttempts) {
+ await this._page.waitForTimeout(retryDelayMs);
+ } else {
+ return false;
+ }
}
-
- if (!input) {
- this._logger.warn('Could not find chat input field');
- return false;
- }
-
- // Click to focus
- await input.click();
- await this._page.waitForTimeout(200);
-
- // Type the message
- await this._page.keyboard.type(text, { delay: 10 });
- await this._page.waitForTimeout(200);
-
- // Press Enter to send
- await this._page.keyboard.press('Enter');
- this._logger.info('Chat message sent');
- return true;
-
- } catch (error) {
- this._logger.error('Error sending chat message:', error);
- return false;
}
+
+ this._logger.warn('Could not find chat input field');
+ return false;
}
/**
diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts
index ee1cee2..0c37e50 100644
--- a/src/bot/orchestrator.ts
+++ b/src/bot/orchestrator.ts
@@ -72,6 +72,8 @@ export class BotOrchestrator {
private _isShuttingDown: boolean = false;
private _isDebugMode: boolean = false;
private _keepAliveInterval: NodeJS.Timeout | null = null;
+ private _chatMessageQueue: string[] = [];
+ private _chatQueueProcessing: boolean = false;
constructor(
sessionId: string,
@@ -699,10 +701,8 @@ export class BotOrchestrator {
case 'sendChatMessage':
const chatMsg = message as SendChatMessage;
this._logger.info(`Gateway sendChatMessage received: ${chatMsg.text?.substring(0, 60)}...`);
- try {
- await this.sendChatMessageToMeeting(chatMsg.text);
- } catch (chatErr) {
- this._logger.error(`Failed to send chat message to meeting: ${chatErr}`);
+ if (chatMsg.text) {
+ this._enqueueChatMessage(chatMsg.text);
}
break;
@@ -1234,7 +1234,10 @@ export class BotOrchestrator {
this._logger.info(`Sending join greeting (chat + voice): ${greeting}`);
- // Chat greeting
+ // Brief delay so chat input is ready after panel open (Teams DOM can lag)
+ await new Promise((r) => setTimeout(r, 800));
+
+ // Chat greeting (queued; retries if input not found)
await this.sendChatMessageToMeeting(greeting);
// Voice greeting — ask Gateway to generate TTS and send back playAudio
@@ -1294,14 +1297,40 @@ export class BotOrchestrator {
}
/**
- * Send a text message to the meeting chat.
+ * Enqueue a chat message for serialized sending (prevents interleaved text when
+ * multiple messages arrive in quick succession).
+ */
+ private _enqueueChatMessage(text: string): void {
+ this._chatMessageQueue.push(text);
+ this._processChatQueue();
+ }
+
+ /**
+ * Process chat queue one message at a time to avoid interleaved typing.
+ */
+ private async _processChatQueue(): Promise {
+ if (this._chatQueueProcessing || this._chatMessageQueue.length === 0) return;
+ this._chatQueueProcessing = true;
+ while (this._chatMessageQueue.length > 0) {
+ const text = this._chatMessageQueue.shift()!;
+ if (this._isShuttingDown || this._state !== 'in_meeting' || !this._chatProcedure) {
+ this._logger.warn('Cannot send chat message - not in meeting, skipping queued message');
+ continue;
+ }
+ try {
+ await this._chatProcedure.sendChatMessage(text);
+ } catch (chatErr) {
+ this._logger.error(`Failed to send chat message to meeting: ${chatErr}`);
+ }
+ }
+ this._chatQueueProcessing = false;
+ }
+
+ /**
+ * Send a text message to the meeting chat (enqueued for serialized delivery).
*/
async sendChatMessageToMeeting(text: string): Promise {
- if (this._isShuttingDown || this._state !== 'in_meeting' || !this._chatProcedure) {
- this._logger.warn('Cannot send chat message - not in meeting');
- return;
- }
- await this._chatProcedure.sendChatMessage(text);
+ this._enqueueChatMessage(text);
}
/**