export type SentMessageLookup = { text?: string; messageId?: string; }; export type SentMessageCache = { remember: (scope: string, lookup: SentMessageLookup) => void; has: (scope: string, lookup: SentMessageLookup) => boolean; }; // Keep the text fallback short so repeated user replies like "ok" are not // suppressed for long; delayed reflections should match the stronger message-id key. const SENT_MESSAGE_TEXT_TTL_MS = 5_000; const SENT_MESSAGE_ID_TTL_MS = 60_000; function normalizeEchoTextKey(text: string | undefined): string | null { if (!text) { return null; } const normalized = text.replace(/\r\n?/g, "\n").trim(); return normalized ? normalized : null; } function normalizeEchoMessageIdKey(messageId: string | undefined): string | null { if (!messageId) { return null; } const normalized = messageId.trim(); if (!normalized || normalized === "ok" || normalized === "unknown") { return null; } return normalized; } class DefaultSentMessageCache implements SentMessageCache { private textCache = new Map(); private messageIdCache = new Map(); remember(scope: string, lookup: SentMessageLookup): void { const textKey = normalizeEchoTextKey(lookup.text); if (textKey) { this.textCache.set(`${scope}:${textKey}`, Date.now()); } const messageIdKey = normalizeEchoMessageIdKey(lookup.messageId); if (messageIdKey) { this.messageIdCache.set(`${scope}:${messageIdKey}`, Date.now()); } this.cleanup(); } has(scope: string, lookup: SentMessageLookup): boolean { this.cleanup(); const messageIdKey = normalizeEchoMessageIdKey(lookup.messageId); if (messageIdKey) { const idTimestamp = this.messageIdCache.get(`${scope}:${messageIdKey}`); if (idTimestamp && Date.now() - idTimestamp <= SENT_MESSAGE_ID_TTL_MS) { return true; } } const textKey = normalizeEchoTextKey(lookup.text); if (textKey) { const textTimestamp = this.textCache.get(`${scope}:${textKey}`); if (textTimestamp && Date.now() - textTimestamp <= SENT_MESSAGE_TEXT_TTL_MS) { return true; } } return false; } private cleanup(): void { const now = Date.now(); for (const [key, timestamp] of this.textCache.entries()) { if (now - timestamp > SENT_MESSAGE_TEXT_TTL_MS) { this.textCache.delete(key); } } for (const [key, timestamp] of this.messageIdCache.entries()) { if (now - timestamp > SENT_MESSAGE_ID_TTL_MS) { this.messageIdCache.delete(key); } } } } export function createSentMessageCache(): SentMessageCache { return new DefaultSentMessageCache(); }