Commit Graph
24 Commits
Author SHA1 Message Date
Jamie BruntonandClaude Opus 5 1ac6ad22ad feat: grow new accounts by commenting on active threads
A new Reddit account cannot start chats until Reddit considers it
established, and nothing in the app previously did anything about that.
Account growth posts a small number of AI-written comments per day on
active threads in an operator-chosen list of communities, per account,
so the history that lifts the hold actually accumulates.

Growth is account-scoped rather than part of the shared outreach funnel:
karma, comment history and pacing all belong to one identity. It runs as
the lowest-priority work in the account runtime, after inbound replies,
and deliberately keeps running while an account is held out of outbound
messaging.

Comments publish without operator review, so the guards are the feature:

- Both screening stages fail closed. A deterministic gate rejects links,
  contact solicitation, user tags, filler and any text that reveals the
  generator; the classifier must actively approve with confidence, and
  an outage stalls growth rather than publishing unreviewed text.
- Thread text reaches the model only as fenced untrusted data, since
  Reddit posts do contain instructions aimed at automated readers.
- The daily allowance is adaptive, not configured: it starts at two per
  day and rises only while comments survive and earn upvotes.
- Posted comments are swept at ~15m/2h/24h with one batched info.json
  request, plus a single signed-out probe that detects a comment the
  public cannot see. Two consecutive removals, an invisible comment, or
  a captcha challenge pauses the account until an operator clears it. A
  failed probe never pauses anything.
- One comment per thread across every owned account, enforced by a
  unique index, because two of our accounts on one post is the easiest
  pattern to correlate.
- A comment interrupted mid-post is abandoned, never retried: we cannot
  know whether the write landed and a duplicate cannot be withdrawn.
- Reddit's refusals are authoritative — a ban disables that community
  for that account, a throttle backs off at the scope Reddit named, and
  a signed-out response invalidates the session.

Comments are submitted through the account's own browser session against
the old.reddit form endpoint, which returns machine-readable error codes
and needs no credentials beyond the cookies already imported.

API-first: /api/growth exposes state, settings, the community list and
pause/resume, all verified against a local instance. Route responses are
parsed with the shared contracts in tests so the server-side mirror
cannot drift from what the dashboard expects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:08:52 +01:00
Jamie BruntonandClaude Opus 5 89eb4583f8 feat: flag accounts Reddit considers unestablished and re-test them
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>
2026-08-11 09:14:47 +01:00
jjbrunton d0a8bd0e2f feat: expand outreach targeting and messaging 2026-08-10 17:31:05 +01:00
Jamie BruntonandClaude Opus 5 38d175e4a1 feat: push selected events to a phone over ntfy
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>
2026-08-10 13:55:00 +01:00
Jamie BruntonandClaude Opus 5 4404ae75ff 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>
2026-08-10 12:53:00 +01:00
jjbrunton f98c5c732d Treat Telegram references as seller signals 2026-08-09 17:09:41 +01:00
jjbrunton e9083281a8 feat: configure ready outreach funnel limit 2026-08-09 16:13:58 +01:00
jjbrunton b73e305cf3 Add hybrid R4R media conversation mode 2026-08-09 11:45:45 +01:00
jjbrunton 0f76bcf59a Add per-subreddit R4R conversation modes 2026-08-09 11:30:32 +01:00
jjbrunton 23d4c3a320 Add varied messaging experiments 2026-08-09 10:28:55 +01:00
jjbrunton 9e1b1aeefe Shorten strategy response window to 12 hours 2026-08-09 08:03:54 +01:00
jjbrunton dcf57273e2 Disable unavailable subreddits after Reddit 404 2026-08-08 22:53:24 +01:00
jjbrunton e8b730b592 feat: add agent API and operational analytics 2026-08-08 22:08:14 +01:00
jjbrunton 2c100d72b6 feat: persist settings and enrich conversation context 2026-08-08 18:18:28 +01:00
jjbrunton c483c7b8be feat: replace browser login with imported Reddit sessions 2026-08-08 16:48:59 +01:00
jjbrunton ad770a792a feat: secure React dashboard cutover 2026-08-08 15:51:06 +01:00
jjbruntonandClaude Opus 4.7 d9b9a79bdd Make subreddit list editable from the dashboard
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>
2026-05-22 22:43:05 +01:00
jjbruntonandClaude Opus 4.7 46a213e3e0 Add subreddit tone profiles for message style guidance
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>
2026-05-22 22:29:01 +01:00
jjbruntonandClaude Opus 4.7 2260388dc8 Blur gallery images by default, sync progress UI, and add chat/monitor tests
- 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>
2026-05-22 10:01:09 +01:00
jjbrunton 633bb0ec04 Restructure chat service, web routes, and add test suite
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.
2026-05-21 01:32:10 +01:00
jjbruntonandClaude Opus 4.6 dfa0580859 Update source code and remove runtime data from tracking
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>
2026-05-20 18:54:07 +01:00
jjbrunton 7b3ea37127 Update 2026-01-05 00:17:06 +00:00
jjbrunton 0c5ffc5001 Update 2026-01-04 19:24:56 +00:00
jjbrunton c552956516 Initial 2026-01-04 19:16:31 +00:00