New accounts cannot start chats until Reddit considers them established.
Until now the bot would burn a candidate on every invitation Reddit was
always going to reject, with nothing in the dashboard explaining why the
account looked idle.
Two signals feed the verdict. A periodic profile check reads the
account's own age, karma and email verification from about.json and
compares them against configurable thresholds — that is what flags a
brand new account before it wastes anything. A rejected invitation is
authoritative: Reddit has told us directly, so the account is held out of
sending with a backoff that doubles per rejection, and a delivered
invitation clears the flag outright.
An unestablished account takes no new posts (canAcceptPost), raises an
attention item, reports an `unestablished` sending state, and shows its
age/karma evidence on the Accounts page. An unchecked account is always
allowed through, so a first run is never blocked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a Notifications tab to Settings so an operator can pick which events
reach their phone, published to a single self-hosted ntfy topic.
The notifier subscribes to the realtime event hub the dashboard already
consumes, so every notifiable event keeps exactly one publisher. Inbound
media had no event, so `media.received` is added and published from the
existing durable `gotImage` gate in the chat service — one notification
per conversation, never repeated when the room re-syncs. It feeds the
in-app notification bell as well.
- Best-effort delivery: a rejecting or unreachable ntfy server is logged
and never propagated into the path that produced the event.
- Events buffered before the notifier attaches are skipped, so a restart
cannot replay a backlog to somebody's phone.
- Settings are applied field by field onto the live config object, so a
save takes effect without a restart.
- The access token is environment-owned (NTFY_AUTH_TOKEN) like the other
secrets: stripped before the settings record is written, never returned
by the API, which reports only `hasAuthToken`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Adds an inline subreddit editor on the dashboard that lets you add,
remove, and edit (tone, type, weight) the subreddit list, with changes
persisted to config.yaml on disk so they survive restarts.
Backend
- src/config/persistence.ts: round-trips config.yaml via
YAML.parseDocument, mutating existing row nodes in place so their
trailing inline `# ...` comments survive an edit; new rows are
appended as flow-style maps to match the on-disk idiom; removed rows
are dropped. Default values ('default' tone, 'image' type) are
omitted on write so the file stays clean. Atomic via tmp+rename.
- src/web/routes/config.ts: GET /api/config now also returns the
current subreddit list and the available tone keys. PUT /api/config
accepts a `subreddits` field, validates with the existing
SubredditConfigSchema, rejects duplicates, persists, then mutates
ctx.config.reddit.subreddits in place so the processor picks up the
new tone immediately without a restart.
Frontend
- public/index.html: replaces the read-only subreddit chip list in the
Configuration snapshot section with a grid of editable rows (name,
type, tone dropdown, weight, remove). + Add / Save buttons with
inline status. Tones are sourced from /api/config so the catalog
drives the dropdown.
Tests
- tests/config-persistence.test.ts: covers in-place mutation comment
preservation, removal, appends, reorder, default omission, and
atomic write cleanup.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduces a tone-profile system that lets the processor adapt outgoing
message style per-subreddit (and per-flair) instead of using one tone
for everything. Profiles act as a "tone mic" layered after the base
prompt and strategy override, so they shape voice without redefining
the whole message.
- src/config/tone-profiles.ts defines the catalog (default, dominant,
submissive, playful, direct, ...) and a resolveTone() helper that
falls back to the default profile for unknown keys.
- config.yaml maps subreddits to tone keys; the schema in
src/config/index.ts validates it.
- src/services/processor.ts threads the resolved tone into the prompt
build, picking based on subreddit and optional link flair.
- src/reddit/api.ts adds link_flair_text to RedditPost so the
processor can use it for tone selection.
- src/ai/openrouter.ts accepts and forwards the tone-shaped prompt.
- public/index.html surfaces the active tone in the dashboard.
- Tests cover the catalog, config validation, and processor integration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Blur NSFW media gallery images until hover/reveal
- Surface chat sync progress in the dashboard header
- Extend chat service, cache, and monitor with delta-media handling
- Add tests for chat cooldown, delta media, and monitor flows
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Split monolithic chat service into modular structure with Matrix client
support, extract web server into per-resource route modules, introduce
bootstrap/main-loop separation, and add comprehensive vitest coverage
across services, repositories, and utilities. Adds project docs and
CLAUDE.md agent instructions.
Add gitignore rules for browser session data, SQLite databases,
media downloads, and build artifacts. Remove previously tracked
runtime files from the repository.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>