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

+28
View File
@@ -14,6 +14,7 @@ function minimalConfig(overrides: Record<string, unknown> = {}): Record<string,
media: overrides.media ?? {},
strategies: overrides.strategies ?? {},
web: overrides.web ?? {},
...(overrides.conversations ? { conversations: overrides.conversations } : {}),
};
}
@@ -161,6 +162,33 @@ describe('ConfigSchema', () => {
}
});
it('defaults the conversation kill controls on, with AI screening off', () => {
const result = ConfigSchema.safeParse(minimalConfig({ reddit: { subreddits: ['test'] } }));
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.conversations).toEqual({
offPlatformKill: true,
killOnSellerKeyword: true,
aiReplyScreening: { enabled: false, killConfidence: 0.8, flagConfidence: 0.6 },
});
}
});
it('accepts overridden conversation kill controls', () => {
const result = ConfigSchema.safeParse(minimalConfig({
reddit: { subreddits: ['test'] },
conversations: { killOnSellerKeyword: false, aiReplyScreening: { enabled: true, killConfidence: 0.95 } },
}));
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.conversations.offPlatformKill).toBe(true);
expect(result.data.conversations.killOnSellerKeyword).toBe(false);
expect(result.data.conversations.aiReplyScreening).toEqual({
enabled: true, killConfidence: 0.95, flagConfidence: 0.6,
});
}
});
it('accepts custom sellerKeywords', () => {
const result = ConfigSchema.safeParse(minimalConfig({
reddit: { subreddits: ['test'], sellerKeywords: ['custom1', 'custom2'] },