Skip to main content

Review of RFC — AI Spam Gatekeeper Phase 1: Silent Classification & Block (BE), cycles R1 (initial) + R2 (post-fix), 2026-07-14.

R2 summary (2026-07-14): all 7 R1 findings applied and verified fixed in the same-day RFC revision. PM confirmed the REV-2 product intent (spam message stays visible in the resolved room). Re-score: 8.5/10 — Agentic-Ready — PROCEED. No open findings on the chatbot-owned scope; the only remaining gates are the two by-design external dependencies (hub-core provenance, ai_agent_alert).

RFC Review: AI Spam Gatekeeper Phase 1 — Silent Classification & Block (BE)

Executive Summary

A well-grounded backend RFC with unusually strong repo evidence (every anchor in Detail 2.0 carries session-verified file:line, several independently re-verified), clean decision hygiene (9 ADRs, honest "no alternative considered" where true), and a fail-open posture that correctly protects the messaging hot path. The cross-repo dependency boundaries (Detail 2.D) are exactly right: the core cost-saving path has zero hard runtime dependency on either external squad.

Two genuine gaps keep it out of the Agentic-Ready band, both at the core of the feature rather than its edges: (1) the classifier prompt template — the actual spec of the classification behavior — is never written down; an agent reaching Chunk 2 must invent the system prompt, the policy-interpolation format, and the few-shot examples from nothing. (2) The reply-suppression side-effect contract on the high_confidence path is explicitly deferred (§5 item 3) — and it hides a real product question the RFC doesn't surface: whether the spam message itself remains visible in the resolved room. It must (the "Bukan spam" review flow is impossible if the supervisor can't see what was classified), which means suppression cannot simply mirror the legacy early-return; insert_history/SendContext behavior needs pinning.

Both are fixable inline without redesign. Verdict: PROCEED with notes — fix REV-1 and REV-2 in the RFC before implementation starts; the rest can be addressed during Chunk review.

Quick Verdict

Overall scoreR1: 7.5 / 10 → R2: 8.5 / 10
Rating bandR1 Strong (7.0–8.4) → R2 Agentic-Ready (8.5–10.0)
Agentic Readiness VerdictR1: PROCEED with notes → R2: PROCEED — all 7 findings fixed; chatbot-owned scope has no open findings
RFC type / sub-typebackend / new-feature
ConfidenceHigh — all sections present, consistent; R2 gaps closed with concrete spec (prompt template, suppression contract, resolve-idempotency table)
Mermaid validity6/6 blocks parse (mmdc-validated both cycles; happy-path sequence re-validated after the R2 suppression-note edit)

Findings Ledger

IDSeverityFindingStatusFirst seenResolved in
REV-1majorClassifier prompt template unspecified — the SystemPreference params_spam_classifier seed was only an envelope; system prompt, interpolation contract, and calibration examples absent.fixedR1R2 — Detail 4.A now carries the verbatim system prompt, {{CATEGORIES}}/{{DEFINITION}}/{{MESSAGE}} template, JSON contract, and a 6-row calibration table (incl. the disabled-category-never-matched assertion)
REV-2majorReply-suppression side-effect contract undefined — did the spam message get insert_history'd / SendContext-pushed? Design decision (message must stay visible for SPAM-S04), not an implementation detail.fixedR1R2 — PM confirmed message-visibility; promoted into Decision 4 (insert_history runs, SendContext skipped, early return after insert) + happy-path sequence updated + Chunk 3 assertion added
REV-3minorResolve-of-already-closed-room "success-equivalent" claim asserted but unenumerated.fixedR1R2 — Detail 2.C resolve-response table (2xx / already-closed no-op / 4xx terminal / 5xx retryable)
REV-4minorspam_policy PATCH contract lacked example payloads and a concrete 400 error body.fixedR1R2 — Detail 2.4 example request/success/two-400s/404
REV-5minorProcessSpamVerdictWorker queue/concurrency unstated.fixedR1R2 — Detail 2.C pins Sidekiq default queue (follows ResolveRoomWorker), no cap needed
REV-6minorGET exposure didn't name the entity file.fixedR1R2 — Detail 2.4 names entities/channel_integration/channel_integration.rb + the expose :spam_policy shape
REV-7minorRedis-down skip indistinguishable from lost lock in telemetry.fixedR1R2 — distinct spam_classifier_lock_error event + alert added to Detail 3.A

