Make conversation queues mutually exclusive

This commit is contained in:
Jamie Brunton committed 2026-08-14 09:25:45 +01:00
1 parent 3bb1dda6bb
commit 2447563c60
20 files changed
+386 -220

No files matched your search

+3 -1
View File
@@ -76,6 +76,8 @@ Explicit bot clients can send to a username through `POST /api/v1/messages` or t
Each account runtime refreshes recent chats and processes actionable inbound conversations oldest-first before it can accept new outreach. After the initial conservative startup scan, the persisted latest Matrix message event plus the worker's prior-scan watermark is authoritative: a newly observed inbound event newer than the confirmed opener remains actionable even if Matrix's presentation-only unread notification count has already returned to zero. On startup, read-marked rooms without a pending reply claim are not revived. Durable event claims, conversation policy state, and a later outbound event prevent duplicate replies.
Every running-mode reply scan also applies the shared deterministic seller/off-platform rules to the complete cached inbound transcript before actionability is calculated. This retrospective pass catches Telegram, Snapchat, other contact-platform pushes, payment requests, verification scams, paid-content funnels, and configured seller evidence that arrived before a restart or after an operator took ownership. It uses an in-process transcript watermark, spends no classifier tokens, persists the seller/terminal state, and moves killed conversations to Done. Scan mode remains non-mutating. The operator-forced seller scan uses the same deterministic decision before its optional batched AI fallback.
Outgoing Matrix events are attributed against the recorded opener, durable automated-reply fingerprints, and confirmed exchange-library event records. If synchronization observes an unmatched outgoing message, the conversation becomes operator-owned before another reply can be generated. An unmatched outgoing message also supersedes, rather than confirms, a pending automated claim; only the claimed reply text can reconcile that claim as sent.
```mermaid
@@ -100,7 +102,7 @@ flowchart TD
SEND --> MARK["Mark claim sent and record latency"]
```
The inbound event key, intended reply, claim status, and conversation state make retries idempotent. Immediately after any limiter wait, automation refreshes Matrix and revalidates the latest inbound event, boundaries, images, seller evidence, policy status, and the one-media-request limit. Newer state makes the old claim terminally `superseded`; it is never sent. If the process dies after Matrix accepted a reply, reconciliation finds the outbound event and marks the old claim sent instead of sending it again.
The inbound event key, intended reply, claim status, and conversation state make retries idempotent. Immediately after any limiter wait, automation refreshes Matrix and revalidates the latest inbound event, boundaries, images, the shared seller/off-platform decision, policy status, and the one-media-request limit. Newer state makes the old claim terminally `superseded`; it is never sent. If the replacement event is a seller or off-platform push, the same revalidation ends and classifies the conversation instead of leaving it for a later pass. If the process dies after Matrix accepted a reply, reconciliation finds the outbound event and marks the old claim sent instead of sending it again.
General R4R generation explicitly permits consensual adult NSFW conversation. A model response that softly refuses, moralizes, or diverts solely because the topic is sexual is rejected before send and falls through to the next configured chat model. General replies may offer a generic approved saved picture without that offer being miscounted as a request for recipient media.
+1 -1
View File
@@ -78,7 +78,7 @@ Exchange-library uploads require an explicit consent checkbox before the raw ima
Conversation reply composers include a collapsed saved-image picker. It lists only server-approved assets for the latest inbound request, keeps thumbnails blurred until revealed, and requires selection plus a dedicated confirmation dialog before sending. The server remains authoritative and may reject a choice if the conversation changes before the rate-limited send executes.
The conversation inbox has a system-derived **Account deleted** bucket alongside the operator-maintained open, snoozed, and done queues. A deleted recipient is excluded from the default, unread, needs-reply, waiting, manual, and image buckets and is labelled in both the list and detail views. Suspended profiles are not treated as deleted.
The conversation inbox assigns every row to exactly one primary workflow queue: **Needs attention**, **Waiting**, **Sellers**, **Snoozed**, **Done**, **Deleted**, or **Archived**. Removed-account history belongs only to Archived, deleted recipients belong only to Deleted, and seller/off-platform outcomes belong only to Sellers, so none can leak into actionable work. Unread state, manual ownership, and received images are independent badges and secondary toggles rather than competing queues. The default queue is Needs attention. Suspended profiles are not treated as deleted.
## Feature structure
+1 -1
View File
@@ -54,7 +54,7 @@ The snapshot matters: changing configuration later must not silently rewrite the
Within those boundaries, R4R replies are expected to participate naturally in consensual adult NSFW conversation instead of moralizing, declining the topic, or diverting ordinary flirting into generic small talk. In-character soft refusals are treated like provider refusals and fall through to the next configured chat model. This does not weaken recipient boundaries, the under-18 handoff, seller/off-platform controls, location and meeting prohibitions, or the separate media-request rules.
Two of those stops are terminal rather than handoffs. A reply that pushes the conversation onto another messaging app, asks for contact details, solicits payment, points at a paid-content funnel, or runs the age/bot "verification" scam sets the automation status to `killed`, marks the durable seller flag, and resolves the operator's triage state, so the conversation leaves every actionable queue instead of collecting in the manual one. Detection normalizes the text first — unicode folding, zero-width stripping, leet mapping, separator removal — because these words are routinely obfuscated (`0nlyfans`, `t3l3gram`, `t e l e g r a m`). Ambiguous terms need context: `snap` on its own is a photo request in this product and never a kill on its own. Off-platform kills are a safety control and stay on independently of `reddit.filterSellers`; a configured seller keyword or domain also kills unless `conversations.killOnSellerKeyword` is disabled. Under-18 disclosures and in-person/contact-detail asks remain human handoffs, and now apply to every policy mode rather than the R4R ones only.
Two of those stops are terminal rather than handoffs. A reply that pushes the conversation onto another messaging app, asks for contact details, solicits payment, points at a paid-content funnel, or runs the age/bot "verification" scam sets the automation status to `killed`, marks the durable seller flag, and resolves the operator's triage state, so the conversation leaves every actionable queue instead of collecting in the manual one. The same deterministic decision runs against cached inbound history during each running-mode reply scan and during the operator-forced seller scan, including conversations that are too old to revive for an automated reply or have already become operator-owned. Detection normalizes the text first — unicode folding, zero-width stripping, leet mapping, separator removal — because these words are routinely obfuscated (`0nlyfans`, `t3l3gram`, `t e l e g r a m`). Ambiguous terms need context: `snap` on its own is a photo request in this product and never a kill on its own. Off-platform kills are a safety control and stay on independently of `reddit.filterSellers`; a configured seller keyword or domain also kills unless `conversations.killOnSellerKeyword` is disabled. Under-18 disclosures and in-person/contact-detail asks remain human handoffs, and now apply to every policy mode rather than the R4R ones only.
Optional second-stage AI screening (`conversations.aiReplyScreening`) runs only when the deterministic rules come up clean, classifies the inbound side of the conversation as genuine, seller, off-platform, scam, bot, or unclear, and kills at or above `killConfidence` while handing off at or above `flagConfidence`. It fails open: a classifier error or outage never blocks a reply, and each inbound event is screened at most once per process. Short refusals are interpreted in sequence: phrases such as “I’m good” become media boundaries only when they answer a media request.
+3 -1
View File
@@ -57,8 +57,10 @@ Verification is the safety mechanism that matters most. Posted comments are swep
| `services/chat/matrix/username-resolver.ts` | Matrix/Reddit identity mapping | Preserve unresolved internal IDs without leaking them in operator text. |
| `services/chat/matrix/token-refresher.ts` | Explicit refresh-unavailable behavior | New credentials come from verified extension dumps, not token sniffing. |
| `services/recipient-account-status.ts` | Periodic cached-conversation recipient profile checks | Only definitive Reddit results replace durable state; checks are authentication-bound and must not block the reply loop. |
| `services/conversation-automation.ts` | Reply-first policy, claims, retries, send-time refresh, handoffs, turn budgets | Snapshot policy; claim before send; supersede stale queued work; reconcile pending claims; fail closed. |
| `services/conversation-automation.ts` | Reply-first policy, cached-history deterministic risk screening, claims, retries, send-time refresh, handoffs, turn budgets | Snapshot policy; apply the shared risk decision before actionability and send; claim before send; supersede stale queued work; reconcile pending claims; fail closed. |
| `services/conversation-state.ts` | Pure conversation parsing/state/safety helpers | Conversation text is untrusted data; keep prompt boundaries explicit. |
| `services/conversation-risk.ts` | Shared deterministic seller, funnel, contact-platform, payment, and scam decision | Live, revalidation, automatic history, and forced-sweep paths must not diverge. |
| `services/seller-sweep.ts` | Operator-forced deterministic and batched-AI seller screening | Use complete cached inbound text; deterministic off-platform outcomes remain terminal; unchanged clean AI screens use the durable watermark. |
| `services/conversation-boundaries.ts` | Sequence-aware media requests, refusals, and full-stop detection | Short ambiguous replies become boundaries only in the matching request context. |
| `services/outbound-message-safety.ts` | Final no-proximity/no-meeting rule | This is defense in depth beyond prompts. |
| `services/conversation-outcome*.ts` | Response outcome classification/recording | Keep analytics attribution account/message scoped. |