Files
RedditBot/docs/data-flow.md
T

7.3 KiB

Runtime flows

Candidate funnel

The production multi-account path is three independent loops backed by candidate_queue.

stateDiagram-v2
    [*] --> discovered: listing intake
    discovered --> screening: leased by screening worker
    screening --> ready: eligibility and preparation pass
    screening --> retry_wait: transient failure
    screening --> rejected: terminal eligibility or AI failure
    ready --> sending: leased to available account
    sending --> sent: Matrix confirms conversation
    sending --> retry_wait: account cooldown or transient failure
    sending --> rejected: terminal delivery result
    retry_wait --> discovered: screening retry due
    retry_wait --> ready: delivery retry due
    discovered --> expired: outside configured recent window
    ready --> expired: outside configured recent window

1. Discovery

The discovery loop rotates through enabled subreddits and authenticated runtimes. A runtime fetches Reddit /new data using the process-shared RedditRequestCoordinator. Intake deduplicates post IDs and authors, excludes processed/contacted history, stores the full post snapshot, source type, weight, and timestamps, and logs exact intake counts.

The queue grows to a demand-based target rather than a fixed backlog. The target is the smaller of configured reddit.maxReadyCandidates and the work that authenticated running accounts can still send within the recent-post window, accounting for rolling hourly capacity and minimum message spacing. Intake admits only the current deficit, pauses at zero outbound capacity, and reports fetched posts left unevaluated because the funnel is full. Reddit request pacing, not a tight application poll, remains the primary request limiter.

2. Screening and preparation

A screening worker leases one discovered candidate. MonitorService.screenCandidate() applies cheap rules before expensive/networked rules:

  1. Global processed-post and recipient-history checks.
  2. For chat sources, self-declared gender and adult age parsing.
  3. Seller evidence from the post, profile/about, and recent activity.
  4. Source-specific preparation in ProcessorService.

For image posts, preparation downloads and validates the image, detects target gender, performs model analysis, selects an image strategy, and generates an opener. For chat posts, it selects a chat strategy and generates from the post text. Both paths apply outbound safety checks and return either a prepared immutable result or an explicit skip reason.

Terminal results become rejected candidates. Rate limits and transient failures become retryable candidates. Prepared candidates become ready; the persisted snapshot means a different account can deliver without repeating AI work.

3. Dispatch

An account is eligible for new outreach only when its runtime is running, both identities verify, it has no actionable reply backlog, there is no invitation cooldown, and its rate limiter permits a send. Available accounts are ordered by their current hourly send count.

Matrix cooldowns retain the operation that triggered them. A room-creation or invitation limit pauses new outreach but does not suppress replies in existing rooms; a limit returned while sending into an existing room pauses those sends as well.

The dispatch worker leases a ready candidate for an account, materializes the processed post in that account's repository, applies the account rate limiter, checks dry-run, and atomically claims the recipient globally. ChatService.startConversationDetailed() returns one of:

  • sent: persist the message and outcome metadata, commit recipient history, update activity, and mark the candidate sent;
  • terminal: release the recipient reservation and reject the candidate;
  • retryable: release/defer so another healthy account can claim it soon.

Only Matrix confirmation makes a send successful. A healthy HTTP process, room identifier lookup, or attempted request does not.

Reddit request and cooldown flow

Every runtime shares one RedditRequestCoordinator, because Reddit can enforce limits by origin as well as account. JSON requests are serialized with a minimum interval; failed requests consume spacing too. A 429 extends the global cooldown using the server delay plus a buffer. Broad 403 behavior can also result in coordinated backoff. A listing 404 is treated as terminal for that subreddit and persisted as enabled=false.

Do not add local retries around this layer without preserving the shared schedule. Per-account bursts defeat the coordinator.

Reply-first conversation automation

Each account runtime refreshes recent chats and processes actionable unread conversations oldest-first before it can accept new outreach.

flowchart TD
    IN["Unread inbound Matrix event"] --> VERIFIED{"Confirmed after a recorded opener?"}
    VERIFIED -- no --> IGNORE["Ignore as unverified"]
    VERIFIED -- yes --> CLAIM{"Durable event claim exists?"}
    CLAIM -- sent --> IGNORE
    CLAIM -- pending --> RECON["Reconcile against Matrix history"]
    CLAIM -- no --> STATE["Load or snapshot conversation policy"]
    STATE --> STOP{"Manual, seller, image, boundary, unsafe, or turn limit?"}
    STOP -- yes --> HANDOFF["Persist terminal/human-required outcome"]
    STOP -- no --> GEN["Generate policy-specific reply"]
    GEN --> SAFE{"Outbound checks pass?"}
    SAFE -- no --> HANDOFF
    SAFE -- yes --> PERSIST["Claim event and persist intended state"]
    PERSIST --> SEND["Rate-limited Matrix send"]
    SEND --> MARK["Mark claim sent and record latency"]

The inbound event key, intended reply, claim status, and conversation state make retries idempotent. 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.

Chat and media synchronization

ChatService communicates with Reddit's Matrix homeserver, maintains an account-scoped SQLite-backed cache, resolves t2_ identities to Reddit usernames, and emits sync progress/new-reply callbacks. Startup chooses full or delta sync from persisted cursor state. Operators can explicitly run tracked sync jobs through the versioned API.

MediaService resolves Matrix media, downloads it into the persistent data directory, and writes an account-scoped media index. MediaSyncService schedules incremental work only while the account is authenticated. Private media is served behind the same authorization boundary as the dashboard and APIs.

Dashboard data and events

Routes assemble snapshots from repositories and injected services. LogBroadcaster/the realtime event hub publishes typed events for bot state, sends, failures, conversations, replies, sync, sessions, rate limits, and logs. The React client validates incoming events, invalidates matching TanStack Query keys, and reconnects with replay support. Cached screens remain visible but stale during a disconnect.

Failure classification

Use three categories consistently:

  • Terminal: the candidate/action is unsuitable or impossible; persist a reason and do not retry automatically.
  • Retryable: service limits, temporary auth/network errors, or account cooldown; preserve the candidate and retry time.
  • Human-required: policy or safety cannot continue autonomously; stop automation and surface an attention item.

Avoid converting errors into generic skips. Operational diagnosis depends on preserving which stage failed and why.