Review History

CycleDateReviewed revisionScoreVerdictOpen / Fixed
R12026-07-14RFC last_updated 2026-07-14, working tree (uncommitted)7.5PROCEED with notes7 open / 0 fixed
R22026-07-14same-day post-fix revision, working tree (uncommitted)8.5PROCEED0 open / 7 fixed

Delta R1 → R2: 7 findings fixed (2 major, 5 minor), 0 still open, 0 newly found. Score +1.0 (7.5 → 8.5). Both majors (REV-1 prompt, REV-2 suppression) closed with concrete spec, moving TDC/DMS off their caps and clearing the 9.0-band blocker on dangling decisions. Remaining gates are the two by-design external dependencies only.

PRD → RFC Traceability Matrix

PRD: Phase 1 — Silent Classification & Block v1.4 (READY).

PRD itemRFC coverageVerdict
SPAM-S01 (config + 4 ACs)Chunk 1 + Detail 2.4 + 4.A✅ covered (REV-4/REV-6 polish)
SPAM-S01-NEGBranch catalog: "not a new conversation" + retroactivity guard
SPAM-S02 (classify/block + 4 ACs)Chunks 2–4, Decisions 1/2/4/5/8, sequences✅ covered (REV-1/REV-2 gate the spec depth)
SPAM-S02-NEG (WA only)Branch catalog + hook placement
SPAM-S03 (tag + 4 ACs)Chunk 4 fallback + Detail 2.C; is_create_tag: true pinned
SPAM-S03-NEG (never block ambiguous)Verdict dispatch + Data Integrity Matrix row 2
SPAM-S04 (undo)Detail 2.D contract only — by design, Omnichannel-owned✅ as scoped (dependency honest)
SPAM-S05 (alert + NEG)Chunk 5 emitter boundary; NEG spec-asserted✅ as scoped (delivery = dependency)
PRD §5 constraints (3s SLA, flag default, classification scope)Decisions 2/3, Detail 4.A
PRD §10 observability (8 events)Chunk 5 acceptance: "all 8 PRD §10 events observable"
PRD §9/§12 rollout + gates§4 + Detail 4.D
Reverse check (RFC → PRD)Every net-new component (classifier repo, worker, emitter, lock, i18n entry) maps to a story or named decision; no scope creep found

Forward: 12/12 covered (2 as explicitly-bounded dependencies). Reverse: no unjustified additions.

Scorecard (backend — new-feature weighting)

CategoryScoreEvidence (one line)
PRT — PRD Traceability9.0Bidirectional matrix (Detail 1.A/1.C) incl. per-AC → chunk mapping; reverse clean
TDC — Technical Decisions7.59 ADRs with real alternatives; but Decision 2/4 jointly leave reply-suppression side-effects dangling (REV-2), and Decision 1's "prompt in SystemPreference" defers the prompt itself (REV-1)
DMS — Data Model & Schema7.5No-DDL claim is verified and correct; spam_policy JSON shape precise w/ provenance stamps; docked for the unspecified prompt-row content (the one piece of "data" an agent must author blind, REV-1)
ACV — API Contract & Versioning7.0Param table + validation + role gate + wire-compat reasoning present; missing example payloads/error bodies (REV-4), entity exposure vague (REV-6)
DIC — Data Integrity8.0Block-first invariant, NX lock, legacy-bucket separation all explicit; idempotency of resolve asserted but unenumerated (REV-3)
FMC — Failure Modes8.5Full catalog incl. the loud-fail on missing prompt row and the blocked-contact/open-room edge; per-call timeout/retry/fallback pinned; no-DLQ-needed reasoning holds (tag fallback is the terminal sink)
CSS — Concurrency & Scaling7.5Collision map covers the real races (incl. tenant-disable mid-flight); Chunk 6 load gate is honest; worker queue/concurrency unstated (REV-5)
SAS — Security8.0Prompt-injection surface named with a structural mitigation (enum-constrained output, channel-scoped blast radius); role gate reused; no new authz surface; rationale-in-logs-only
MRP — Migration & Rollout9.0No-migration verified; 4-level rollback ladder incl. no-deploy prompt-row disable; config contract complete; stage gates numeric
OBS — Observability8.58 named events w/ properties, numeric alert thresholds incl. page condition, hard FP pause trigger; REV-7 telemetry blind spot on Redis-down
SBC — Service Boundary9.0Detail 2.D is the best section of the RFC — per-step ownership, wire-compat sequencing, per-boundary graceful degradation
CPA — Pattern Alignment9.0Every pattern cites an actually-read reference file (summarization prompt, NX guards, retry conventions, Grape shape)
CDG — Compliance (triggered: PII/message content)7.5Data-processor channel argument sound (same OpenAI path as summarization); block-record reversibility explicit; right-to-delete inherits existing block semantics (implicit — one sentence would make it explicit)
RCS — Resource & Cost (advisory)Strong: unit-economics framing (call cost vs 332 IDR saved), cohort ceiling quantified, spend visible in existing token reporting

