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>
Stopping the bot previously halted conversation syncing too. On the
multi-account path the run-flag check sat above both the reply-sync and
response-check branches, so a stop left only MediaSyncService's coarse
30-minute chat refresh. The legacy single-account path did the opposite
and kept syncing while stopped — one boolean meaning two things.
Replace the boolean with an explicit three-way mode:
running — discover, screen, send, automate replies
scan — refresh chats, detect replies, check responses; send nothing
and discover nothing; actionable replies surface as backlog
stopped — everything halts, including background media sync
scan differs from browser.dryRun, which still discovers and pays for AI
screening before suppressing the send.
The mode persists in runtime_state.bot_mode; a migration backfills it
from bot_running, and rows written by an older build still read
correctly. Both loops now gate on the mode, the account runtime loop
reconciles background media sync on transitions only, and the dashboard
and agent API expose all three states.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The settings page had accumulated five tabs whose names no longer matched
their contents, three different save models, and the whole subreddit
configuration buried under a tab named after a checkbox.
Structure:
- Subreddits becomes its own page: a list showing each community's mode,
tone, computed share of polling, and live sent/reply stats, with one
subreddit edited at a time in a drawer.
- Accounts becomes its own page. It is session lifecycle, not config.
- Settings collapses to one scrolling page: Safety, Pacing (Monitoring and
Rate limits merged), Targeting, Notifications, Advanced.
- The sidebar is grouped, having gone from 9 to 11 destinations.
Saving:
- Every section owns its own draft, validation, risk gate and save via
useConfigCard, submitting only the keys it owns. A syntax error in the
advanced JSON can no longer block a rate-limit change, and enabling an
autonomous policy is its own deliberate commit.
- UnsavedChangesProvider restores the leave-page guard across cards that no
longer share a draft.
Subreddit editing:
- Errors are keyed by field name instead of array index, removing the
defensive invalidation that existed because row indices shifted on add
and remove. Confirming an autonomous mode can no longer be attributed to
the wrong row.
- Weight is shown as the share it actually produces, live while editing.
- Bulk actions cover start/pause polling, tone, policy and removal, because
production runs 30-40 subreddits and per-row editing does not scale.
Bulk policy changes name every subreddit turning autonomous, and
normalise the hybrid mode's turn budget, which has no form to catch it.
Physique targeting moves into its own Targeting card. It sends only
reddit.physiqueTargeting, which the config route applies independently, so
it cannot clobber the pacing card's fields. Turning screening on now warns
that it will reduce how many posts qualify.
Also surfaces the effective sending rate, since the hourly limit and the
minimum delay both throttle and only the binding one matters.
Legacy ?section= links redirect or resolve to anchors.
Fixes found while verifying in a browser: a radio-card rule was shrinking
nested number inputs; .share-preview had no styles; the throughput readout
reported a rate from a blank field; and the reconcile logic warned about a
server change whenever a sibling card saved.
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>