Skip to content

[pull] main from forem:main - #404

Merged
pull[bot] merged 4 commits into
amishakov:mainfrom
forem:main
Sep 29, 2026
Merged

pull[bot] merged 4 commits into
amishakov:mainfrom
forem:main

Conversation

@pull

@pull pull Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

jonmarkgo and others added 4 commits September 28, 2026 15:41
)

* Use Jev as a cheap escalation check for spam without links

Article and comment spam checks only reached Gemini (ArticleCheck/CommentCheck) when the
content had an <a> tag, so spam with contact details written as text (Telegram, WhatsApp,
phone) never got an AI check. Content without links now goes to a Jev check (TypeSafe's
classifier) first, and only escalates to the existing Gemini check when Jev flags it.
Jev never flags anything on its own. Without TYPESAFE_API_KEY nothing changes.

Claude-Session: https://claude.ai/code/session_01FLx46puMiJkQWtNe7kTRJU

* Move the spam escalation check onto Ai::TypeSafe and Ai::FunctionConfig

Replaces the PR's standalone Ai::Jev client with the shared Ai::TypeSafe::Client,
Questions and Result from #23879, so the escalation check gets the same retries,
AiAudit logging and model pinning (TYPESAFE_API_MODEL) as every other Jev function.

Escalation is now the :spam_escalation AI function, selectable under Config > AI
Models. It is Jev-only: its choices are Off (the default) and Jev, so setting
TYPESAFE_API_KEY for other functions no longer turns it on implicitly. The spam
handler gates on Ai::FunctionConfig.available? for the article/comment spam checks,
so escalated content goes to whichever model those checks are set to, Gemini or Jev.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bound Jev latency in high-priority jobs and make the escalation threshold admin-tunable

The spam and moderation jobs (Articles/Comments::HandleSpamWorker,
DetectCodeBlockLanguagesWorker) run on high_priority and can make several Jev calls each.
With a 20s timeout and 3 retries, a TypeSafe outage could hold a worker for ~87s per call.

- Ai::TypeSafe::Client takes timeout: and max_retries:, defaults to the SDKs' 10s timeout,
  and exposes FAIL_FAST (5s, no retries). Every Jev caller on a high-priority path uses it:
  ArticleCheck, CommentCheck, SpamEscalationCheck, ContentModerationLabeler, SubforemFinder,
  ArticleEnhancer and DetectCodeBlockLanguages. Background callers keep retries.
- A circuit breaker shared through Rails.cache opens after 5 outage failures (timeouts,
  connection errors, 429, 5xx) in a minute and skips all Jev calls for 2 minutes.
- DetectCodeBlockLanguages aborts when the circuit is open instead of writing "plaintext"
  for every remaining block.
- The spam escalation threshold is a global Settings::AiFunctions value (default 0.3,
  validated to (0, 1]) editable under Config > AI Models > Tuning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Ben Halpern <bendhalpern@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…by it (#23900)

* Run the spam domain check in a worker so banish can't be rolled back by it

Granting the spam or suspended role ran Spam::DomainDetector inside the
rolify after_add callback. Its `email LIKE '%@Domain'` scan can hit the
statement timeout on uncommon domains; the QueryCanceled then rolled back
the role insert and aborted Moderator::BanishUser right after it created
the BanishedUser row. Honeybadger ignores QueryCanceled, so it failed
silently and re-banishing hit the same wall.

Claude-Session: https://claude.ai/code/session_018TsZkCc8o2SwEbux3MQgqz

* Skip domain detection for already-blocked domains and cover the worker

BlockDomainAndSuspendUsersWorker blocks the domain before suspending each
user, so each resulting detector job now short-circuits on the indexed
blocked_email_domains lookup instead of re-scanning users by email.

Claude-Session: https://claude.ai/code/session_018TsZkCc8o2SwEbux3MQgqz
ForemStatsClient calls are forwarded to Datadog unchanged and also
written to Better Stack as one structured event each, so monitors built
on the app's own metrics can be rebuilt before Datadog is turned off.
Inert unless BETTERSTACK_METRICS_SOURCE_TOKEN is set in production.

Claude-Session: https://claude.ai/code/session_01JGecM5hifj2QYgVRa7qDm7
…led spam reactions (#23883)

* Auto-mark repeat spam authors, fix spam check commit race, log failed spam reactions

- Low-trust authors (<4 badges) with 3+ mascot vomits on clear_and_obvious_* posts in
  the last month get the spam role, without waiting for moderators to confirm each vomit.
- Enqueue Articles::HandleSpamWorker after the transaction commits so the worker can find
  newly created articles and sees the committed published state.
- Log when the mascot's spam reaction fails validation instead of dropping it silently.

Claude-Session: https://claude.ai/code/session_01FLx46puMiJkQWtNe7kTRJU
Entire-Checkpoint: 01M3D7GD03KRTQBATV6Z11S3TE

* Count only active auto-flags for repeat offenders, keep the count database-side

- Ignore mascot vomits that moderators have invalidated or archived (valid_or_confirmed).
- Pass the flagged articles as a subquery instead of loading their IDs into Ruby.
- Specs for invalidated flags and for the failed-reaction warning and its quiet case.

Claude-Session: https://claude.ai/code/session_01FLx46puMiJkQWtNe7kTRJU

* Note why repeat auto-flagged authors are marked as spam, expand spam handler tests

Leave an automatic_spam note from the mascot (with the flag count) when a
repeat auto-flagged author gets the spam role, skipping it when the author is
already spam so queued jobs don't duplicate notes. Add specs for the note, each
exclusion rule of the repeat-offender count, and the after-commit enqueue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Ben Halpern <bendhalpern@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@pull pull Bot locked and limited conversation to collaborators Sep 29, 2026
@pull pull Bot added the ⤵️ pull label Sep 29, 2026
@pull
pull Bot merged commit 6d0df2a into amishakov:main Sep 29, 2026
2 of 4 checks passed

This branch had an error being deployed

1 failed deployment
staging — 6d0df2a3 Deployed Sep 29, 2026 by pull[bot] via deploy (staging) #380
production — 6d0df2a3 Deployed Sep 29, 2026 by pull[bot] via deploy (production) #380
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant