feat: kill conversations on off-platform and seller pushes

Seller detection on replies was a case-insensitive substring match over the
configured keyword list, and every hit landed in `human-required` alongside
genuine prospects that need a person. Off-platform pushes were only checked
for r4r policy modes, as were the under-18 signals.

Three layers now:

- New `off-platform-signals` module matches on normalized text (unicode
  folding, zero-width stripping, leet mapping, separator removal), so
  `0nlyf4ns`, `t3l3gr4m` and `t e l e g r a m` are caught. Ambiguous terms
  need context: `snap` on its own stays a photo request, never a kill.
- New terminal `killed` automation status. It records the evidence, sets the
  durable seller flag, supersedes the pending claim, and resolves triage to
  `done`, so killed conversations leave every actionable queue. Off-platform
  kills are independent of `reddit.filterSellers`.
- Optional AI screening of inbound replies behind
  `conversations.aiReplyScreening` (off by default). Runs only after the
  deterministic gates pass, memoizes per inbound event, and fails open.

Under-18 and in-person/contact-detail signals now apply to every policy mode
rather than r4r only, and remain human handoffs rather than kills.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jamie BruntonandClaude Opus 5 committed 2026-08-10 12:53:00 +01:00
1 parent ac12a3146a
commit 4404ae75ff
18 files changed
+907 -31

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
import { describe, expect, it } from 'vitest';
import {
findOffPlatformSignal,
foldSignalText,
normalizeSignalText,
offPlatformSignalReason,
} from '../src/services/off-platform-signals.js';
describe('normalizeSignalText', () => {
it('lowercases, folds unicode, and strips zero-width characters', () => {
expect(normalizeSignalText('Tele​gram ME')).toBe('telegram me');
});
it('normalizes curly apostrophes so contraction patterns match', () => {
expect(normalizeSignalText('What’s your snap')).toBe("what's your snap");
});
});
describe('foldSignalText', () => {
it('maps leet characters back to letters', () => {
expect(foldSignalText('0nly F4n5')).toBe('onlyfans');
});
it('removes separators used to break up a word', () => {
expect(foldSignalText('t e l e g r a m')).toBe('telegram');
expect(foldSignalText('t.e.l.e.g.r.a.m')).toBe('telegram');
expect(foldSignalText('t-e-l-e-g-r-a-m')).toBe('telegram');
});
it('collapses padded character runs', () => {
expect(foldSignalText('snaaaapchat')).toBe('snapchat');
});
});
describe('findOffPlatformSignal', () => {
it('returns null for an empty or whitespace message', () => {
expect(findOffPlatformSignal('')).toBeNull();
expect(findOffPlatformSignal(' ')).toBeNull();
});
it('detects named messaging platforms', () => {
for (const text of [
'im on telegram if you want to chat there',
'do you have whatsapp?',
'my snapchat is jess_xo',
'i have a discord',
'kik me',
'wickr?',
]) {
expect(findOffPlatformSignal(text)?.kind, text).toBe('contact-platform');
}
});
it('detects obfuscated platform names', () => {
expect(findOffPlatformSignal('t3l3gr4m @jess')?.label).toBe('Telegram');
expect(findOffPlatformSignal('s n a p c h a t?')?.label).toBe('Snapchat');
expect(findOffPlatformSignal('what​sapp me')?.label).toBe('WhatsApp');
expect(findOffPlatformSignal('0nlyf4ns.com/jess')?.label).toBe('OnlyFans');
});
it('detects platform link shorteners without the platform name', () => {
expect(findOffPlatformSignal('here: t.me/jessxo')?.label).toBe('Telegram');
expect(findOffPlatformSignal('wa.me/447700900123')?.label).toBe('WhatsApp');
});
it('detects seller funnels and link-in-bio pushes', () => {
expect(findOffPlatformSignal('my linktr.ee/jess has everything')?.kind).toBe('seller-link');
expect(findOffPlatformSignal('all my links are in my bio')?.kind).toBe('seller-link');
expect(findOffPlatformSignal('subs are on fansly now')?.kind).toBe('seller-link');
});
it('detects payment rails and cashtags', () => {
expect(findOffPlatformSignal('cashapp me and ill send more')?.kind).toBe('payment');
expect(findOffPlatformSignal('my tag is $jessxo')?.kind).toBe('payment');
expect(findOffPlatformSignal('paypal works too')?.kind).toBe('payment');
});
it('does not read a plain dollar amount as a cashtag', () => {
expect(findOffPlatformSignal('it cost me $200 lol')).toBeNull();
});
it('detects the age and bot verification scam', () => {
expect(findOffPlatformSignal('i need you to do age verification first')?.kind).toBe('verification-scam');
expect(findOffPlatformSignal('just verify you are not a bot on this site')?.kind).toBe('verification-scam');
expect(findOffPlatformSignal('send the verification code from the link')?.kind).toBe('verification-scam');
});
it('detects generic requests to continue somewhere else', () => {
expect(findOffPlatformSignal('add me on the other app')?.kind).toBe('handle-ask');
expect(findOffPlatformSignal('whats your handle?')?.kind).toBe('handle-ask');
expect(findOffPlatformSignal('lets talk somewhere else')?.kind).toBe('handle-ask');
expect(findOffPlatformSignal('wanna take this off reddit?')?.kind).toBe('handle-ask');
});
it('treats "snap" as a photo rather than the app', () => {
for (const text of [
'send me a snap',
'ill snap something for you later',
'got any snaps of you',
'heres a snap from this morning',
]) {
expect(findOffPlatformSignal(text), text).toBeNull();
}
});
it('leaves other ordinary messages alone', () => {
for (const text of [
'hey, hows your week going?',
'i signal my turns like a proper driver',
'that discount was insane',
'im 32 and from manchester',
'i work in insurance, its as dull as it sounds',
'i dont really send pics to strangers sorry',
'we should get a drink sometime',
'i barely use this app, im hardly ever off reddit',
]) {
expect(findOffPlatformSignal(text), text).toBeNull();
}
});
it('keeps an on-platform reply from matching the contact-request rule', () => {
expect(findOffPlatformSignal('just message me on reddit whenever')).toBeNull();
});
it('builds a reason that names the kind and the evidence', () => {
const signal = findOffPlatformSignal('my telegram is @jess');
expect(signal).not.toBeNull();
expect(offPlatformSignalReason(signal!)).toBe(
'Off-platform contact push detected (Telegram: "telegram").',
);
});
});