Pause automation after manual Reddit replies
This commit is contained in:
1 parent
bf474dfe10
commit
84a133451f
5 files changed
+175
-6
No files matched your search
@@ -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'
|
||||
|
||||
@@ -14,6 +14,13 @@ export interface MediaExchangeChat {
|
||||
sendChatMessage(roomId: string, message: string, txnId?: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface MediaExchangeOutboundAttribution {
|
||||
eventIds: string[];
|
||||
texts: string[];
|
||||
}
|
||||
|
||||
const RECIPROCAL_MESSAGE = 'Your turn — send me one back if you’re comfortable.';
|
||||
|
||||
export type MediaExchangeResult =
|
||||
| { kind: 'not-requested' }
|
||||
| { kind: 'dry-run'; requestKind: MediaRequestKind }
|
||||
@@ -40,6 +47,14 @@ export class MediaLibraryExchangeService {
|
||||
);
|
||||
}
|
||||
|
||||
getOutboundAttribution(conversationId: string): MediaExchangeOutboundAttribution {
|
||||
const sends = this.repository.listConversationSends(this.accountId, conversationId);
|
||||
return {
|
||||
eventIds: sends.flatMap((send) => send.matrixMediaEventId ? [send.matrixMediaEventId] : []),
|
||||
texts: sends.flatMap((send) => send.matrixTextEventId ? [RECIPROCAL_MESSAGE] : []),
|
||||
};
|
||||
}
|
||||
|
||||
async handle(
|
||||
conversationId: string,
|
||||
incomingKey: string,
|
||||
@@ -103,7 +118,7 @@ export class MediaLibraryExchangeService {
|
||||
? true
|
||||
: await this.chat.sendChatMessage(
|
||||
conversationId,
|
||||
'Your turn — send me one back if you’re comfortable.',
|
||||
RECIPROCAL_MESSAGE,
|
||||
`rbot-library-reciprocal-${send.id}`,
|
||||
);
|
||||
if (!textSent) {
|
||||
|
||||
Reference in new issue
Block a user