Pause automation after manual Reddit replies

This commit is contained in:
jjbrunton committed 2026-08-09 19:30:16 +01:00
1 parent bf474dfe10
commit 84a133451f
5 files changed
+175 -6

No files matched your search

+53 -2
View File
@@ -189,6 +189,22 @@ export class ConversationAutomationService {
this.states.save(conversationId, state);
}
const incomingKey = messageKey(latest);
const mediaAttribution = this.mediaExchange?.getOutboundAttribution?.(conversationId);
if (state.automation.status === 'active'
&& hasUnattributedSelfMessage(
messages,
opener,
state.automation.aiMessageFingerprints,
mediaAttribution?.eventIds,
mediaAttribution?.texts,
)) {
state.automation.status = 'user-overridden';
state.automation.handoffReason = 'A manual reply was observed in this conversation.';
this.states.save(conversationId, state);
this.claims.markSuperseded?.(conversationId, incomingKey);
this.logger.info(`[automation] Paused u/${conversation.username}; an operator-authored message was observed`);
return null;
}
const existingClaim = this.claims.getClaim?.(conversationId, incomingKey);
if (existingClaim?.status === 'pending') {
const observedOutbound = messages.some((message) => message.isSelf && message.timestamp > latest.timestamp);
@@ -398,9 +414,17 @@ export class ConversationAutomationService {
const messages = conversation.messages ?? [];
const incomingIndex = messages.findIndex((message) => messageKey(message) === claim.incomingKey);
if (incomingIndex < 0) continue;
if (!messages.slice(incomingIndex + 1).some((message) => message.isSelf)) continue;
if (this.claims.markSent(claim.conversationId, claim.incomingKey)) {
const laterSelfMessages = messages.slice(incomingIndex + 1).filter((message) => message.isSelf);
if (laterSelfMessages.length === 0) continue;
const expectedFingerprint = claim.replyText
? conversationMessageFingerprint(claim.replyText)
: null;
const observedClaimedReply = expectedFingerprint !== null
&& laterSelfMessages.some((message) => conversationMessageFingerprint(message.text) === expectedFingerprint);
if (observedClaimedReply && this.claims.markSent(claim.conversationId, claim.incomingKey)) {
this.logger.info(`[automation] Reconciled interrupted reply for u/${conversation.username} from Matrix history`);
} else if (!observedClaimedReply && this.claims.markSuperseded?.(claim.conversationId, claim.incomingKey)) {
this.logger.info(`[automation] Superseded queued reply for u/${conversation.username}; an operator-authored message was observed`);
}
}
}
@@ -632,6 +656,33 @@ function messageKey(message: ChatMessage): string {
return message.eventId ?? `${message.timestamp}:${message.sender}:${message.text}`;
}
function hasUnattributedSelfMessage(
messages: readonly ChatMessage[],
opener: SentMessageRecord,
automatedFingerprints: readonly string[],
automatedEventIds: readonly string[] = [],
automatedTexts: readonly string[] = [],
): boolean {
const knownEventIds = new Set(automatedEventIds);
const knownFingerprints = new Map<string, number>();
const addKnown = (fingerprint: string) => {
knownFingerprints.set(fingerprint, (knownFingerprints.get(fingerprint) ?? 0) + 1);
};
addKnown(conversationMessageFingerprint(opener.message));
for (const fingerprint of automatedFingerprints) addKnown(fingerprint);
for (const text of automatedTexts) addKnown(conversationMessageFingerprint(text));
for (const message of messages) {
if (!message.isSelf || message.timestamp < opener.sentAt) continue;
if (message.eventId && knownEventIds.has(message.eventId)) continue;
const fingerprint = conversationMessageFingerprint(message.text);
const remaining = knownFingerprints.get(fingerprint) ?? 0;
if (remaining === 0) return true;
knownFingerprints.set(fingerprint, remaining - 1);
}
return false;
}
function replyPlan(state: ConversationState): { isLegacy: boolean; isHybridMediaAsk: boolean } {
const isLegacy = state.automation.policyMode === 'legacy';
const isHybridMediaAsk = state.automation.policyMode === 'r4r-media'