Qontak | AI Agent | AI Spam Gatekeeper — Phase 1: Silent Classification & Block
HEADER BLOCK
| Field | Value |
|---|---|
| PM | Dimas Fauzi Hidayat (Product Manager, Mekari Qontak) |
| PRD Version | 1.4 |
| Status | DRAFT |
| PRD Type | NEW |
| Epic | TBD — add once Epic is created |
| Squad | BOT — Chatbot Squad |
| RFC Link | N/A — pending; internal chatbot-squad TECH RFC for the classifier, see Dependencies |
| Figma Master | N/A — pending; net-new "Spam filter" settings UI has no existing design, see Dependencies |
| Anchor | Yes — AI Spam Gatekeeper — ANCHOR |
| Labels | epic:qontak-chat | module:ai-agent | feature:ai-spam-gatekeeper |
| Last Updated | 2026-07-14 |
Status values:
DRAFT→READY→BUILD→SHIPPED
Table of Contents
- HEADER BLOCK
- 2. One-liner + Problem
- 3. Target Users + Persona Context
- 4. Non-Goals
- Scope Changes
- 5. Constraints
- 6. New Features
- 7. API & Webhook Behavior
- 8. System Flow + User Stories
- 9. Rollout
- 10. Observability
- 11. Success Metrics
- 12. Launch Plan & Stage Gates
- 13. Dependencies
- 14. Key Decisions + Alternatives Rejected
- 15. Open Questions
- PRD CHANGELOG
2. One-liner + Problem
One-liner: Let a tenant define spam for their WhatsApp inbox so the AI can silently resolve and block confirmed spam before any billable reply.
Problem:
From 1 October 2026, every AI Agent reply to an inbound WhatsApp conversation becomes a billable Meta service message with no volume discount — including replies sent to spam. Internal analysis (Bifrost, meta-whatsapp-pricing-oct-2026) sizes this at ~14–15B IDR/month in new exposure across Qontak's client base. Today, nothing in Qontak stops the AI Agent from answering a spam conversation exactly like a real customer: the only existing spam mechanism (chatbot BE's spam_protection) is a dormant heuristic (repeated-intent counting) with zero tenant-facing UI, so no tenant can see it, configure it, or trust it. Tenants running production AI Agents — the segment most exposed to this new cost — will pay to have their bot politely respond to loan pitches, phishing attempts, and bulk blasts, with no way to define what "spam" means for their business or to stop paying for it once a sender is confirmed spam.
3. Target Users + Persona Context
| Persona | Role | Goal | Pain | Workaround |
|---|---|---|---|---|
| Primary — Tenant Admin | Owner/admin of a Qontak WhatsApp channel, responsible for inbox quality and Meta messaging cost | Stop the AI Agent from spending billable replies on spam, without risking a real customer getting silently blocked | No way to define "spam" for their business today; every inbound conversation gets a normal AI reply regardless of content | Manually notices a spam contact in the inbox after the fact (post-cost) and blocks them one at a time from the room information panel |
| Secondary — CS Supervisor | Oversees the shared inbox and the human agents backing up the AI Agent | Trust that every AI auto-block was actually spam, and be able to review or reverse a wrong call within minutes | No visibility today into which contacts the AI has silently blocked, or why — the existing spam_protection heuristic acts with zero audit surface | None — no auto-block mechanism exists yet for a supervisor to review |
4. Non-Goals
- AI clarifying interrogation (asking the sender up to 3 questions before deciding) — Phase 2. Phase 1 classifies silently, in one shot, with no outbound message to the suspected sender.
- Meta-level blocking (Block Users API — suppressing delivery at the WhatsApp network level) — Phase 2. Phase 1's Qontak-level block already stops billing; Meta's API adds delivery suppression on top, not required to hit the cost-avoidance goal.
- A dedicated "spam review queue" screen. Phase 1 surfaces the "suspected spam" tag using the existing tag/label UI in the inbox — it does not ship a new triage-queue surface. A dedicated queue is Phase 2.
- Tenant spam-definition feedback/tuning loop (auto-suggesting definition improvements from false-positive corrections) — Phase 2.
- Non-WhatsApp channels. Phase 1 only classifies inbound WhatsApp conversations; Instagram, email, and other Omnichannel channels are out of scope.
- Retroactive classification of historical conversations. Only new inbound conversations arriving after a tenant enables the feature are classified — existing open/resolved rooms are never re-evaluated.
- Per-AI-Agent spam configuration. Config is channel/tenant-level only (see Section 5), not attached to individual AI Agents.
- Auto-blocking on ambiguous or low-confidence verdicts. The system never blocks except on a high-confidence match — this is a hard design constraint, not a configurable tenant option in Phase 1.
Scope Changes
Engineering surfaces this PRD touches (controlled vocab). Kept in sync with the scope_changes frontmatter above.
- Backend —
chatbot: new inbound-hook classifier call (reusingRepositories::Gpt::Completion), new channel-levelspam_policyconfig (categories, free-text definition, confidence action mapping), newRESOLVE_AI_SPAMclosed_reason value, "suspected spam" tag application via existingAssignTagWorker(withis_create_tag: true, see Section 7 Behavior 3), new supervisor-alert trigger event, plus newsource/reasonfields added to the existingHub::ChatService::Contacts#blockcall body (today it sends{ room_id }only — confirmed via grounding,lib/hub/chat_service/contacts.rb:22-25).hub-core: not pure reuse — confirmed net-new work via grounding (2026-07-14):Repositories::Contacts::Block::Create#build_paramsmust be extended to persist thesourceparam it already accepts but currently drops, andEntities::ContactBlockneeds a newsourceattribute exposed through the endpointhub-chatreads. - Frontend —
chatbot-fe: new "Spam filter" section under Chatbot settings → AI configuration (category toggles, free-text definition field, enable/disable toggle; note the tab list is 2 hardcoded elements, not array-driven — a third tab needs a manual addition in bothAiAssistTabList.vueandai-assist.vue).hub-chat: not pure relabel — confirmed net-new work via grounding:ModalBlockContact.vue/ModalUnblockContact.vue/ContactInfo.vuecurrently render zero block-attribution data (generic "Contact is blocked" string only) and need a new field wired through before they can branch onsource === 'ai_agent'for the "Bukan spam" copy (see Section 8, Story SPAM-S04). - Data — spam-classification verdicts and auto-block events are logged for the initiative's success metrics (messages avoided, false-positive rate, suspected-spam tag precision); no new datamart in Phase 1, reporting reads directly from
rooms.closed_reasonandContactBlockLog.
5. Constraints
- Feature flag:
ai_spam_gatekeeper | default: OFF. Stored per-channel-integration (same storage pattern as the existingspam_protectionremote_config), so a tenant must explicitly enable it from Chatbot settings → AI configuration → Spam filter. No org is auto-enrolled. - Plan/tier availability: All Qontak plans with an active WhatsApp channel integration. Not gated behind a premium tier — the tenants most exposed to the Oct 2026 pricing shock are frequently on smaller plans, so gating the mitigation behind a paywall would be self-defeating.
- Platform availability: Web only for the settings UI (chatbot-fe). This is a scope boundary, not an access-denial case — no mobile Spam filter screen exists to deny access to; a tenant admin configures the feature from the web app only. Enforcement itself is entirely server-side at the inbound message pipeline — it is not device- or platform-dependent for the end customer.
- Classification scope: The classifier only runs on the first inbound message of a NEW room (i.e.,
history.blank?at the routing hook) — never on subsequent messages within an already-classified conversation. This bounds the added AI-call volume to one call per new conversation, not per message. - Classifier latency SLA: Hard timeout of 3 seconds on the classification call. On timeout or any classifier error, the system fails open — treats the conversation as
cleanand proceeds through the normal reply flow with zero added delay to the customer. This SLA is non-negotiable: the spam classifier must never be the reason a real customer's message goes unanswered or delayed. - Confidence threshold for auto-block: Only a
high_confidenceverdict from the classifier triggers auto-resolve + block.ambiguousandcleanverdicts never trigger a block in Phase 1 (see Section 8 for the full verdict contract).
5.1. Data Lifecycle
No new retained-artifact type is introduced. The classification call reuses Repositories::Gpt::Completion, which already logs each request/response (_save_log) — spam-classification calls inherit that repo's existing GPT completion-log retention policy, unchanged. The "suspected spam" tag reuses the existing tag data model (AssignTagWorker) with no additional retention window — it persists exactly as long as any other room tag under the platform's existing room/tag retention, and is cleared only by explicit tenant/agent action (removing the tag) or Phase 2's review-queue resolution.
6. New Features
Screen: Spam Filter settings
URL path: /settings/ai-assist (existing route) — new "Spam filter" tab added alongside the existing Conversation / Response tabs in AiAssistTabList.vue.
Figma: Pending — no spam/moderation UI pattern exists anywhere in chatbot-fe or the qontak-designer prototype today (confirmed P0 design gap). See Section 13 Dependencies. Stitch prompts to be generated once this PRD reaches READY.
Access: Tenant Admin roles with existing Chatbot settings permission only — same access model as the rest of /settings/ai-assist (see SPAM-S01 Permission Model, Section 8). A tenant with no active WhatsApp channel integration never sees this tab rendered, since /settings/ai-assist itself is scoped per-channel (per S4 §Plan/tier availability).
Component tree:
SpamFilterTab(new) — the tab's root panelEnableToggle— master on/off switch bound to theai_spam_gatekeeperflag (Section 5); when OFF, the rest of the panel is disabled/greyed but still visibleCategoryToggleList— 4 pre-built universal spam categories, each a labeled toggle with a one-line example: "Unsolicited B2B pitches", "Scam / phishing", "Bot / bulk blasts", "Gibberish / no intent"SpamDefinitionField— free-text textarea, "Your definition of spam", with placeholder examples (e.g. "Job applications sent to our sales line, requests outside Jakarta")SaveBar— Save / Cancel actions, disabled until a change is made
All 4 UI states:
- Empty: First time a tenant opens the tab —
EnableToggleOFF, all 4 category toggles unchecked,SpamDefinitionFieldblank with placeholder text visible,SaveBarhidden (no changes to save). - Loading: Skeleton rows for the toggle list and a shimmer bar for the textarea while the current channel config is fetched.
- Error: Fetch or save failure shows an inline banner ("Couldn't load/save your spam filter settings — try again") with a Retry action; the panel keeps the last successfully loaded state rather than reverting to Empty.
- Success: Config loaded/saved reflects the tenant's actual settings; when
EnableToggleis ON, a confirmation hint renders below it: "The AI now silently reviews new conversations against your spam definition."
Existing UI reused (relabeled, no new component): the "Bukan spam" undo action reuses hub-chat's existing ModalUnblockContact.vue (block/unblock system, already shipped) — Phase 1 only relabels the copy shown when the block's source is ai_agent; see Section 8, Story SPAM-S04.
📊 UI State Diagram — Spam Filter Settings
stateDiagram-v2
[*] --> Loading: Tenant opens Spam filter tab
Loading --> Empty: First-time tenant, no config saved yet
Loading --> Success: Existing config fetched
Loading --> Error: Fetch fails
Empty --> Success: Tenant toggles ON, selects categories, clicks Save
Success --> Success: Tenant edits definition/categories, clicks Save
Success --> Error: Save request fails
Error --> Loading: Tenant clicks Retry
Success --> [*]: Config persisted for the channel
📊 Component Tree — Spam Filter Settings
graph TD
Tab["SpamFilterTab (new)<br/>root panel"]
Tab --> Enable["EnableToggle<br/>master on/off, bound to ai_spam_gatekeeper flag"]
Tab --> Cats["CategoryToggleList<br/>4 pre-built universal categories"]
Tab --> Def["SpamDefinitionField<br/>free-text tenant definition"]
Tab --> Save["SaveBar<br/>Save/Cancel, disabled until change"]
7. API & Webhook Behavior
| # | Behavior | Entity Affected | Triggered By | Expected Behavior | Failure Behavior |
|---|---|---|---|---|---|
| 1 | Inbound conversation classified | Room, Contact | First inbound message of a NEW room (history.blank?), when ai_spam_gatekeeper is ON for that channel | Chatbot BE calls Repositories::Gpt::Completion with the tenant's category toggles + free-text definition; receives a verdict: clean, ambiguous, or high_confidence (+ matched category if not clean) | Timeout at 3s or any classifier error → fail open: verdict forced to clean, normal reply flow proceeds with zero added delay, error logged as classifier_error for Observability (Section 10) |
| 2 | High-confidence spam auto-block | Room, Contact | Classifier verdict = high_confidence | 1) Contact block called first (source: 'ai_agent', reason: 'spam:<category>') via the existing hub-core block interactor — requires new source/reason fields added to the chatbot→hub-core block-call payload, and hub-core to persist + expose source (confirmed net-new plumbing via grounding, not pure reuse; see Scope Changes) 2) Only on block success, room is resolved (closed_reason: RESOLVE_AI_SPAM) via the existing resolve path 3) A system message is written to the room 4) A supervisor alert is emitted (Behavior 4) | If the block call fails, the room is not resolved (fail-open) — the conversation proceeds through the normal reply flow, the failure is retried asynchronously up to 3 times, and if still unresolved after retries the room is instead tagged "suspected spam" (Behavior 3) rather than left in a silent half-applied state |
| 3 | Ambiguous verdict tagging | Room | Classifier verdict = ambiguous, or a Behavior-2 block retry exhaustion | Room receives a "Suspected spam" tag via the existing AssignTagWorker, called with is_create_tag: true (required — without it, the first-ever application of this tag on any org 422s per the existing _spam_protection_action precedent, process_incoming_message_with_resolve.rb:804); the conversation proceeds completely normally through the regular AI/bot-tree reply flow — tagging never blocks or delays a reply | Tag-write failure is logged but never blocks or retries the customer-facing reply — tagging is best-effort only |
| 4 | Supervisor alert on auto-block | Notification | A Behavior-2 auto-block completes successfully | An alert is emitted via the existing ai_agent_alert notification category (reused from the AI Agent Live Monitoring initiative) carrying a room deep-link and the matched spam category | If notification-service is unavailable, the block/resolve in Behavior 2 still completes — the alert is best-effort and non-blocking of the core spam action; failure is logged for this initiative's own Observability |
| 5 | "Bukan spam" undo | Contact, Room | Agent/supervisor clicks "Bukan spam" in the relabeled hub-chat unblock modal | The existing unblock interactor runs (contact.is_blocked = false), the room is unparked, and a system message is logged — fully reused, unchanged behavior from the existing block/unblock system | Standard existing unblock failure behavior (reused, unchanged) — out of scope to redefine in this PRD |
📊 API Sequence — High-Confidence Spam Auto-Block (Behavior 2)
sequenceDiagram
participant Chatbot as Chatbot BE
participant HubCore as hub-core
participant HubChat as hub-chat (FE)
participant Notif as notification-service
Chatbot->>HubCore: POST /api/core/v1/contact_block<br/>{ room_id, source: 'ai_agent', reason: 'spam:<category>' }
Note over HubCore: Net-new fields — today's call sends { room_id } only (Section 13)
alt block succeeds
HubCore-->>Chatbot: 200 OK
Chatbot->>Chatbot: Resolve room (closed_reason: RESOLVE_AI_SPAM)
Chatbot->>Notif: Emit ai_agent_alert
Notif-->>Chatbot: Delivery ack (best-effort, non-blocking)
Note over HubChat: Later — supervisor opens ContactInfo,<br/>reads source: 'ai_agent' (net-new field),<br/>renders "Ditandai spam oleh AI" copy
else block fails
HubCore-->>Chatbot: 4xx/5xx or timeout
Chatbot->>Chatbot: Retry (up to 3x)
Note over Chatbot: On continued failure, room falls through<br/>to "Suspected spam" tag path (Behavior 3)
end
8. System Flow + User Stories
8.1. System Flow
Actors: Customer (WhatsApp sender) · Chatbot BE (inbound pipeline) · Classifier (Gpt::Completion) · hub-core (block/resolve) · notification-service · Supervisor · CS Agent
- Customer sends the first message of a brand-new WhatsApp conversation.
- Chatbot BE's inbound pipeline receives it and reaches the new-conversation routing branch (
history.blank?). - Chatbot BE checks whether
ai_spam_gatekeeperis enabled for this channel.- OFF → skip directly to step 9 (normal flow, unchanged).
- ON → continue to step 4.
- Chatbot BE calls the Classifier with the tenant's spam policy (category toggles + free-text definition) and the message content, with a 3-second timeout.
- Decision — did the Classifier respond in time?
- No (timeout/error) → verdict forced to
clean,classifier_errorlogged → go to step 9. - Yes → verdict is one of
clean,ambiguous,high_confidence→ continue.
- No (timeout/error) → verdict forced to
- Decision — verdict =
high_confidence?- Yes → Chatbot BE calls hub-core's block interactor (
source: 'ai_agent',reason: 'spam:<category>'). 7. Decision — did the block call succeed?- Yes → room is resolved (
closed_reason: RESOLVE_AI_SPAM), a system message is written, a supervisor alert is emitted (step 10). End — zero billable reply sent for this conversation. - No → retried asynchronously up to 3 times; if still failing, falls through to step 8 (tag instead of silently dropping).
- Yes → room is resolved (
- No → continue to step 8.
- Yes → Chatbot BE calls hub-core's block interactor (
- Decision — verdict =
ambiguous(or step-7 retry exhaustion)?- Yes → room is tagged "Suspected spam" (best-effort, non-blocking) → continue to step 9.
- No (verdict =
clean) → continue directly to step 9.
- Normal reply flow proceeds completely unmodified — AI Agent or bot-tree replies exactly as it does today.
- (Triggered from step 7's success branch) notification-service emits a real-time alert to the org's configured Supervisor with a room deep-link and the matched category.
- (Separate, later flow) If a Supervisor or CS Agent determines an auto-blocked contact was not actually spam, they open the room and click "Bukan spam" in the relabeled unblock modal; the existing unblock interactor reverses the block and unparks the room.
📊 System Flow — AI Spam Gatekeeper (Phase 1)
sequenceDiagram
actor Customer
participant Chatbot as Chatbot BE (inbound pipeline)
participant Classifier as Classifier (Gpt::Completion)
participant HubCore as hub-core (block/resolve)
participant Notif as notification-service
actor Supervisor
actor Agent as CS Agent
Customer->>Chatbot: First message of a NEW room
Chatbot->>Chatbot: Check ai_spam_gatekeeper flag
alt flag OFF
Chatbot->>Customer: Normal AI/bot-tree reply (unchanged)
else flag ON
Chatbot->>Classifier: Classify (spam policy + message), timeout 3s
alt timeout or error
Note over Chatbot: Fail open — verdict = clean, log classifier_error
Chatbot->>Customer: Normal AI/bot-tree reply (unchanged)
else classifier responds
Classifier-->>Chatbot: verdict (clean / ambiguous / high_confidence)
alt verdict = high_confidence
Chatbot->>HubCore: Block contact (source: ai_agent, reason: spam:<category>)
alt block succeeds
HubCore-->>Chatbot: Block OK
Chatbot->>Chatbot: Resolve room (closed_reason: RESOLVE_AI_SPAM) + system message
Chatbot->>Notif: Emit ai_agent_alert (room deep-link, category)
Notif-->>Supervisor: Real-time alert
Note over Customer: No reply sent — zero billable message
else block fails (after 3 retries)
Note over Chatbot: Falls through to ambiguous tagging path
Chatbot->>Chatbot: Tag room "Suspected spam"
Chatbot->>Customer: Normal AI/bot-tree reply (unchanged)
end
else verdict = ambiguous
Chatbot->>Chatbot: Tag room "Suspected spam" (best-effort)
Chatbot->>Customer: Normal AI/bot-tree reply (unchanged)
else verdict = clean
Chatbot->>Customer: Normal AI/bot-tree reply (unchanged)
end
end
end
opt Wrong auto-block discovered later
Agent->>HubCore: Click "Bukan spam" (relabeled unblock modal)
HubCore-->>Agent: Contact unblocked, room unparked, system message logged
end
8.2. User Stories
| User Story | Importance | Mockup | Technical Notes | Acceptance Criteria |
|---|---|---|---|---|
| [SPAM-S01] — Tenant defines and enables the spam policy As a Tenant Admin, I want to define what spam means for my WhatsApp inbox and turn on AI spam detection, so that the AI can start protecting me from paying to answer spam. | Must Have | Figma: Pending — see Stitch prompt (Section 6) | Data Fields: • ai_spam_gatekeeper (boolean, required) — source: User input (settings toggle)• spam_categories (array of enum: b2b_pitch, scam_phishing, bulk_blast, gibberish; required, multi-select) — source: User input• spam_definition (string, optional, free text) — source: User input• channel_integration_id (string, required) — source: Auth session / route contextBefore-After Behavior: Before: no spam configuration exists anywhere in the product. After: a tenant can enable AI Spam Gatekeeper for a channel, select universal categories, and add a free-text definition from Chatbot settings → AI configuration → Spam filter. | — Happy Path — • AC-1: Given a tenant admin on the Spam filter tab with the feature OFF (default per S5 §Feature flag), when they toggle it ON, select ≥1 category, and click Save, then the config is persisted for that channel and classification begins on the next new inbound conversation. • AC-2: Given the feature is already ON with saved config, when the admin edits the free-text definition and clicks Save, then the update applies to all subsequent classifications — already-open conversations are never retroactively reclassified (per Non-Goals). • AC-3: Given a tenant enables the feature with category toggles selected but no free-text definition, when they Save, then the save succeeds (the field is optional) and a hint is shown: "Add your own definition to catch spam specific to your business." • AC-4: Given a tenant's channel has no active WhatsApp integration (per S4 §Plan/tier availability — every tier is eligible, but only with an active WhatsApp channel), when they navigate to /settings/ai-assist, then the Spam filter tab is not rendered at all, consistent with the existing settings permission model — there is no "disabled" state to reach.— Error / Unhappy Path — • ERR-1: Given the admin clicks Save, when the backend save request fails, then the UI shows the Error state (Section 6) with a Retry action, the previously saved config remains active and unchanged, and spam_config_save_failed is logged.— Permission Model — • CAN: Tenant Admin roles with existing Chatbot settings access • CANNOT: Agents without settings access; end customers • Unauthorized: Spam filter tab not rendered (existing settings permission model, unchanged) — UI States — • Loading: skeleton rows while config fetches • Empty: all toggles off, blank definition, Save bar hidden • Error: inline banner + Retry, last-known-good config retained • Success: reflects saved state; ON shows the "AI now silently reviews…" hint |
| [SPAM-S01-NEG] — No retroactive classification (Guard Rail — from Non-Goals) As a tenant admin, when I enable the Spam Gatekeeper, then previously existing conversations are never retroactively classified or blocked. | Guard Rail | — | — | NEG-1: Given a room already exists (opened before this feature was enabled or before this conversation started), when the tenant enables ai_spam_gatekeeper, then that pre-existing room is never passed to the classifier — only conversations whose first inbound message arrives after enablement are evaluated (per S5 §Classification scope). |
| [SPAM-S02] — AI classifies new conversations and auto-blocks high-confidence spam As a Tenant Admin, I want the AI to silently classify new conversations and automatically resolve + block confirmed spam senders, so that I never pay for an AI reply to spam again. | Must Have | — (system story; resulting state is visible in the existing room UI) | Data Fields: • classifier_verdict (enum: clean/ambiguous/high_confidence) — source: Classifier response• matched_category (string, optional) — source: Classifier response• room_id, contact_id — source: existing room/contact context• closed_reason (set to RESOLVE_AI_SPAM) — source: system-set• block_source ('ai_agent'), block_reason ('spam:<category>') — source: system-setBefore-After Behavior: Before: every new conversation gets a normal reply, spam included. After: when ON, the first message of every new conversation is classified in ≤3s; a high_confidence verdict auto-resolves the room and blocks the contact before any reply is sent — zero billable messages for that conversation. | — Happy Path — • AC-1: Given ai_spam_gatekeeper is ON and a new conversation's first message clearly matches an enabled category, when the classifier returns high_confidence, then the contact is blocked (source: 'ai_agent', reason: 'spam:<category>') per S7 Behavior 2 — which requires the chatbot→hub-core block-call payload to carry these new fields, and hub-core to persist/expose source (net-new plumbing, confirmed via grounding — not pre-existing reuse) — the room is resolved (closed_reason: RESOLVE_AI_SPAM), and no AI/bot-tree reply is ever sent.• AC-2: Given the classifier returns clean for a normal inquiry, when the conversation proceeds, then the AI/bot-tree replies exactly as it does today, unmodified, with no added latency beyond the classification call.• AC-3: Given two messages arrive for the same brand-new room in rapid succession before classification completes, when the first message triggers a high_confidence block, then the second message is dropped by the existing inbound short-circuit (hub-core customer_send_message.rb:76-84, reused unchanged) — no duplicate classification call for the same room.• AC-4: Given the classifier does not respond within 3 seconds (per S5 §Classifier latency SLA), when the timeout fires, then the verdict is forced to clean, classifier_error is logged, and the conversation proceeds with no perceptible delay to the customer.— Error / Unhappy Path — • ERR-1: Given a high_confidence verdict, when the downstream block call to hub-core fails, then the room is NOT resolved (per S7 Behavior 2), the failure retries asynchronously up to 3 times, and on continued failure the room falls through to the "suspected spam" tag path (SPAM-S03) instead of being silently dropped.— Permission Model — • CAN: System-only action, gated entirely by the ai_spam_gatekeeper flag and classifier verdict• CANNOT: N/A • Unauthorized: N/A — UI States — • N/A — backend/system story; result is a resolved room with a spam system message in the existing room UI |
| [SPAM-S02-NEG] — Non-WhatsApp channels are never classified (Guard Rail — from Non-Goals) As a tenant admin, when an inbound message arrives on a non-WhatsApp channel, then it is never passed to the spam classifier. | Guard Rail | — | — | NEG-1: Given an inbound message arrives on Instagram, email, or any other Omnichannel channel, when it is processed, then it is never routed to the spam classifier and ai_spam_gatekeeper has no effect on it — Phase 1 classifies WhatsApp conversations only (per Non-Goals). |
| [SPAM-S03] — Ambiguous conversations are tagged, never blocked As a Tenant Admin, I want conversations the AI isn't sure about to stay open and simply get flagged, so that I never lose a real customer to a wrong auto-block. | Must Have | — (reuses existing inbox tag UI) | Data Fields: • tag_name (fixed: "Suspected spam") — source: system-set• room_id — source: existing contextBefore-After Behavior: Before: no ambiguous-handling concept exists. After: an ambiguous verdict adds a non-blocking "Suspected spam" tag to the room and the conversation proceeds through the normal reply flow untouched. | — Happy Path — • AC-1: Given ai_spam_gatekeeper is ON and the classifier returns ambiguous, when tagging completes, then the room is labeled "Suspected spam" using the existing tag system (AssignTagWorker called with is_create_tag: true per S7 Behavior 3, so the tag auto-provisions on an org's first use), and the AI/bot-tree reply is sent exactly as it would be for a clean verdict — the tag never delays or blocks the reply.• AC-2: Given a room is already tagged "Suspected spam", when a supervisor or agent opens it, then they see the tag alongside the normal conversation view (existing tag-rendering UI, unchanged). • AC-3: Given a Behavior-2 block call exhausts its 3 retries (per S7), when exhaustion is detected, then the room falls through to this same tagging path rather than being left in an undefined state. • AC-4: Given a room is tagged "Suspected spam" and a supervisor or agent determines it is not spam, when they remove the tag using the existing tag-removal UI (same generic tag-removal control every room tag already has — no new component), then the tag is cleared and no further AI spam action is taken on that room; this is the tag's full undo path, distinct from the contact-block undo in SPAM-S04. — Error / Unhappy Path — • ERR-1: Given the tag-write call to AssignTagWorker fails, when the failure occurs, then it is logged (spam_tag_failed) but never blocks, delays, or retries against the customer-facing reply — tagging is best-effort only (per S7 Behavior 3).— Permission Model — • CAN: System-only trigger; tag visible to any role that can already view the room • CANNOT: N/A • Unauthorized: N/A — UI States — • N/A — reuses existing tag rendering; no new UI states introduced |
| [SPAM-S03-NEG] — Never auto-block on ambiguous (Guard Rail — hard design constraint, S5 §Confidence threshold) As a tenant admin, when the classifier returns an ambiguous verdict, then the contact is never blocked and the room is never auto-resolved. | Guard Rail | — | — | NEG-1: Given a classifier verdict of ambiguous, when the system processes it, then no call to the hub-core block interactor is made and closed_reason is never set to RESOLVE_AI_SPAM for that room — only the "Suspected spam" tag is applied (per S5 §Confidence threshold for auto-block). |
| [SPAM-S04] — Supervisor/agent reverses a wrong auto-block ("Bukan spam") As a CS Supervisor, I want to undo an AI auto-block that turns out to be a real customer, so that I can recover the relationship within minutes, not lose it permanently. | Must Have | Figma: Pending — see Stitch prompt (relabel of existing hub-chat modal, no new frame) | Data Fields: • contact_id — source: room context• unblock_reason (optional) — source: User input (existing modal field, if present)Before-After Behavior: Before: the existing block/unblock modal shows generic copy regardless of who blocked the contact or why, and — confirmed via grounding — the underlying API never captures or exposes any block-attribution data ( Entities::ContactBlock has no source field; the modals fetch nothing beyond a static "Contact is blocked" string). After: when a block's source is ai_agent, the modal shows AI-attributed copy ("Ditandai spam oleh AI — [category], [timestamp]") and a "Bukan spam" action label instead of the generic "Unblock". This is not a pure relabel — it requires hub-core to persist source (Repositories::Contacts::Block::Create#build_params currently accepts but drops it) and expose it via Entities::ContactBlock + the endpoint hub-chat reads, before the FE work is even possible. See Section 13 Dependencies. | — Happy Path — • AC-1: Given a contact was auto-blocked by the AI ( source: 'ai_agent'), when a supervisor or agent opens the block info and clicks "Bukan spam", then the existing unblock interactor runs, contact.is_blocked is set to false, the room is unparked, and a system message records the reversal.• AC-2: Given the block info modal is open for an AI-sourced block, when it renders, then it shows the matched category and block timestamp in plain language, not the internal spam:<category> enum value (per Section 14 decision on human-readable provenance).• AC-3: Given a contact was blocked manually by a human agent ( source: 'user', pre-existing behavior), when the modal renders, then it shows the existing generic "Unblock" copy, unchanged — AI-attributed relabeling only applies to source: 'ai_agent' blocks.• AC-4: Given a tenant has zero, one, or many AI-auto-blocked contacts, when a supervisor views any single contact's block info, then the "Bukan spam" action always operates on exactly that one contact — there is no bulk-undo in Phase 1 (explicitly out of scope; a supervisor working through a spam wave reverses contacts one at a time, same as the existing manual-block undo flow). — Error / Unhappy Path — • ERR-1: Given the unblock action fails (existing failure mode, reused unchanged), when it occurs, then the modal shows the existing generic error message ("Couldn't unblock this contact — try again") with a Retry action, and the contact remains blocked until the retry succeeds — this is the same failure UI the pre-existing manual-unblock flow already uses (per S7 Behavior 5), not a new state. — Permission Model — • CAN: Roles with existing block/unblock permission (Supervisor, Agent — unchanged) • CANNOT: Roles without block/unblock permission today (unchanged) • Unauthorized: "Bukan spam" action not rendered (existing permission enforcement, unchanged) — UI States — • Reuses existing modal states (loading/error/success) — only copy/labels differ for source: 'ai_agent' blocks |
| [SPAM-S05] — Supervisor receives a real-time alert on every auto-block As a CS Supervisor, I want to be notified in real time whenever the AI auto-blocks a contact as spam, so that I can review high-stakes automated decisions as they happen, not days later. | Must Have | — (delivered via existing notification-service surface) | Data Fields: • alert_category ('ai_agent_alert', reused) — source: system-set• room_deep_link, matched_category — source: system-generated / Story SPAM-S02 verdict• supervisor_sso_id — source: existing supervisor-resolution mechanism (AI Agent Live Monitoring initiative)Before-After Behavior: Before: no visibility into AI auto-blocks exists beyond manually checking the block log. After: every high-confidence auto-block fires a real-time push alert to the org's configured supervisor via the existing notification-service chat channel. | — Happy Path — • AC-1: Given a Behavior-2 auto-block completes successfully, when the block+resolve finishes, then an alert is emitted to the org's configured supervisor(s) via the existing ai_agent_alert category, carrying a room deep-link and the matched category.• AC-2: Given an org has no supervisor configured yet (open dependency, see Section 13), when an auto-block occurs, then the block/resolve still completes normally (alert is best-effort and non-blocking, per S7 Behavior 4) and the missing-supervisor condition is logged, not surfaced as a user-facing error. • AC-3: Given a tenant experiences a burst of auto-blocks in a short window, when multiple alerts would fire in quick succession, then the existing per-room throttle/dedup mechanism built for the Live Monitoring initiative (reused, not rebuilt here) applies, preventing alert-fatigue of its own. — Error / Unhappy Path — • ERR-1: Given notification-service is unavailable when an auto-block completes, when the alert emit call fails, then the failure is logged and does not retry indefinitely against the block/resolve action already taken (per S7 Behavior 4) — this story never blocks SPAM-S02's core action. — Permission Model — • CAN: Configured supervisor role(s) only (reuses Live Monitoring's supervisor-resolution mechanism) • CANNOT: Non-supervisor roles do not receive this alert by default • Unauthorized: N/A — notification delivery, not an access-controlled action — UI States — • N/A — delivered through the existing notification-service surface (bell + push); no new UI states |
| [SPAM-S05-NEG] — Alerts fire only on confirmed auto-blocks, never on suspected-spam tags (Guard Rail) As a CS Supervisor, when the classifier returns an ambiguous verdict, then no supervisor alert is fired. | Guard Rail | — | — | NEG-1: Given a classifier verdict of ambiguous and the room is tagged "Suspected spam" (SPAM-S03), when tagging completes, then no ai_agent_alert is emitted — alerts fire exclusively on Behavior-2 successful high-confidence auto-blocks (SPAM-S02), never on the lower-confidence tag path, to avoid alert-fatigue on every ambiguous case. |
Dependencies: SPAM-S02 depends on SPAM-S01 · SPAM-S03 depends on SPAM-S01, SPAM-S02 · SPAM-S04 depends on SPAM-S02 · SPAM-S05 depends on SPAM-S02, and externally on the AI Agent Live Monitoring ai_agent_alert category + supervisor-resolution mechanism (blocking, see Section 13)
9. Rollout
| Stage | Audience | Duration | What must be true to advance |
|---|---|---|---|
| 1 — Internal QA | Chatbot squad + PM, internal test channels only | 1 week | 100% of a synthetic test matrix (clean / ambiguous / high-confidence spam samples per category) classifies as expected; zero unhandled exceptions; forced-timeout fail-open verified end-to-end |
| 2 — Closed Beta | 3–5 design-partner tenants selected for high spam volume (per Tableau segment data), manually enabled by the squad | 2 weeks minimum | False-positive rate ("Bukan spam" clicks ÷ auto-blocks) ≤5%; zero P0 incidents; suspected-spam tag precision showing early positive signal (≥30%) |
| 3 — Limited GA (self-serve, "Beta" labeled) | All tenants with an active WhatsApp channel integration may self-enable from Chatbot settings | 3 weeks | False-positive rate ≤3%; classifier error/timeout rate <2% of classification attempts; supervisor alert delivery success ≥95% (where a supervisor is configured) |
| 4 — General Availability | All eligible tenants; "Beta" label removed | Ongoing | Stage 3 thresholds sustained for 1 full week with no regression — targeted to land ahead of the 1 October 2026 Meta billing start |
Feature flag gating: Every stage in this table is gated by the ai_spam_gatekeeper flag (per S4 §Feature flag: ai_spam_gatekeeper | default: OFF) — Stages 2–4 differ only in how many tenants are permitted to flip it ON (curated invite → self-serve), never in the flag's default state or mechanics.
Semantic-regression rollback: Because classification is AI-driven, a "worse" model/prompt version can look structurally valid while silently over- or under-triggering. The named mechanism: the false-positive proxy metric (spam_undo ÷ spam_auto_block, Section 10.1) is reviewed daily during Closed Beta and weekly for 8 weeks post-Limited-GA; if it exceeds 2% in any window, new-tenant enrollment onto ai_spam_gatekeeper is paused immediately (a hard, not advisory, trigger) until the classifier/prompt is root-caused. This does not retroactively reverse already-completed blocks (see Story SPAM-S04 for the per-contact undo path) — it stops the bleeding forward.
Legacy coexistence: The existing spam_protection heuristic feature (repeated-intent counting, closed_reason: 'SPAM', no source/reason attribution) remains active in parallel unless a tenant explicitly disables it — this PRD does not turn it off. Rooms and blocks it produced before or alongside this feature carry the legacy 'SPAM' reason with no source field, and must be treated as a separate legacy bucket in all Phase 1 reporting (Section 10, Section 11) — they are never merged into or counted toward this initiative's RESOLVE_AI_SPAM / source: 'ai_agent' metrics, to avoid inflating Phase 1's measured impact with blocks the AI Spam Gatekeeper didn't cause.
10. Observability
| Event | Trigger | Key Properties |
|---|---|---|
spam_classification_run | Every classifier call (Section 7, Behavior 1) | room_id, channel_integration_id, verdict, matched_category, latency_ms |
spam_classifier_error | Classifier timeout (3s) or error → fail-open | room_id, error_type, latency_ms |
spam_auto_block | Successful high-confidence block + resolve (Behavior 2) | room_id, contact_id, matched_category |
spam_auto_block_failed | Block call fails after 3 retries, falls through to tag | room_id, retry_count, fallback_action: 'tagged' |
spam_tag_applied | "Suspected spam" tag written (Behavior 3) | room_id, matched_category, source: 'ambiguous' | 'block_retry_exhausted' |
spam_undo | "Bukan spam" clicked (Behavior 5) | room_id, contact_id, original_matched_category |
spam_config_saved | Tenant saves Spam filter settings | channel_integration_id, categories_enabled, has_definition: bool, flag_state |
spam_alert_emitted / spam_alert_failed | Supervisor alert sent or failed (Behavior 4) | room_id, supervisor_sso_id, delivery_status |
Dashboard owner: BOT — Chatbot Squad.
Alert thresholds:
spam_classifier_errorrate >5% of classification attempts in a rolling 1-hour window → page on-call (protects the fail-open SLA itself from silently degrading).spam_auto_block_failedrate >10% of attempted blocks in a rolling 1-hour window → investigate hub-core integration health.- False-positive proxy (
spam_undo÷spam_auto_block) >5% in any rolling 24-hour window → notify PM immediately; pause new-tenant enrollment pending review.
10.1. Post-Launch Monitoring Cadence
Daily review during Closed Beta (Rollout Stage 2) given the small, closely-watched cohort. Weekly review for the first 8 weeks after Limited GA (Stage 3) begins. Trigger: if the false-positive proxy (spam_undo ÷ spam_auto_block) exceeds 2% in any weekly window, escalate immediately to the PM and pause auto-enrollment of new tenants until root-caused — this is a hard gate, not advisory, given the irreversible-by-default nature of the auto-block action.
11. Success Metrics
Efficiency & Impact:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| ⭐ Billable AI messages avoided | spam_auto_block count × avg. messages a clean-routed conversation of the same type would have generated | N/A — no spam measurement exists today | Establish baseline during Closed Beta (Stage 2, first 2 weeks); ≥80% reduction in spam-driven billable AI replies among opted-in tenants within 90 days of GA |
Quality & Accuracy:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| False-positive rate | spam_undo ÷ spam_auto_block | N/A — new | ≤5% during Closed Beta (Stage 2); tightening to ≤2% within 60 days of General Availability |
| Suspected-spam tag precision | Share of spam_tag_applied rooms a human later confirms as spam (via manual block or resolve) | N/A — new | ≥30% by end of Closed Beta; ≥50% within 90 days of GA |
| Classifier reliability | 1 − (spam_classifier_error ÷ spam_classification_run) | N/A — new | ≥98% within 30 days of Limited GA (Stage 3) |
Adoption & Usage:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| Closed Beta onboarding | Design-partner tenants with the feature enabled | 0 | ≥3 tenants onboarded within 2 weeks of Stage 2 start |
| Tenant opt-in rate | Share of eligible tenants (active WhatsApp channel) with ai_spam_gatekeeper ON | 0 | ≥25% of eligible tenants within 60 days of General Availability |
12. Launch Plan & Stage Gates
| Stage | Audience | Duration | Gate to advance (references Section 11 metrics) |
|---|---|---|---|
| Internal QA → Closed Beta | Chatbot squad + PM, internal test channels only (per Section 9, Stage 1) | 1 week | 100% synthetic test-matrix pass rate; zero unhandled exceptions; fail-open verified under forced timeout |
| Closed Beta → Limited GA | 3–5 high-spam-volume design-partner tenants (per Section 9, Stage 2) | 2 weeks minimum | False-positive rate ≤5% (Section 11); zero P0 incidents; suspected-spam tag precision ≥30%; ≥3 design-partner tenants successfully onboarded |
| Limited GA → General Availability | All tenants with an active WhatsApp channel, self-enable, "Beta" labeled (per Section 9, Stage 3) | 3 weeks | False-positive rate ≤3%; classifier reliability ≥98%; supervisor alert delivery success ≥95% (where configured); thresholds sustained for 1 full week with no regression |
| General Availability | All eligible tenants (per Section 9, Stage 4) | Ongoing | "Beta" label removed; targeted to land ahead of the 1 October 2026 Meta service-message billing start |
13. Dependencies
| Dependency | Owner | What's needed | Blocking? |
|---|---|---|---|
| Spam Filter settings UI design | Design (Wulan's team) | Change-request spec for the net-new "Spam filter" tab (Section 6) — zero existing spam/moderation pattern exists in qontak-designer today, confirmed P0 gap | YES — Phase 1 has no path to a self-serve tenant config without it |
| Internal chatbot-squad TECH RFC | BOT — Chatbot Squad (self-owned) | Formalize the Repositories::Gpt::Completion classification call contract, spam_policy config schema, and retry/fail-open mechanics before build starts | YES — build-blocking, but internally owned so low external risk |
Block-provenance plumbing (chatbot + hub-core + hub-chat) — discovered via ground-prd, 2026-07-14, not in the original scope estimate | BOT — Chatbot Squad (chatbot side) + Omnichannel squad (hub-core/hub-chat side) | Chatbot: add source/reason to the Hub::ChatService::Contacts#block call body (currently { room_id } only, contacts.rb:22-25). hub-core: persist source in Repositories::Contacts::Block::Create#build_params (currently accepted but silently dropped) and add it to Entities::ContactBlock. hub-chat: surface the new field in ModalBlockContact.vue/ModalUnblockContact.vue/ContactInfo.vue to enable the "Bukan spam" relabel | YES — blocks Story SPAM-S04 as written; requires a small joint TECH RFC or coordinated change across two squads |
AI Agent Live Monitoring — ai_agent_alert category + supervisor-resolution mechanism | Broadcast / notification squad | The alert category and per-org supervisor resolution. Grounding correction (2026-07-14): this is PRD-draft with a minted Epic (BOT-4569) but zero code exists yet in either chatbot or notification-service — not "already building" as earlier phrasing implied | YES — SPAM-S05 (supervisor alert) is now a Must Have story (PM decision, 2026-07-14); Phase 1 cannot reach READY/GA until this dependency resolves. Note: the underlying block/resolve action (S02) still executes independently even if this dependency is delayed (S7 Behavior 4, alert delivery itself is best-effort) — what's blocking is the story, not the core spam-blocking mechanism |
| hub-core / hub-chat contact-block system (core block/resolve/unblock mechanics) | Omnichannel squad (reused, not modified) | ContactBlock, block/resolve interactors, and the existing block/unblock modal shell — the core mechanics are confirmed reused as-is; only the source attribution layer above is net-new | NO for the core mechanism — already exists and shipped in production (the attribution layer is tracked separately above) |
| Confirmed Meta per-message rate (Indonesia) | Bifrost — meta-whatsapp-pricing-oct-2026 | Exact per-message rate, published 1 Sep 2026, needed to convert "messages avoided" into a tenant-visible rupiah savings figure. Verified directly (2026-07-14): documentations/monetary-impact-to-clients.md:12,24,54 confirms the ~332 IDR/message and ~487K IDR/month median figures cited in the ANCHOR | NO — needed only for the savings display, not for the core blocking mechanism (see ANCHOR Open Question #1) |
📊 Dependency Graph — AI Spam Gatekeeper (Phase 1)
graph LR
F[AI Spam Gatekeeper — Phase 1]
F -->|BLOCKING| D1[Design change-request spec]
F -->|BLOCKING, internal| D2[Chatbot-squad TECH RFC]
F -->|BLOCKING — new, found via grounding| D3[Block-provenance plumbing: chatbot + hub-core + hub-chat]
F -->|BLOCKING — SPAM-S05 is Must Have, zero code exists yet| D4[ai_agent_alert category + supervisor resolution]
F -->|non-blocking for core mechanism| D5[hub-core block/resolve/unblock mechanics]
F -->|non-blocking, savings display only| D6[Meta per-message rate — published 1 Sep 2026]
14. Key Decisions + Alternatives Rejected
Initiative-level decisions (config altitude, DSAI removal, block provenance modeling, fail-open design, supervisor alerting, Meta-blocking scope) are recorded in the ANCHOR, Section 5. This section covers Phase 1–specific implementation decisions only.
14a — Decisions Made
| Date | Decision | Rationale |
|---|---|---|
| 2026-07-14 | Classify only the first inbound message of a NEW room — never subsequent messages in an already-classified conversation | Bounds the added classifier-call volume to one per new conversation, not per message; production data shows AI-resolved rooms average only ~2.3–3.0 messages total, so per-message classification would roughly double AI-call volume for no added spam-detection value |
| 2026-07-14 | Block-before-resolve ordering: the contact block call happens first, and the room is only resolved on block success | Prevents a window where a room is silently resolved while the spammer remains unblocked and able to keep messaging into a closed room |
| 2026-07-14 | Block-call failures retry 3 times, then fall through to the "suspected spam" tag rather than failing silently | Guarantees the conversation always lands in a defined state — either successfully blocked, or visibly tagged for human review — never silently dropped |
| 2026-07-14 | Reuse and relabel the existing hub-chat block/unblock modal rather than building a new "AI Block" component | Minimizes scope and engineering time ahead of the 1 Oct 2026 deadline; the underlying action (unblock) is functionally identical regardless of who performed the block |
14b — Alternatives Rejected
| Alternative | Why Rejected | Date |
|---|---|---|
| Classify every message in an ongoing conversation, not just the first | Contradicts the cost-bounding goal this feature exists to serve, and once a genuine conversation is underway it is very unlikely to still be spam | 2026-07-14 |
| Resolve-first-then-block ordering | Leaves a risk window where a "resolved" room's contact can still send further messages before the block takes effect | 2026-07-14 |
| Hard-fail (no retry, no fallback) on any hub-core block error | Too brittle — a transient error would silently drop a confirmed-spam conversation into an undefined state instead of a safe, visible fallback | 2026-07-14 |
| Build a new dedicated "AI Block" modal | Unnecessary duplicate UI for a functionally identical unblock action; relabeling the existing modal ships faster with equivalent clarity | 2026-07-14 |
15. Open Questions
| # | Type | Question | Owner | Deadline |
|---|---|---|---|---|
| 1 | Assumption | The existing Chatbot settings permission model (no new role) is sufficient to gate the new Spam filter tab | Dimas + chatbot-fe | Build kickoff |
| 2 | Open Question | Exact wording/UX for the 4 pre-built category examples, including Bahasa Indonesia localization | Dimas + Design | Aligned with design change-request delivery (see Section 13) |
| 3 | Risk | Closed Beta design partners may not generate enough spam volume in 2 weeks to validate the false-positive rate meaningfully — mitigation: select partners using Tableau-confirmed spam-heavy segments; extend Stage 2 duration if signal is thin (Section 9) | Dimas | Before Stage 2 exit decision |
| 4 | Open Question | Should a suspected-spam-tagged or auto-blocked customer ever see any indication they were flagged (even indirectly)? Current assumption: no — fully internal, customer sees nothing different in either path (silent classification, silent tag; blocked contacts simply stop being able to send messages, per existing block behavior) | Dimas | Before Stage 2 Closed Beta start |
| 5 | Risk | Repositories::Gpt::Completion's existing OpenAI request budget may not have headroom for one additional classification call per new inbound conversation across all opted-in tenants at Limited GA scale — mitigation: load-test during Internal QA (Stage 1) against Tableau's observed cohort volume (~620–695k rooms/month) | Dimas + Chatbot BE | Before Stage 1 exit |
| 6 | Risk | The block-provenance plumbing (Section 13) requires coordinated changes across chatbot (Chatbot Squad) and hub-core/hub-chat (Omnichannel Squad) — a cross-squad dependency discovered via grounding, not in the original scope estimate. If not scoped as a small joint TECH RFC early, it risks becoming the critical-path item for the whole Phase 1 timeline — mitigation: raise with Omnichannel squad immediately, scope as a 1-sprint joint ticket, not a full cross-squad SUPPORT PRD | Dimas + Omnichannel squad | Before Stage 1 (Internal QA) starts |
Types:
Assumption·Open Question·Risk
PRD CHANGELOG
| Version | Date | By | Section | Type | Summary |
|---|---|---|---|---|---|
| 1.0 | 2026-07-14 | Claude | All | CREATED | Initial Phase 1 PRD for the AI Spam Gatekeeper initiative — silent classification + auto-block + fail-open tagging + settings UI, grounded in chatbot BE's inbound pipeline, hub-core's contact-block system, and the confirmed chatbot-fe/qontak-designer design gap |
| 1.1 | 2026-07-14 | Claude | 8, 13 | UPDATED | SPAM-S05 (supervisor alert) promoted from Should Have to Must Have per PM decision; AI Agent Live Monitoring dependency reclassified as blocking to match |
| 1.2 | 2026-07-14 | Claude | Scope Changes, 7, 8, 13, 15 | UPDATED | Applied ground-prd findings (P0/P1): corrected the false "hub-core reused as-is / pure relabel" assumption — block-call source/reason attribution is confirmed net-new plumbing across chatbot + hub-core + hub-chat, added as a new blocking dependency; added the is_create_tag: true requirement to Behavior 3 / SPAM-S03; corrected ai_agent_alert dependency maturity wording (PRD-draft + Epic only, zero code); added a cross-squad coordination Risk. Bifrost pricing figures and all chatbot BE inbound-pipeline claims independently re-verified, no change needed |
| 1.3 | 2026-07-14 | Claude | 1, 6, 8, 9, 12 | UPDATED | Applied score-prd Full-mode findings (all 5 improvement items): trimmed one-liner to ≤25 words and quantified problem-statement scale; added S6 access-role line + UI State + Component Tree diagrams; added SPAM-S03 tag-removal rollback AC, SPAM-S04 specific error message + volume AC, SPAM-S01 plan/tier-ineligibility AC; cross-referenced the ai_spam_gatekeeper flag by name in S9 and sharpened the semantic-regression rollback framing; restated audience/duration per stage in S12. Re-scored 8.5→ higher on Header/S6/S12; no gate-relevant regressions |
| 1.4 | 2026-07-14 | Claude | 4, 7, 8 | UPDATED | Closed remaining Known Gaps: added the S7 Tier-2 API sequence diagram (Behavior 2, high-confidence auto-block, cross-referencing the net-new attribution fields from grounding); clarified S4 §Platform availability as a scope boundary, not an access-denial case (no mobile screen exists to deny); added SPAM-S02-NEG (non-WhatsApp channels never classified) and SPAM-S05-NEG (alerts never fire on suspected-spam tags, only confirmed auto-blocks) — both grounded in existing Non-Goals/Behaviors, not fabricated. SPAM-S04 intentionally has no dedicated NEG: it IS the reversal mechanism for SPAM-S02, so no separate Non-Goal boundary applies |