Fix missed conversation replies
This commit is contained in:
1 parent
70c156e1f7
commit
dc3fc6373b
4 files changed
+68
-18
No files matched your search
@@ -88,14 +88,15 @@ export class ConversationAutomationService {
|
||||
private readonly pendingReplyRetryDelayMs = PENDING_REPLY_RETRY_DELAY_MS,
|
||||
) {}
|
||||
|
||||
async processUnreadConversations(signal?: AbortSignal): Promise<ConversationAutomationResult[]> {
|
||||
async processPendingConversations(signal?: AbortSignal): Promise<ConversationAutomationResult[]> {
|
||||
this.processing = true;
|
||||
let conversations: ActiveChat[] = [];
|
||||
try {
|
||||
const previousScanAt = this.lastScanAt;
|
||||
await this.chat.refreshTopChats(REPLY_REFRESH_LIMIT);
|
||||
conversations = await this.chat.getCachedConversations();
|
||||
this.reconcilePendingClaims(conversations);
|
||||
const actionable = this.actionableConversations(conversations);
|
||||
const actionable = this.actionableConversations(conversations, previousScanAt);
|
||||
this.actionableBacklog = actionable.length;
|
||||
this.oldestPendingAt = actionable[0]?.messages?.at(-1)?.timestamp ?? null;
|
||||
this.lastScanAt = this.now();
|
||||
@@ -144,10 +145,10 @@ export class ConversationAutomationService {
|
||||
conversation: ActiveChat,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ConversationAutomationResult | null> {
|
||||
if (!conversation.unread || conversation.isFromUs || !conversation.roomId || !conversation.url) return null;
|
||||
if (!conversation.roomId || !conversation.url) return null;
|
||||
const messages = conversation.messages ?? [];
|
||||
const latest = messages.at(-1);
|
||||
if (!latest || latest.isSelf) return null;
|
||||
const latest = latestInboundMessage(messages);
|
||||
if (!latest) return null;
|
||||
|
||||
const conversationId = conversation.roomId;
|
||||
const opener = this.openers.getByChatRoomId(conversationId);
|
||||
@@ -322,9 +323,12 @@ export class ConversationAutomationService {
|
||||
);
|
||||
}
|
||||
|
||||
private actionableConversations(conversations: ActiveChat[]): ActiveChat[] {
|
||||
private actionableConversations(
|
||||
conversations: ActiveChat[],
|
||||
observedAfter: number | null = this.lastScanAt,
|
||||
): ActiveChat[] {
|
||||
return conversations
|
||||
.filter((conversation) => this.isActionableConversation(conversation))
|
||||
.filter((conversation) => this.isActionableConversation(conversation, observedAfter))
|
||||
.sort((left, right) => {
|
||||
const leftAt = left.messages?.at(-1)?.timestamp ?? left.lastMessageTime;
|
||||
const rightAt = right.messages?.at(-1)?.timestamp ?? right.lastMessageTime;
|
||||
@@ -346,19 +350,25 @@ export class ConversationAutomationService {
|
||||
}
|
||||
}
|
||||
|
||||
private isActionableConversation(conversation: ActiveChat): boolean {
|
||||
if (!conversation.unread || conversation.isFromUs || !conversation.roomId || !conversation.url) return false;
|
||||
const latest = conversation.messages?.at(-1);
|
||||
if (!latest || latest.isSelf) return false;
|
||||
private isActionableConversation(conversation: ActiveChat, observedAfter: number | null): boolean {
|
||||
if (!conversation.roomId || !conversation.url) return false;
|
||||
const latest = latestInboundMessage(conversation.messages ?? []);
|
||||
if (!latest) return false;
|
||||
const opener = this.openers.getByChatRoomId(conversation.roomId);
|
||||
if (!opener || latest.timestamp <= opener.sentAt) return false;
|
||||
const incomingKey = messageKey(latest);
|
||||
const claim = this.claims.getClaim?.(conversation.roomId, incomingKey);
|
||||
if (claim?.status === 'pending') return true;
|
||||
if (claim?.status === 'sent') return false;
|
||||
// On the first scan after startup, retain Matrix unread as the conservative
|
||||
// bootstrap signal so a deployment cannot revive historical read replies.
|
||||
// Later scans can recover fresh inbound events even if that flag cleared.
|
||||
if (!conversation.unread && (observedAfter === null || latest.timestamp <= observedAfter)) return false;
|
||||
const state = this.states.get(conversation.roomId);
|
||||
if (!state) return true;
|
||||
return state.automation.status === 'active' && state.automation.lastIncomingKey !== incomingKey;
|
||||
if (state) {
|
||||
return state.automation.status === 'active' && state.automation.lastIncomingKey !== incomingKey;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private advanceStateForReply(
|
||||
@@ -454,6 +464,17 @@ export class ConversationAutomationService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matrix unread counts are presentation state and can return to zero before
|
||||
* the automation worker observes an inbound event. The durable message list,
|
||||
* opener timestamp, scan watermark, conversation state, and event claim decide
|
||||
* whether work remains; only a latest inbound event can enter that flow.
|
||||
*/
|
||||
function latestInboundMessage(messages: ChatMessage[]): ChatMessage | null {
|
||||
const latest = messages.at(-1);
|
||||
return latest && !latest.isSelf ? latest : null;
|
||||
}
|
||||
|
||||
function hasReceivedImage(messages: ChatMessage[]): boolean {
|
||||
return messages.some((message) => !message.isSelf && (message.media?.length ?? 0) > 0);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user