Score caps: none triggered (no category < 5.0). 9.0+ band requires no dangling decisions and DMS/DIC/FMC ≥ 8.5 — blocked by REV-1/REV-2.

Decision Closure Assessment

#DecisionVerdict
1Squad-owned Gpt::Completion + SystemPreference promptPartial — backend choice fully resolved w/ alternatives; the prompt content/interpolation contract it defers is the classifier's actual spec (REV-1)
2Inline classification, 3s, fail-openPartial — call-site choice resolved; suppression side-effects on high_confidence unpinned (REV-2, cross-cuts Decision 4)
3Config on remote_config.spam_policyResolved
4ProcessSpamVerdictWorker, block-first, exhaustion→tagPartial — ordering + fallback fully specified; resolve-idempotency classification unenumerated (REV-3); queue unstated (REV-5)
5RESOLVE_AI_SPAM literal + i18nResolved
6Cross-repo provenance contractResolved (dependency honestly bounded, wire-compat reasoned)
7SpamAlertEmitter no-op boundaryResolved
8Redis NX lockResolved (REV-7 is telemetry polish, not the decision)
9No cachingResolved

6 of 9 fully resolved, 3 Partial, 0 Dangling. The Partials share one root: the RFC is strongest where it grounds against existing code, and thinnest on the two artifacts that have no existing-code anchor (the prompt, the suppression semantics) — exactly where an agent has nothing to mirror.

Data Integrity Deep-Dive

Write pathTransaction scopePartial failureIdempotencyConsistencyDuplicate handling
Contact block (hub-core, via HTTP)hub-core-internal (existing)worker retry ×3 → tag fallback; room never resolved without block 2xxhub-core existing ContactBlock dedupeventual (HTTP boundary)re-run safe per existing interactor
Room resolve RESOLVE_AI_SPAMexisting ResolveRoom pathretry re-runs whole chain; block idempotent; already-closed classification unpinned (REV-3)claimed, mechanism unenumeratedeventualtreated-as-success asserted
"Suspected spam" tagChatService (existing)best-effort; spam_tag_failed log onlyexisting 422-create-retryeventualtag re-apply is naturally idempotent
spam_policy config writesingle JSON key updatelast-write-wins + provenance stampn/astrong (single row)n/a

Concurrency Collision Map — verified

RFC Detail 2.B covers all five real collision surfaces including the two easy to miss (tenant-disable mid-flight; verdict worker vs. human takeover). No missing collision found by this review.

API Contract Completeness Check

PATCH /api/v1/channel_integrations/:id (extended): method/path ✅ · auth ✅ (named role gate w/ file:line) · request schema ✅ (fields/types/validation) · response schema ⚠️ (echo asserted, no example) · error taxonomy ⚠️ ("existing error_response shape" — no concrete body) · idempotency ✅ (natural PATCH last-write-wins + stamp) · examples ❌ (REV-4). GET exposure ⚠️ (REV-6).

Async Job / Event Consumer Spec

JobTriggerInput shapeRetryDLQConcurrencyIdempotency keyTimeout
ProcessSpamVerdictWorkerverdict dispatchroom_id, contact_id, category, organization_id×3 + exhaustion hook ✅tag fallback is the terminal sink (reasoned — no DLQ needed) ✅❌ unstated (REV-5)room-scoped chain, idempotent by claim (REV-3)inherits Sidekiq default — acceptable, HTTP timeouts pinned per call

