From 0f76bcf59a7316c8f126bff43b63f12ffe47ad08 Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Sun, 9 Aug 2026 11:30:32 +0100 Subject: [PATCH] Add per-subreddit R4R conversation modes --- .../src/features/settings/settings-editor.tsx | 7 ++ .../src/features/settings/settings-model.ts | 13 +++ .../features/settings/settings-sections.tsx | 39 +++++++- packages/api-contracts/src/config.ts | 5 + .../api-contracts/tests/contracts.test.ts | 8 +- src/ai/openrouter.ts | 7 +- src/config/index.ts | 10 ++ src/services/conversation-automation.ts | 95 ++++++++++++++++--- src/services/conversation-state.ts | 39 +++++++- .../reddit-account-runtime-builder.ts | 1 + tests/config.test.ts | 28 +++++- tests/conversation-automation.test.ts | 74 ++++++++++++++- tests/conversation-state.test.ts | 7 ++ tests/dashboard/settings.test.tsx | 34 +++++++ 14 files changed, 343 insertions(+), 24 deletions(-) diff --git a/apps/dashboard/src/features/settings/settings-editor.tsx b/apps/dashboard/src/features/settings/settings-editor.tsx index 4d27250..43af148 100644 --- a/apps/dashboard/src/features/settings/settings-editor.tsx +++ b/apps/dashboard/src/features/settings/settings-editor.tsx @@ -76,6 +76,13 @@ export function SettingsEditor({ config, section }: SettingsEditorProps) { if (validation.data.maxMessagesPerHour > 20 && validation.data.maxMessagesPerHour > persistedLimit) { reasons.push(`The limit will allow ${validation.data.maxMessagesPerHour} messages per hour.`); } + const enablesAutonomousR4R = validation.data.payload.subreddits?.some((subreddit, index) => ( + subreddit.conversation.mode === 'r4r' + && persistedDraft.subreddits[index]?.conversationMode !== 'r4r' + )); + if (enablesAutonomousR4R) { + reasons.push('Autonomous R4R will send conversational replies without approval for the selected subreddit. Existing conversations keep their snapshotted policy.'); + } if (reasons.length > 0) { setPendingRiskySave({ payload: validation.data.payload, reasons }); diff --git a/apps/dashboard/src/features/settings/settings-model.ts b/apps/dashboard/src/features/settings/settings-model.ts index 48430e7..4ec9492 100644 --- a/apps/dashboard/src/features/settings/settings-model.ts +++ b/apps/dashboard/src/features/settings/settings-model.ts @@ -14,6 +14,8 @@ export interface SubredditDraft { weight: string; tone: z.infer; scripted: string; + conversationMode: 'legacy' | 'r4r' | 'manual'; + maxAutomatedTurns: string; } export interface SettingsDraft { @@ -73,6 +75,8 @@ export function createSettingsDraft(config: ConfigResponse): SettingsDraft { weight: subreddit.weight === undefined ? '' : String(subreddit.weight), tone: subreddit.tone, scripted: subreddit.scripted ?? '', + conversationMode: subreddit.conversation.mode, + maxAutomatedTurns: String(subreddit.conversation.maxAutomatedTurns), })), advancedConfigJson: config.advanced ? JSON.stringify(config.advanced, null, 2) : '', }; @@ -89,6 +93,9 @@ export function validateSettingsDraft(draft: SettingsDraft): SettingsValidationR if (subreddit.weight.trim() !== '' && Number(subreddit.weight) <= 0) { requiredErrors[`subreddits.${index}.weight`] = 'Subreddit weight must be greater than 0.'; } + if (subreddit.conversationMode === 'r4r' && subreddit.maxAutomatedTurns.trim() === '') { + requiredErrors[`subreddits.${index}.maxAutomatedTurns`] = 'Maximum automated turns is required.'; + } }); if (Object.keys(requiredErrors).length > 0) return { errors: requiredErrors }; @@ -119,6 +126,12 @@ export function validateSettingsDraft(draft: SettingsDraft): SettingsValidationR weight: subreddit.weight.trim() === '' ? undefined : Number(subreddit.weight), tone: subreddit.tone, scripted: subreddit.scripted.trim() === '' ? undefined : subreddit.scripted, + conversation: { + mode: subreddit.conversationMode, + maxAutomatedTurns: subreddit.conversationMode === 'r4r' + ? Number(subreddit.maxAutomatedTurns) + : 3, + }, })), }); diff --git a/apps/dashboard/src/features/settings/settings-sections.tsx b/apps/dashboard/src/features/settings/settings-sections.tsx index 905beba..c31cde1 100644 --- a/apps/dashboard/src/features/settings/settings-sections.tsx +++ b/apps/dashboard/src/features/settings/settings-sections.tsx @@ -122,7 +122,7 @@ export function BehaviourSection({ config, draft, errors, onChange }: SectionPro ...draft, subreddits: [ ...draft.subreddits, - { name: '', enabled: true, type: 'image', weight: '1', tone: 'default', scripted: '' }, + { name: '', enabled: true, type: 'image', weight: '1', tone: 'default', scripted: '', conversationMode: 'legacy', maxAutomatedTurns: '3' }, ], })} type="button" @@ -190,6 +190,43 @@ export function BehaviourSection({ config, draft, errors, onChange }: SectionPro {config.tones.map((tone) => )} + + {subreddit.conversationMode === 'r4r' ? ( + + ) : null}