Add per-subreddit R4R conversation modes

This commit is contained in:
jjbrunton committed 2026-08-09 11:30:32 +01:00
1 parent 23d4c3a320
commit 0f76bcf59a
14 files changed
+343 -24

No files matched your search

+34 -5
View File
@@ -33,7 +33,9 @@ export interface ConversationState {
automation: ConversationAutomationState;
}
export const MAX_AUTOMATED_CONVERSATION_TURNS = 3;
export const LEGACY_AUTOMATED_CONVERSATION_TURNS = 3;
export const CONVERSATION_POLICY_VERSION = 1;
export type ConversationPolicyMode = 'legacy' | 'r4r' | 'manual';
export type ConversationAutomationStatus =
| 'active'
@@ -43,6 +45,9 @@ export type ConversationAutomationStatus =
| 'user-overridden';
export interface ConversationAutomationState {
policyMode: ConversationPolicyMode;
policyVersion: number;
maxAutomatedTurns: number;
turnsUsed: number;
status: ConversationAutomationStatus;
lastIncomingKey: string | null;
@@ -51,8 +56,14 @@ export interface ConversationAutomationState {
aiMessageFingerprints: string[];
}
export function createConversationAutomationState(): ConversationAutomationState {
export function createConversationAutomationState(
policyMode: ConversationPolicyMode = 'legacy',
maxAutomatedTurns = LEGACY_AUTOMATED_CONVERSATION_TURNS,
): ConversationAutomationState {
return {
policyMode,
policyVersion: CONVERSATION_POLICY_VERSION,
maxAutomatedTurns,
turnsUsed: 0,
status: 'active',
lastIncomingKey: null,
@@ -67,7 +78,7 @@ export function conversationMessageFingerprint(message: string): string {
export function remainingAutomationTurns(state: ConversationState): number {
if (state.automation.status !== 'active') return 0;
return Math.max(0, MAX_AUTOMATED_CONVERSATION_TURNS - state.automation.turnsUsed);
return Math.max(0, state.automation.maxAutomatedTurns - state.automation.turnsUsed);
}
const EMPTY_PERSONA: ConversationPersona = {
@@ -126,7 +137,7 @@ export function chooseConversationIntent(
}
return {
kind: 'media-ask',
reason: `image-sharing turn ${state.automation.turnsUsed + 1} of ${MAX_AUTOMATED_CONVERSATION_TURNS}`,
reason: `image-sharing turn ${state.automation.turnsUsed + 1} of ${state.automation.maxAutomatedTurns}`,
};
}
@@ -187,6 +198,13 @@ export function isSafeConversationReply(message: string): boolean {
return sentences.length <= 3;
}
/** R4R automation may converse and flirt, but it must never solicit sexual media. */
export function isSafeR4RReply(message: string): boolean {
if (!isSafeConversationReply(message)) return false;
return !/\b(send|show|share|swap|trade|drop|give|dm)\b[^.!?\n]{0,60}\b(pic|pics|photo|photos|selfie|selfies|video|videos|nude|nudes|content)\b/i.test(message)
&& !/\b(can|could|would|will) you\b[^.!?\n]{0,60}\b(pic|pics|photo|photos|selfie|selfies|video|videos|nude|nudes)\b/i.test(message);
}
export function serializeConversationState(state: ConversationState): string {
return JSON.stringify(state);
}
@@ -220,13 +238,24 @@ export function parseConversationState(value: string | null | undefined): Conver
function parseAutomationState(value: unknown): ConversationAutomationState {
if (!value || typeof value !== 'object') return createConversationAutomationState();
const parsed = value as Partial<ConversationAutomationState>;
const policyMode: ConversationPolicyMode = parsed.policyMode === 'r4r' || parsed.policyMode === 'manual'
? parsed.policyMode
: 'legacy';
const maxAutomatedTurns = typeof parsed.maxAutomatedTurns === 'number'
? Math.max(1, Math.min(20, Math.trunc(parsed.maxAutomatedTurns)))
: LEGACY_AUTOMATED_CONVERSATION_TURNS;
const status: ConversationAutomationStatus =
parsed.status === 'image-received' || parsed.status === 'human-required' || parsed.status === 'boundary' || parsed.status === 'user-overridden'
? parsed.status
: 'active';
return {
policyMode,
policyVersion: typeof parsed.policyVersion === 'number' && parsed.policyVersion > 0
? Math.trunc(parsed.policyVersion)
: CONVERSATION_POLICY_VERSION,
maxAutomatedTurns,
turnsUsed: typeof parsed.turnsUsed === 'number'
? Math.max(0, Math.min(MAX_AUTOMATED_CONVERSATION_TURNS, Math.trunc(parsed.turnsUsed)))
? Math.max(0, Math.min(maxAutomatedTurns, Math.trunc(parsed.turnsUsed)))
: 0,
status,
lastIncomingKey: typeof parsed.lastIncomingKey === 'string' ? parsed.lastIncomingKey : null,