Compliance Trigger Check

Triggered (message content → OpenAI; "spam" labeling of a person). CDG scored (7.5). One-line improvement: state explicitly that right-to-delete/deletion of a blocked contact follows the existing ContactBlock deletion semantics.

Agentic Readiness Deep-Dive

Vague word audit: spec sections are largely free of weasel words; the two instances of deferred language are explicitly ledgered by the RFC itself (§5 items 3–5) rather than hidden — good hygiene, but item 3 is load-bearing (REV-2). Dangling alternatives: none — every "or" in the RFC is resolved or explicitly ledgered. Task decomposition: 6 chunks, each independently mergeable behind the flag, each with assertable acceptance criteria and real repo commands (AGENTS.md:52-79 verified) — strong.

Strengths

  1. Evidence discipline. The Source Verification table is the strongest this reviewer has seen in this repo — every claim has file:line, several double-verified, including the negative claims (RESOLVE_AI_SPAM unused; ai_agent_alert zero code).
  2. Boundary honesty. Detail 2.D states exactly what this RFC does NOT ship, who owns it, and how the system degrades at each boundary — the two external dependencies cannot silently block the core path.
  3. Fail-open coherence. One consistent principle (never degrade the messaging pipeline) applied at every failure point, each with a named log event and customer-impact statement.

Biggest Gaps

  1. The prompt is the spec, and it's missing (REV-1, Decision 1 / Detail 4.A).
  2. Suppression semantics hide a product decision (REV-2, Decision 2/4): the spam message must stay visible in the resolved room for SPAM-S04 review — the RFC neither states this nor specs the insert_history/SendContext behavior that delivers it.
  3. Consumer-ready polish on the config API (REV-4/REV-6): example payloads, error bodies, and the named entity file.

Priority Actions (sequenced)

R2 (2026-07-14): all six actions below applied and verified. Retained as the R1 record.

  1. [REV-1] Add an appendix (or extend Detail 4.A) with the draft classifier prompt: system prompt text, user-prompt template with named placeholders (e.g. {{CATEGORIES}}, {{DEFINITION}}, {{MESSAGE}}), the JSON-output instruction, and 2–3 calibration examples per verdict class. The synthetic accuracy matrix (Chunk 2) then tests against this, not an invention.
  2. [REV-2] Promote suppression semantics into Decision 4: customer message IS inserted into room history (SPAM-S04 requires visibility); SendContext push skipped or no-op'd (no AI conversation will continue); reply path suppressed via early return after history insert. Delete §5 item 3.
  3. [REV-3] Enumerate resolve responses: which ChatService/ResolveRoom outcomes are success-equivalent (already-closed) vs. retryable — one table row in Detail 2.C.
  4. [REV-4/REV-6] Add one example request/response/error-body triple to Detail 2.4 and name the entity file + field.
  5. [REV-5] Pin the worker's queue (default vs. dedicated) and note the concurrency expectation.
  6. [REV-7] Add a distinct spam_classifier_lock_error (or reason field on the skip log) so a Redis outage is visible.

Dangling Decisions

None fully dangling. Three Partials (Decisions 1, 2, 4) — see ledger REV-1/2/3/5.

Open Questions (for the author)

  1. On REV-2: confirm the product intent that a spam-resolved room retains the offending message in its history (this review asserts it must, for the "Bukan spam" flow — confirm with the PM, then spec it).
  2. On REV-5: is there precedent for a dedicated low-priority Sidekiq queue for non-latency-critical action chains in this repo, or does everything ride default? Follow whatever ResolveRoomWorker does.

Implementation Readiness Checklist

R1 snapshot: Unblocked now — Chunks 1, 5, 6. Blocked pending RFC edits — Chunk 2 (REV-1), Chunk 3 (REV-2), Chunk 4 (REV-3).

R2 (post-fix): REV-1/2/3 landed in the RFC → all six chunks unblocked on the chatbot-owned scope. The only remaining external gates are by design: hub-core/hub-chat provenance (Detail 2.D) and the ai_agent_alert emit-body swap — both consumed through stable boundaries that never block the core cost-saving path.