Skip to main content

RFC: AI Agent Knowledge — Conversation History (Phase 1)

Document Conventions (do not remove)

This RFC follows the Qontak RFC Template format for governance — the metadata table, Confluence sections 1–6, and Comment logs are mandatory. Sections marked N/A — reason are intentional, not omissions.

It is agent-execution-ready: §1 Design References (FE half) + §1 PRD-to-Schema Derivation (BE half), §2 Repo Reading Guide (Detail 2.0) for both layers, mermaid diagrams, §2.G Cross-Layer Contract Verification, and §4 Agent Execution Plan + Verification & Rollback Recipe are filled to the extent the current repository and the (still-open) cross-team contracts allow. Unresolved blockers are listed in §5 and gate §7 to no.

Delivery & project management live elsewhere. This is the technical artifact only. Staffing, effort, and schedule live in the initiative's delivery/ folder. Until handed to delivery, the Delivery row reads not yet handed to delivery.

The YAML frontmatter is the machine-readable index; the Metadata table is the human-readable governance record. Both must agree on every shared field.

Metadata

FieldValueNotes
StatusIDEAHuman label; YAML status: carries the remapped linter enum draft
DRIDimas Fauzi HidayatAccountable owner carried from the PRD (PM). Engineering DRI assigned at delivery handoff.
TeamchatbotAdvisory squad slug; squad = BOT (Hadiningbot)
Author(s)BOT squad (Hadiningbot)Eng author(s) to be named
ReviewersBOT tech lead · Data & AI team · chatbot-fe leadData & AI owns masking + vector + webhook
Approver(s)Engineering Lead · InfoSec approverNames TBD; InfoSec required (PII handling)
Submitted Date2026-06-20ISO-8601
Last Updated2026-06-20ISO-8601
Target Release2026-Q3From initiative target_quarter
Target Quarter2026-Q3Advisory; from PRD / initiative README
Deliverynot yet handed to deliveryPointer added once handed to delivery
Related../prds/conversation-history.md · ../ai-agent-knowledge-anchor.mdPRD + initiative ANCHOR
Discussion#bot-squad — RFC thread TBDSlack

Type: full-stack Frontend sub-type: new-feature Backend sub-type: enhancement (extends existing agent_conversation_history scaffolding)

Sections at a Glance

  1. Overview (Design References — FE half; PRD-to-Schema Derivation — BE half)
  2. Technical Design (Repo Reading Guide both layers → end-to-end mermaid → DDL → APIs → cross-layer contract verification)
  3. High-Availability & Security
  4. Backwards Compatibility and Rollout Plan
  5. Concern, Questions, or Known Limitations
  6. Comment logs
  7. Ready for agent execution

1. Overview

This RFC designs Phase 1 of the AI Agent Knowledge initiative: Conversation History — a new dynamic knowledge source that lets the Qontak AI Agent (Airene Copilot) learn from the resolved chat conversations of selected expert agents. An Admin picks a Division and up to 15 expert agents; the system ingests the last ~90 days of those agents' resolved, customer-facing, text-only conversations (PII-masked by the Data & AI pipeline), indexes them into the AI vector store, keeps them fresh with a daily rolling-window sync, and surfaces a per-source training status (Training in Progress / Active / Error) and "Last Updated" timestamp in the existing Knowledge / AI Resources UI.

Grounding note — this is an enhancement on a partial existing build, not greenfield. The backend already ships an agent_conversation_history knowledge-source type, the ai_knowledge_source_omnichannel_agent_conversations table, a create endpoint (POST /v1/gpt/ai_knowledge_sources/omnichannel-agent-conversations), a detail endpoint, a soft-delete, a usages/quota endpoint, an AI-Service type: 'conversation' post path, and a poll-based status updater. The frontend already declares a "Conversation history" source tile but does not wire it to a working flow. This RFC therefore mostly extends verified backend contracts and builds the frontend configuration surface plus the missing daily-sync, division-scoping, 15-agent-cap, and status-webhook pieces. See Detail 2.0.

Success Criteria

  • Adoption (PRD §13 ⭐): ≥ 30% of AI-Agent-active Qontak360 companies configure ≥1 Conversation History source within 90 days of GA.
  • Training latency (PRD §6 / §14): Initial training of a 15-agent source reaches Active within ~1 hour; daily sync success rate ≥ 99%.
  • Freshness: rolling 90-day window maintained — data older than 90 days is purged each daily sync; "Last Updated" advances on every successful sync.
  • Privacy: 100% masking of standard phone/email formats before indexing (Internal Alpha gate, PRD §14); no raw PII persisted post-processing.
  • Isolation: conversation ingestion runs in the Data & AI-owned isolated service (no "noisy neighbor" load on the chatbot app).

Out of Scope

Mirrors PRD §5 Non-Goals: automated/CSAT-based agent selection; real-time ingestion; multi-media (image/file/voice) indexing; cross-division knowledge pooling; new reporting dashboards. Plus: this RFC does not specify the internals of the Data & AI ingestion/masking/vector service (owned by that team) — it specifies only the contract the chatbot BE/FE depend on.

Assumptions

  1. The Data & AI team owns and delivers: PII masking, the isolated conversation-training/vector service, and the rolling-90-day ingestion+purge. The chatbot BE only configures, triggers, and reflects status. (PRD §15 — all marked blocking.)
  2. The existing AI-Service contract POST …/knowledge-base/vectors accepting { id, type: 'conversation', participant_ids: [...] } and GET …/vectors/status/:company_id remains stable (verified in post_knowledge_sources.rb / update_status.rb).
  3. "Resolved" room status + ≥5 bubble messages is an acceptable proxy for a useful resolution (PRD OQ#6 — owned by PM, not yet confirmed).
  4. The division→agent directory needed by the config modal is served by the existing Division / AgentsDivision models (no new directory service).
  5. Qontak360 plan/tier gating is expressed via the existing SystemPreference flag mechanism.

Dependencies

DependencyOwning teamDeliverableBlocking?Status
PII masking engineData & AINLP redaction → generic tags before indexingYESContract/SLA open (PRD OQ#1)
Isolated conversation training + vector indexingData & AIProcess heavy conversation data; expose type:'conversation' vector create + statusYESCreate + poll status verified live; rolling-90d window behavior to confirm
Training-status webhookData & AIInbound webhook: in_progress/completed/failed payload + retry semanticsYESContract not defined (PRD OQ#4) — see Decision 6
Agent–Division directoryBOT (chatbot)GET list divisions + agents for the config modalYESNew endpoint — verified no GET directory exists (only internal sync PATCH /v1/chat/divisions); build from Division/AgentsDivision (chunk 2b)
Daily sync cronBOT (chatbot)24h job: re-trigger ingest, purge >90d, advance "Last Updated"YESNewconfig/schedule.yml + worker (this RFC)
Resolved-room fetch (90-day window)Inbox / Platform (ChatService)Fetch resolved rooms by agent over 90 daysYESHub::ChatService::Rooms::List exists; consumed by Data & AI service
Onboarding consentLegal / OpsExisting consent covers internal AI training useNOReuse existing

Design References (frontend half — required)

Design status is DRAFT (PRD §8 + PRD OQ#7). The only located UI is a throwaway design prototype in qontak-designer (README: "playground for designers to bring ideas to prototype"). It is not the Figma master and omits several PRD requirements. Production chatbot-fe has only a non-wired "Conversation history" tile. Several frames are therefore n/a — design pending and listed in §5 as blockers.

PRD-named surfaceFigma / design linkFrame nameDesign system versionDesign QA contactNotes
Add Knowledge → source-type picker (+ "Conversation history")Bot - AI 20840-30474 / 20840-32079 / 20839-31336Add source flow@mekari/pixel3@1.0.12 (chatbot-fe package.json)Wulan Febyazzahra PutriPrototype: qontak-designer/app/components/bot-automation/sources/AddKnowledgeDrawer.vue
Conversation History config form (Division + ≤15 agents + tooltip + 15-cap validation)n/a — design pending@mekari/pixel3@1.0.12WulanGap (PRD OQ#7): prototype uses a "Use as source" toggle + fixed 9-agent read-only list; no division-first multiselect, no 15-cap, no expertise tooltip. Blocker → §5.
Source detail (Agent · Division table)AI Resources 10859-83239Source detail@mekari/pixel3@1.0.12WulanPrototype: qontak-designer/app/components/bot-automation/sources/SourceDetailDrawer.vue (read-only Agent·Division table present)
AI Resources list (Status, Last updated, remove/delete)Bot - AI 20671-248358Resources list@mekari/pixel3@1.0.12WulanPrototype qontak-designer/app/pages/bot-automation/resources/index.vue shows only Active/Inactive — no Training-in-Progress / Error states (PRD OQ#7). Blocker → §5.

PRD-to-Schema Derivation (backend half — required)

PRD-described entity / attribute / rulePersisted as (table.column)Exposed via (endpoint / event)Enforced whereSource (PRD §)
A Conversation History source belongs to a company and one source typeai_knowledge_sources.company_id, .organization_id, .ai_knowledge_source_type_id (type code agent_conversation_history), .status, .namePOST …/omnichannel-agent-conversations (create), GET …/ (list)CreateOmnichannelAgentConversationKnowledgeSource use case§7, §8, S01
Selected expert agents (≤15) feed the sourceai_knowledge_source_omnichannel_agent_conversations (agent_id, agent_name, company_id, ai_knowledge_source_id)create payload agents:[{id,name}]; AI-Service participant_idsAiKnowledgeSourceOmnichannelAgentConversations::Create + PostKnowledgeSources§6, S01, S04
One Division = one source + division_id recordedNEW ai_knowledge_sources.division_id (nullable; + partial-unique (company_id, division_id, type_id))create payload division_idcreate use case existence guard (extended to be division-aware) + DB partial-unique index§6, S05, NEG-3 — see Decision 1
Max 15 agents per sourcecontract rule on agents array length ≤ 15create payload validation (422)create use case contract (extended) + FE counter§6, S04
Training status lifecycle (in_progress / active / error)ai_knowledge_sources.status (IN_PROGRESS/ACTIVE/FAILED)status poll worker + (future) inbound webhook; surfaced in list/detailUpdateStatus repo (poll) → status webhook (new, blocked)§7, S06, S10
"Last Updated" freshnessai_knowledge_sources.updated_at (advances on sync)list/detail serializerdaily sync worker (new)§7, S10
Daily rolling 90-day sync + purge(window + purge enforced inside Data & AI vector store)ConversationHistoryDailySyncWorker re-posts via PostKnowledgeSourcesnew sidekiq-cron job @ 0 1 * * * Asia/Jakarta§9#4, S10, S12
PII masking; quality gate (≥5 msgs); whisper exclusion; text-only; recency(enforced inside Data & AI ingestion; not in chatbot)AI-Service ingestion contractData & AI pipelineS06, S07, S08, S09, S13, NEG-2/4
Reference room-id link, permission-scopedask_airene_history_references.ai_knowledge_source_id, room_id (link source)answer reference render + room-view permission checkroom authorization on openS11, OQ#5
Soft-delete (remove from agent) vs hard-delete (delete source)ai_agent_knowledges.deleted_at (unlink) vs ai_knowledge_sources.deleted_at + vector hard-deleteDELETE :id + delete_vector_dbdelete use case (guarded by in-use check)S02, S03

Every §2.3 DDL row and §2.4 endpoint traces to a row here (or to a Figma frame in Design References). Rows owned by Data & AI are explicitly out of chatbot's enforcement scope.

Detail 1.A — PRD Traceability (cross-layer)

Composite AC ids per PRD §10.2 (CONVHIST-Sxx/AC-n, ERR-n, NEG-n).

Forward (PRD AC → RFC):

PRD composite AC idFE section / componentBE section / endpoint
CONVHIST-S01/AC-1..3, ERR-1ConversationHistoryForm in AddKnowledgeDrawer (§2.A)POST …/omnichannel-agent-conversations (§2.4, extended)
CONVHIST-S02/AC-1..3, ERR-1Row action "−" on Resources list (§2.A)unlink ai_agent_knowledges (§2.4)
CONVHIST-S03/AC-1..3, ERR-1Delete action + in-use confirm dialog (§2.A)DELETE :id + in-use guard + delete_vector_db (§2.4)
CONVHIST-S04/AC-1..3, ERR-115-cap counter + validation (§2.A, §2.C)contract agents ≤15 (§2.4)
CONVHIST-S05/AC-1..3, ERR-1Division-first multi-select (§2.A)divisions+agents read (§2.4); division_id persist (§2.3)
CONVHIST-S06/AC-1..3, ERR-1status surfacing onlyData & AI masking (out of scope); status Error on failure (§2.F.2)
CONVHIST-S07, S08, S09, S13n/a — backend/pipelineData & AI ingestion contract (§2.D resp. boundary)
CONVHIST-S10/AC-1..3, ERR-1"Last Updated" cell + status badge (§2.C)ConversationHistoryDailySyncWorker (§2.F)
CONVHIST-S11/AC-1..3, ERR-1reference link render (§2.A)room-view permission check (§3 authz)
CONVHIST-S12/AC-1..3, ERR-1n/async skips deactivated agents; ages out (§2.F)
NEG-1hide tile when not Qontak360 (§2.A)plan flag gate (§3)
NEG-3block 2nd source per division (§2.A error)one-per-division unique guard (§2.3)

Reverse (RFC → PRD AC):

New FE component / BE endpoint / dependencyPRD composite AC id it serves
ConversationHistoryForm (division + ≤15 agents)CONVHIST-S01/AC-1, S04/AC-1, S05/AC-1
division_id column + one-per-division unique indexCONVHIST-S05/AC-2, NEG-3
ConversationHistoryDailySyncWorker + schedule.yml entryCONVHIST-S10/AC-1, S12/AC-2
in-use delete guardCONVHIST-S03/ERR-1
training-status webhook (blocked)CONVHIST-S06/ERR-1, S10/ERR-1

UI / Consumer Surface Coverage

PRD-named surfaceConsumerRequired reads (BE)Required writes (BE)FE componentStatus surface
Add Knowledge modal / drawerweb (Admin)GET …/usages (plan/quota), divisions+agentsPOST …/omnichannel-agent-conversationsAddKnowledgeDrawerConversationHistoryFormai_knowledge_sources.status
Knowledge Index / AI Resources listweb (Admin)GET …/ (list)DELETE :id (delete), unlinkpages/ai-knowledge/training-sources/index.vue + knowledge-list.vuestatus badge + Last Updated
Source detail drawerweb (Admin)GET …/omnichannel-agent-conversation/:idSourceDetailDrawer (new in chatbot-fe)status badge
Copilot answer reference (Inbox)web (Agent)room-by-id (permission-scoped)Inbox Copilot reference linkn/a — read only

Role Coverage

PRD roleAuthorization mechanismEndpoints permitted (BE)UI surface visibility (FE)Cross-tenant?Audit trail
Admin / Super Admin (owner/supervisor/admin)Grape set_role(%w[owner supervisor admin]) (authorization_helpers.rb)create / list / detail / delete / usagesfull config + managementno — scoped by company_id/organization_idPaperTrail on ai_knowledge_sources / agent-conversation rows
Agent (front-line)session rolenone (config); room-scoped read for reference linkconsumes Copilot suggestions; sees reference link only if room-permittednon/a
Non-Qontak360 / lower tierplan flag (SystemPreference)none"Conversation history" tile hidden (NEG-1)n/an/a
Data & AI pipeline (service)service auth (webhook contract TBD)status webhook (inbound, blocked)n/an/a — operates per company_idsync logs

PRD Section Coverage

PRD §TitleWhere covered
2Phase Context§1 Overview
3One-liner + Problem§1 Overview
4Target Users + Persona§1 (Success Criteria / personas)
5Non-Goals§1 Out of Scope
6Constraints§1, §3 (limits, tier, data limits)
6.1Data Lifecycle§2.3 per-status lifecycle + §3.D compliance
7Feature Changes§2.A UI Contract + §2.4 APIs
8New Features§1 Design References + §2.A + §5 (design gaps)
9API & Webhook Behavior§2.4 APIs + §2.F async + Decision 6 (webhook)
10.1System Flow§2.2 sequence + §2.1 branch/skip
10.2User Stories + ACsDetail 1.A + Detail 1.C
11Rollout§4 Rollout Strategy
12Observability§3 Monitoring & Alerting
13Success Metrics§1 Success Criteria
14Launch Plan & Stage Gates§4 Rollout (stage gates)
15Dependencies§1 Dependencies
16Key Decisions§2 Technical Decisions (ADR) + Detail 1.B
17Open Questions§5 Concerns / Open Questions

Detail 1.B — Decisions Closed (cross-layer)

DecisionChosen optionAlternatives rejectedWhy rejectedLayer
1. Source scopingExtend to division-scoped (division_id + one-per-division)Keep existing one-per-companyPRD §6/S05/NEG-3 mandate one-Division-one-source; current code only guards one-per-companyboth
2. StorageReuse existing ai_knowledge_sources + agent-conversation table (ChatbotGpt DB)New bespoke tablesType, tables, serializers already exist and are wired to AI-ServiceBE
3. Ingest syncAsync daily sidekiq-cron worker re-posting to AI-ServiceSync on request / real-timeHeavy work; PRD §5 defers real-time; matches existing nightly job patternBE
4. Status propagationPoll (get_vector_status) as baseline; webhook as enhancementWebhook-onlyWebhook contract undefined (OQ#4); poll already worksBE
5. Vector indexingReuse AI-Service type:'conversation' contractBuild vectors in chatbotNo local vector DB; Data & AI owns itBE/Data
6. 15-agent cap enforcementBoth FE (counter+validation) and BE (contract ≤15)FE-onlyDefense-in-depth; BE is source of truthboth
7. Delete semanticsHard-delete vectors only when not in use; unlink = soft-delete join rowAlways hard-deletePRD S03/ERR-1 protects in-use sourcesboth
8. PII masking ownershipData & AI pipeline (no chatbot masking)Mask in chatbotTeam boundary; isolated service requirement (PRD §6/§15)Data
9. Plan/tier gatingReuse SystemPreference flagNew flag systemExisting rollout-flag pattern (knowledge_stores/create.rb:36)BE
10. FE source-type wiringWire the existing "conversation-history" tile + new formNew drawerTile already declared in AddKnowledgeDrawer.vueFE

FE/BE alignment notes: API is snake_case JSON via Grape; FE consumes via $apiMain wrapper. No REST-vs-gRPC or pagination disagreement (offset list reused). Error envelope is the existing ErrorException shape.

Detail 1.C — Per-Story Change Map

Story idTitleLayer scopeFE changesBE changesComposite AC idsAcceptance criteria (verifiable)RFC anchors
CONVHIST-S01Source configurationFE + BEConversationHistoryForm; service add_conversation(); wire tileextend create contract (division_id); reuse POST …/omnichannel-agent-conversationsS01/AC-1..3, ERR-1rspec on create use case passes; row appears with IN_PROGRESS; vitest on form; toast shown§2.4 · §2.A · §4.D ch.3,6 · §1 PRD-to-Schema r1
CONVHIST-S02Remove from agent (unlink)FE + BE"−" row action + confirmsoft-delete ai_agent_knowledges join (no vector delete)S02/AC-1..3, ERR-1rspec: join row deleted_at set, source row intact; agent stops referencing§2.4 · §2.A · §4.D ch.7
CONVHIST-S03Delete sourceFE + BEDelete + in-use guard dialogDELETE :id; if in-use → 422; else delete_vector_db + soft-deleteS03/AC-1..3, ERR-1rspec: in-use delete blocked 422; unused delete purges vectors; no restore§2.4 · §3.B · §4.D ch.7
CONVHIST-S0415-agent capFE + BElive "N/15" counter + validation message + Save disablecontract agents length ≤15 → 422S04/AC-1..3, ERR-1vitest: 16th selection disables Save + message; rspec: 16 agents → 422§2.A · §2.C · §2.4 · §4.D ch.4
CONVHIST-S05Select whole divisionFE + BEdivision-first multiselect; select-all (cap 15); deselect individualsdivisions+agents read; persist division_idS05/AC-1..3, ERR-1vitest: selecting division marks ≤15 agents; rspec: 2nd source/division blocked (NEG-3)§2.A · §2.4 · §2.3 · §4.D ch.2,4
CONVHIST-S06PII maskingCross-squadn/a — status surfaced onlyn/a — Data & AI pipeline; chatbot sets Error on failure signalS06/AC-1..3, ERR-1Data & AI alpha gate: 100% mask standard phone/email; chatbot shows Error on fail§2.D · §2.F.2 · §3.D
CONVHIST-S07Quality gate (≥5 msgs)Cross-squadn/an/a — Data & AI ingestion filterS07/AC-1..3, ERR-1Data & AI: 3-msg room skipped; ≥5 customer-facing indexed§2.D resp. boundary
CONVHIST-S08Exclude whispersCross-squadn/an/a — Data & AI ingestion filterS08/AC-1..3, ERR-1Data & AI: only customer-facing bubbles indexed§2.D · §3.D
CONVHIST-S09Recency prioritizationCross-squadn/an/a — Data & AI rankingS09/AC-1..3, ERR-1Data & AI: latest resolution wins; deterministic tie-break§2.D
CONVHIST-S10Daily maintenance syncBEstatus badge + Last Updated refreshConversationHistoryDailySyncWorker + schedule.yml 0 1 * * * Asia/JakartaS10/AC-1..3, ERR-1rspec-sidekiq: worker enqueues per active source; on fail prior index retained + alert§2.F · §4.D ch.5
CONVHIST-S11Reference room-id linkFE + BErender reference linkroom-view permission check on openS11/AC-1..3, ERR-1cross-division open → 403; authorized → room opens§3 authz · §2.4
CONVHIST-S12Agent deactivationBEn/async skips deactivated agents; data ages out at 90dS12/AC-1..3, ERR-1rspec: deactivated agent → no new ingest; reactivation resumes without dup§2.F
CONVHIST-S13Text-onlyCross-squadn/an/a — Data & AI content filterS13/AC-1..3, ERR-1Data & AI: non-text excluded; text caption indexed§2.D

Stories S06–S09, S13 are Cross-squad (Data & AI pipeline) — chatbot's responsibility is to configure, trigger, and reflect status, per the §2.D Responsibility Boundary Matrix.


2. Technical Design

Detail 2.0 — Repo Reading Guide (read this first)

Repo Map (mermaid, both layers)

flowchart LR
subgraph fe["chatbot-fe (Nuxt 3 / Vue 3)"]
drawer["components/AddKnowledgeDrawer.vue"]
reslist["pages/ai-knowledge/training-sources/index.vue"]
klist["modules/ai-knowledge/views/training-source/knowledge-list.vue"]
svc["common/services/main/v1/ai-assist.ts"]
ep["common/services/main/endpoint.ts"]
end
subgraph be["chatbot (Rails / Grape)"]
api["app/api/frontend_service/v1/gpt/ai_knowledge_sources.rb"]
uc["use_cases/.../ai_knowledge_source/*"]
repo["repositories/gpt/ai_knowledge_sources/*"]
worker["workers/* (sidekiq) + config/schedule.yml"]
aisvc["lib/ai_service/gpt/knowledge_base.rb"]
end
subgraph infra
pg[("ChatbotGpt DB\nai_knowledge_sources +\nomnichannel_agent_conversations")]
redis[["Redis / Sidekiq"]]
extai(["Data & AI vector + masking service"])
end
drawer --> svc --> ep --> api
reslist --> svc
klist --> reslist
api --> uc --> repo --> pg
repo --> aisvc --> extai
worker --> repo
redis --> worker

Existing Code Anchors

LayerPathWhy the agent reads itWhat pattern it teaches
BEapp/api/frontend_service/v1/gpt/ai_knowledge_sources.rbThe Grape resource hosting all knowledge endpoints incl. omnichannel-agent-conversationsGrape route + set_role + Dry::Matcher::ResultMatcher response shape
BEapp/core/use_cases/api/frontend_service/v1/gpt/ai_knowledge_source/create_omnichannel_agent_conversation_knowledge_source.rbThe create use case to extend (add division_id, ≤15 cap)APIAbstractUseCase + contract do params + Dry::Monads::Do; one-per-company guard at L30–33; returns 202
BEapp/core/repositories/gpt/ai_service/post_knowledge_sources.rbBuilds the AI-Service payload { id, type:'conversation', participant_ids } (L35–39)how participant_ids are derived from the agent-conversation rows
BEapp/core/repositories/gpt/ai_knowledge_sources/update_status.rbStatus poll → maps AI-Service status to ACTIVE/uppercased (L32–40, L51–53)poll-based status sync; conversation type handling
BEapp/core/use_cases/api/frontend_service/v1/gpt/ai_knowledge_source/auto_train_agent_conversation.rbExisting manual "auto train" (rails_admin) — bails if source existsshows there is no rolling daily sync yet
BElib/ai_service/gpt/knowledge_base.rbAI-Service client: create_vector_db (L16), get_vector_status (L36)external HTTP call shape + base path
BEapp/controllers/webhook_room_resolved_controller.rbInbound webhook pattern (for a future status webhook)skip_before_action :verify_authenticity_token + UseCases::System::ReceiveWebhook*
BEconfig/schedule.yml (+ config/initializers/sidekiq.rb:44)Existing sidekiq-cron daily jobs at 0 1 * * * Asia/Jakartaexact pattern for the new daily sync entry
BEapp/api/frontend_service/helpers/authorization_helpers.rbset_role(roles) 403 enforcementrole authz (no Pundit)
BEapp/api/frontend_service/helpers/current_user_helpers.rbcurrent_user['organization_id'/'company_id']multi-tenancy scoping
FEcomponents/AddKnowledgeDrawer.vueThe drawer with a declared (un-wired) conversation-history tile (L440–443)<script setup>, Pixel MpDrawer*, sourceTypeTiles, toast.notify
FEmodules/ai-knowledge/views/training-source/knowledge-list.vueStatus badge rendering (ACTIVE green / IN_PROGRESS orange / FAILED red)MpBadge variant-color per status
FEcommon/services/main/v1/ai-assist.tsService wrapper (get/add/add_url)$apiMain(endpoint, {method,body,signal,lazy})
FEcommon/services/main/endpoint.tsEndpoint registrywhere to add the conversation endpoint entry

Existing Contracts to Reuse, Extend, or Replace (BE)

ContractStatusJustificationOwner
ai_knowledge_sources table + type agent_conversation_historyreusealready models the sourceBOT
ai_knowledge_source_omnichannel_agent_conversations tableextendadd division_id (+ unique guard)BOT
POST …/omnichannel-agent-conversationsextendadd division_id + ≤15 cap to contractBOT
GET …/omnichannel-agent-conversation/:idreusedetail already serves agent listBOT
DELETE :idextendadd in-use guard + conditional delete_vector_dbBOT
GET …/usagesreusequota (currently quota=1 source)BOT
AI-Service type:'conversation' vector create + get_vector_statusreuseData & AI owns; contract verified liveData & AI
Daily sync cron workernew-with-justificationno rolling-window job exists today (only manual auto_train)BOT
Inbound training-status webhooknew-with-justificationpoll exists but PRD §9#3 wants webhook; contract undefined (OQ#4)Data & AI + BOT

Patterns to Follow

LayerConcernPattern in repoReference fileDeviation?
BEHTTP handler shapeGrape resource + set_role + ResultMatcherapp/api/frontend_service/v1/gpt/ai_knowledge_sources.rbnone
BEUse caseAPIAbstractUseCase + contract + Dry::Monads::Docreate_omnichannel_agent_conversation_knowledge_source.rbnone
BERepository / DBAbstractRepository, Success/Failurerepositories/gpt/ai_knowledge_sources/update_status.rbnone
BEWorker / cronSidekiq::Worker + config/schedule.ymlapp/workers/refresh_ai_agent_vector_status_worker.rb + config/schedule.ymlnone
BESerializerGrape::Entityapp/api/frontend_service/v1/entities/gpt/ai_knowledge_source/knowledge_source.rbnone
BEError shapeFrontendService::Exceptions::ErrorExceptionapp/api/frontend_service/helpers/error_response_helpers.rbnone
BEFeature flagSystemPreference.find_by(group_code:, code:, enabled:)app/core/repositories/knowledge_stores/create.rb:36none
BELoggingRollbar.error/info(repo-wide; e.g. create use case L42)none
FEComponent<script setup> + Pixel Mp*components/AddKnowledgeDrawer.vuenone
FEStatePinia (store/ai-assist, store/ai-agent)store/ai-assist/*none
FEAPI client$apiMain wrappercommon/services/main/v1/ai-assist.tsnone
FEToast / errortoast.notify({position,variant,title})components/AddKnowledgeDrawer.vue (~L632–646)none
FEStatus badgeMpBadge variant-color green/orange/redmodules/ai-knowledge/views/training-source/knowledge-list.vuenone
Crosssnake_case API → FEJSON snake_case consumed directly via $apiMaincommon/services/main/v1/ai-assist.tsnone
FEi18nnone (no i18n framework)deviation: strings hard-coded inline (PRD validation copy)

Reading Order for the Agent

  1. app/api/frontend_service/v1/gpt/ai_knowledge_sources.rb — endpoint surface + auth.
  2. …/create_omnichannel_agent_conversation_knowledge_source.rb — the use case to extend.
  3. app/core/repositories/gpt/ai_service/post_knowledge_sources.rb — AI-Service payload.
  4. app/core/repositories/gpt/ai_knowledge_sources/update_status.rb — status poll.
  5. config/schedule.yml + config/initializers/sidekiq.rb — cron pattern for daily sync.
  6. app/controllers/webhook_room_resolved_controller.rb — inbound webhook pattern.
  7. db/chatbot_gpt_schema.rb (L137, L150, L173) — tables to migrate.
  8. components/AddKnowledgeDrawer.vue — the tile to wire + drawer pattern.
  9. common/services/main/v1/ai-assist.ts + endpoint.ts — FE service wiring.
  10. modules/ai-knowledge/views/training-source/knowledge-list.vue — status badge.

Source Verification (anti-hallucination — required)

LayerAnchor / pattern / contractVerified byEvidence
BEai_knowledge_source_omnichannel_agent_conversations tablegrep schemadb/chatbot_gpt_schema.rb:137 create_table "…omnichannel_agent_conversations"; FK to ai_knowledge_sources at L492
BEai_knowledge_source_types (code unique)grep schemadb/chatbot_gpt_schema.rb:150; unique index on code (L159)
BEai_knowledge_sourcesgrep schemadb/chatbot_gpt_schema.rb:173; FK to types L494
BEtype code agent_conversation_historygrepused in search.rb:39, update_status.rb:36, create_…rb:27, post_knowledge_sources.rb:23/35
BEcreate endpointreadai_knowledge_sources.rb:259 post '/omnichannel-agent-conversations'; set_role %w[owner supervisor admin] L260
BEdetail / delete / usages endpointsread:229 get '/omnichannel-agent-conversation/:id', :80 delete ':id', :281 get '/usages'
BEcreate use case (no cap, no division, one-per-company guard)readcontract required(:agents).array(:hash){ required(:id), required(:name) } + company_id (L16–21); guard L30–33; no division_id, no length cap; returns 202 (L53)
BEAI-Service conversation payloadreadpost_knowledge_sources.rb:39 knowledges << { id:…, type:'conversation', participant_ids: agent_ids }
BEstatus pollreadupdate_status.rb:14 get_vector_status; L32 find type=='conversation'; L52 success→ACTIVE else upcase
BEmanual auto-train (not cron)read + grepauto_train_agent_conversation.rb bails "already exist" L26; caller is config/initializers/rails_admin.rb:189 (rails_admin form), not schedule.yml
BEsidekiq-cron daily patternreadconfig/schedule.yml L26–29 (0 1 * * * Asia/Jakarta), L61–63; loaded by config/initializers/sidekiq.rb:44–46
BEinbound webhook patternreadapp/controllers/webhook_room_resolved_controller.rb skip_before_action :verify_authenticity_token + UseCases::System::ReceiveWebhookRoomResolved
BEmulti-tenancy + authz(agent) readcurrent_user_helpers.rb:6 set_current_user; authorization_helpers.rb:6 set_role
BEfeature flag(agent) readSystemPreference.find_by(group_code:'rollout', code:…, enabled:true) at knowledge_stores/create.rb:36
BEtest/build commandsread AGENTS.mdrspec, rubocop, brakeman, rails db:migrate, bundle exec sidekiq (AGENTS.md test section)
FEconversation-history tile declared but un-wiredreadcomponents/AddKnowledgeDrawer.vue:440–443 tile id:"conversation-history"; active SOURCE_TYPE_OPTIONS L471–474 + sourceTypeOptions L478–492 exclude it
FEservice wrapperreadcommon/services/main/v1/ai-assist.ts get/add/add_url/save_url via $apiMain(endpoint.v1.ai_assist.*, …)
FEstatus badge(agent) readknowledge-list.vue MpBadge green/orange/red for ACTIVE/IN_PROGRESS/FAILED
FEstack + DS(agent) readNuxt 3.12.2, Vue 3, Pinia; @mekari/pixel3@1.0.12 (package.json)
FEno i18n(agent) grepno vue-i18n / locales dir
FEbuild/test(agent) read package.jsonpnpm dev/build/test (vitest)/test:e2e (playwright)/lint:ts/lint:prettier
Designprototype files exist + gaps(agent) readqontak-designer/app/components/bot-automation/sources/AddKnowledgeDrawer.vue (toggle + 9 hardcoded agents, no cap/tooltip), SourceDetailDrawer.vue (Active/Inactive only), app/pages/bot-automation/resources/index.vue

Design ↔ Code Mapping (frontend half)

Figma frame / componentImplementing file (path)Reuse vs newTokens usedBacking API endpoint(s)Deviation
Add source pickerchatbot-fe/components/AddKnowledgeDrawer.vueextended (wire tile L440)Pixel MpDrawer*, color.icon.brand, text.defaultPOST …/omnichannel-agent-conversationsnone — tile exists
Conversation History config formchatbot-fe/components/.../ConversationHistoryForm.vue (new)newMpSelect, MpCheckbox, MpBadge, MpTooltipdivisions+agents read; POST …/omnichannel-agent-conversationsdesign pending (OQ#7)
Resources listchatbot-fe/pages/ai-knowledge/training-sources/index.vue + knowledge-list.vueextendedMpTable, MpBadgeGET …/, DELETE :idtraining/error states need design
Source detailchatbot-fe/components/.../SourceDetailDrawer.vue (new)newMpDrawer*, MpTableGET …/omnichannel-agent-conversation/:idmirror prototype read-only table

Detail 2.1 — Architecture (mermaid)

End-to-end component diagram

flowchart TB
admin([Admin / Super Admin]) --> drawer["AddKnowledgeDrawer → ConversationHistoryForm (FE)"]
drawer --> svc["ai-assist.ts ($apiMain)"]
svc --> api["/v1/gpt/ai_knowledge_sources/omnichannel-agent-conversations/"]
api --> uc["CreateOmnichannelAgentConversationKnowledgeSource"]
uc --> repo["AiKnowledgeSourceOmnichannelAgentConversations::Create"]
repo --> pg[("ChatbotGpt DB")]
uc --> post["AiService::PostKnowledgeSources"]
post --> aisvc["AiService::Gpt::KnowledgeBase"]
aisvc --> extai(["Data & AI: mask + index (type:conversation)"])
cron[["schedule.yml 0 1 * * *"]] --> dsync["ConversationHistoryDailySyncWorker (new)"]
dsync --> post
poll["ConversationHistoryStatusSyncWorker / UpdateStatus"] --> aisvc
poll --> pg
wh["/webhooks/.../conversation-training (new, blocked OQ#4)/"] -.-> pg

Data model (mermaid erDiagram)

erDiagram
AI_KNOWLEDGE_SOURCE_TYPES ||--o{ AI_KNOWLEDGE_SOURCES : classifies
AI_KNOWLEDGE_SOURCES ||--o{ AI_KS_OMNI_AGENT_CONVERSATIONS : has
AI_KNOWLEDGE_SOURCES ||--o{ AI_AGENT_KNOWLEDGES : linked_to_agent
AI_KNOWLEDGE_SOURCE_TYPES {
bigint id PK
string code "agent_conversation_history"
string name
int sequence
}
AI_KNOWLEDGE_SOURCES {
bigint id PK
string company_id
string organization_id
bigint ai_knowledge_source_type_id FK
string division_id "NEW (Decision 1) — one source per company+division"
string status "IN_PROGRESS|ACTIVE|FAILED"
string name
timestamptz updated_at "Last Updated"
timestamptz deleted_at
}
AI_KS_OMNI_AGENT_CONVERSATIONS {
bigint id PK
bigint ai_knowledge_source_id FK
string agent_id
string agent_name
string company_id
timestamptz deleted_at
}
AI_AGENT_KNOWLEDGES {
bigint id PK
bigint ai_knowledge_id FK
bigint ai_agent_id
timestamptz deleted_at "unlink = soft delete"
}

State machine — source status

stateDiagram-v2
[*] --> IN_PROGRESS: create (POST omnichannel-agent-conversations)
IN_PROGRESS --> ACTIVE: AI-Service status=success (poll/webhook)
IN_PROGRESS --> FAILED: masking/index fail
FAILED --> IN_PROGRESS: retry / next daily sync
ACTIVE --> ACTIVE: daily sync (ingest new, purge >90d, Last Updated advances)
ACTIVE --> [*]: delete source (not in use) + hard-delete vectors
FAILED --> [*]: delete source

Branch & skip flow — plan gate + one-per-division

flowchart TD
start([Admin opens Add Knowledge]) --> q1{Qontak360 plan flag on?}
q1 -- no --> hide[Hide Conversation history tile NEG-1]
q1 -- yes --> q2{Division already has a source?}
q2 -- yes --> block[Block: one division = one source NEG-3]
q2 -- no --> q3{agents count > 15?}
q3 -- yes --> cap[Validation; Save disabled S04]
q3 -- no --> save[Create source → IN_PROGRESS]

Detail 2.2 — Sequence (mermaid, incl. failure paths)

Happy path — create + train

sequenceDiagram
actor A as Admin (FE)
participant LB as Load Balancer
participant API as chatbot Grape API
participant UC as CreateOmni…UseCase
participant DBW as ChatbotGpt DB (primary)
participant POST as PostKnowledgeSources
participant EXT as Data & AI vector/mask svc
participant POLL as StatusSync (poll)
A->>LB: POST /v1/gpt/.../omnichannel-agent-conversations {division_id, agents[≤15]}
LB->>API: HTTP
API->>API: set_role(owner/supervisor/admin)
API->>UC: handle(params)
UC->>UC: validate ≤15 agents; division not already sourced
UC->>DBW: BEGIN; upsert source (division_id) + insert agent-conversation rows; COMMIT
UC->>POST: build {id, type:'conversation', participant_ids}
POST->>EXT: POST /knowledge-base/vectors
Note right of EXT: ingestion+mask+index async (~1h, isolated svc)
EXT-->>POST: 202 accepted
UC-->>API: 202 (status IN_PROGRESS)
API-->>A: 202 row "Training in Progress"
POLL->>EXT: GET /vectors/status/:company_id (periodic)
EXT-->>POLL: status=success
POLL->>DBW: UPDATE status=ACTIVE; updated_at=now (Last Updated)

Failure path — masking/index fails

sequenceDiagram
participant POLL as StatusSync (poll)
participant EXT as Data & AI svc
participant DBW as ChatbotGpt DB
participant ALERT as Rollbar / Slack BOT alert
POLL->>EXT: GET /vectors/status/:company_id
EXT-->>POLL: status=failed (masking/index error)
POLL->>DBW: UPDATE status=FAILED (retain prior index)
POLL->>ALERT: alert (3 consecutive failures → Slack BOT)
Note over DBW: "Last Successful Sync" preserved; Admin notified (S06/ERR-1, S10/ERR-1)

Daily sync (rolling 90 days)

sequenceDiagram
participant CRON as sidekiq-cron 0 1 * * * Asia/Jakarta
participant W as ConversationHistoryDailySyncWorker (new)
participant DBW as ChatbotGpt DB
participant POST as PostKnowledgeSources
participant EXT as Data & AI svc
CRON->>W: enqueue
W->>DBW: list ACTIVE agent_conversation_history sources
loop per source
W->>POST: re-post {type:'conversation', participant_ids}
POST->>EXT: POST /vectors (ingest prior 24h, purge >90d)
EXT-->>POST: 202
end
W->>DBW: on success advance updated_at; on fail retain + alert (S10/ERR-1)

Detail 2.3 — Database Model (DDL)

Tooling: Rails migrations against the chatbot_gpt database — migrations live in db/chatbot_gpt_migrate/ (per config/database.yml:43 migrations_paths), via the ChatbotGptRecord connection (app/models/chatbot_gpt_record.rb). Schema dump: db/chatbot_gpt_schema.rb. The source/type/agent-conversation tables already exist — this RFC adds one column + one partial-unique guard. Match the verified existing partial-index syntax (where: 'deleted_at is null', e.g. db/migrate/20240724144048_unique_knowledge_store.rb:4).

Where division_id lives — Decision 1 (canonical = the source row). The "one Division = one source" rule (NEG-3) is a property of the source, so division_id is added to ai_knowledge_sources (the source the existing one-per-company guard already keys on), not the agent-conversation rows. Nullable: only agent_conversation_history sources set it; FILE/URL sources leave it null. This makes the existing existence guard division-aware with a DB backstop.

# db/chatbot_gpt_migrate/2026XXXX_add_division_id_to_ai_knowledge_sources.rb
class AddDivisionIdToAiKnowledgeSources < ActiveRecord::Migration[7.1]
def change
add_column :ai_knowledge_sources, :division_id, :string # nullable; set only for conversation sources

# One conversation_history source per (company, division). Partial-unique (paranoid soft-delete),
# matching the verified repo pattern (where: 'deleted_at is null').
add_index :ai_knowledge_sources,
%i[company_id division_id ai_knowledge_source_type_id],
unique: true,
where: 'deleted_at IS NULL AND division_id IS NOT NULL',
name: 'index_ai_knowledge_sources_on_company_division_type'
end
end
  • No new tables. Reuses ai_knowledge_sources (schema L173), ai_knowledge_source_types (L150), ai_knowledge_source_omnichannel_agent_conversations (L137). Only ai_knowledge_sources.division_id is added. The agent-conversation rows (agent_id, agent_name, company_id, ai_knowledge_source_id) are unchanged — they remain the per-agent membership of the (now division-scoped) source.
  • Cardinality: ≤1 conversation source per (company, division); ≤15 agent rows per source.
  • PII classification per column: agent_id/agent_name/company_id/division_id = internal identifiers (not customer PII). No raw customer chat content is stored in chatbot — masked conversation vectors live only in the Data & AI vector store.
  • Retention: rolling 90-day vectors (Data & AI store); source/agent rows retained until source delete (acts_as_paranoid soft delete, then hard vector delete on confirmed delete).

Per-status lifecycle (ai_knowledge_sources.status):

StatusVisibilityRetentionRestore semanticsTransitions allowed
IN_PROGRESSlist row "Training in Progress"until status changesn/a→ ACTIVE, → FAILED
ACTIVElist row "Active" + Last Updatedrolling 90d (vectors)n/a→ ACTIVE (sync), → deleted
FAILEDlist row "Error" / "Last Successful Sync"prior index retainedretry/next sync→ IN_PROGRESS, → deleted
deleted (deleted_at)hiddenhard delete vectorsno restore (re-create)terminal

Detail 2.4 — APIs

Outbound endpoints (consumers call us)

EndpointMethodAuthN/AuthZRequest schemaResponse schemaStatus codesIdempotencyVersioningReuse?
/v1/gpt/ai_knowledge_sources/omnichannel-agent-conversationsPOSTset_role(owner/supervisor/admin); company_id from current_user (not body){ division_id (NEW), agents:[{id,name}] (≤15 NEW) }{ status, code, message, data:[{id, agent_id, agent_name, ai_knowledge_source_id}] }202, 422 (>15 / dup-division / type-not-found), 403division-aware existence guard (NEG-3)v1extended
/v1/gpt/ai_knowledge_sources/omnichannel-agent-conversation/:idGETsamepath :id{ data:[{agent_id, agent_name}], message, status_code } (built in use case; no Grape entity)200, 404, 403n/av1reused
/v1/gpt/ai_knowledge_sourcesGETsamefilters incl. source_type{ status, code, message, data:[{id, name, type:{code,name}, status, updated_at, url}], meta:{page,limit,total_data,total_pages,last_page} }200, 403n/av1reused
/v1/gpt/ai_knowledge_sources/:idDELETEsamepath :id{ status, code, message }200, 422 (in-use S03/ERR-1), 403n/av1extended (in-use guard + conditional vector delete)
/v1/gpt/ai_knowledge_sources/usagesGETsame{ status, code, message, data:[{type, usage, qouta, is_available}], meta } — note literal field is qouta (sic)200, 403n/av1reused
GET /v1/divisions + GET /v1/divisions/:id/agents (config modal directory)GETset_role(owner/supervisor/admin) + company scopepath/query{ data:[{division_id, name, agents:[{id, name}]}] } (proposed)200, 403n/av1new-with-justification — verified: no GET directory endpoint exists in frontend_service (only internal sync PATCH /v1/chat/divisions, role qontak_chat, app/api/internal_service/v1/chat/division.rb:20). Build from Division/AgentsDivision models, or proxy the Omnichannel directory.

Grounded payload examples (field names verified against the Grape entities; see Source Verification). company_id is always taken from current_user, never the request body.

Create — request (POST omnichannel-agent-conversations):

{ "division_id": "div-123", "agents": [ { "id": "agent-1", "name": "Alfian Ramadhan" }, { "id": "agent-2", "name": "Christin Purnama Sari" } ] }

Create — response 202 (Entities::Gpt::AiKnowledgeSource::CreateOmnichannelAgentConversationKnowledgeSource):

{ "status": "success", "code": 202, "message": "Success to create AI Knowledge Source Omnichannel Agent Conversation",
"data": [ { "id": 10, "agent_id": "agent-1", "agent_name": "Alfian Ramadhan", "ai_knowledge_source_id": 55 } ] }

List — response 200 (…/get_response.rbKnowledgeSource + MetaPaginate):

{ "status": "success", "code": 200, "message": "OK",
"data": [ { "id": 55, "name": "Conversation History — Sales", "type": { "code": "agent_conversation_history", "name": "Conversation History" }, "status": "IN_PROGRESS", "updated_at": "2026-06-20T01:00:00Z", "url": null } ],
"meta": { "page": 1, "limit": 10, "total_data": 1, "total_pages": 1, "last_page": true } }

Usages — response 200 (…/get_usage_response.rbUsage; quota hard-coded 1 for this type):

{ "status": "success", "code": 200, "message": "OK",
"data": [ { "type": "agent_conversation_history", "usage": 1, "qouta": 1, "is_available": false } ], "meta": {} }

⚠️ FE must read the misspelled qouta key (verified in entities/.../usage.rb) — do not "correct" it to quota on the client. Rate limits / pagination follow the existing list endpoint (offset).

Inbound webhooks (other services call us)

EndpointMethodAuthN/AuthZSource serviceRequest schemaResponseStatusIdempotencyVersioning
/webhooks/v1/conversation-training-status (NEW, BLOCKED)POSTTBD (Data & AI contract, OQ#4) — follow WebhookRoomResolvedController UUID pattern or HMACData & AI training pipeline{ company_id, status: in_progress|completed|failed, last_updated, error? }{ ok }200, 401, 422by (company_id, source_id, status)v1

Webhook is not buildable until Data & AI publishes the contract (OQ#4). The poll-based UpdateStatus path is the shipping baseline; the webhook is an enhancement (Decision 4/6).

Detail 2.A — UI Contract

For ConversationHistoryForm.vue (new) and SourceDetailDrawer.vue (new):

  • Figma frame URL: n/a — design pending (config form) / AI Resources 10859-83239 (detail).
  • Implementation file: chatbot-fe/components/ai-knowledge/ConversationHistoryForm.vue, …/SourceDetailDrawer.vue.
  • Props type:
interface ConversationHistoryFormProps {
visible: boolean;
maxAgents?: number; // default 15
}
interface ConversationHistoryFormState {
divisionId: string | null;
selectedAgentIds: Set<string>; // capped at maxAgents
}
  • State ownership: local <script setup> refs; persisted via store/ai-assist action ADD_CONVERSATION_HISTORY calling ai-assist.ts add_conversation().
  • Event payloads (analytics, PRD §12): knowledge_source_add_click {source_type:'conversation_history'}; conv_history_config_save {division_id, agent_count(1–15), source_id}.
  • Conditional rendering: tile hidden when plan flag off (NEG-1); Save disabled until divisionId && 1≤count≤15; validation message at 16th selection.
  • A11y: MpSelect/MpCheckbox keyboard nav; counter has aria-live="polite"; validation message linked via aria-describedby.

Detail 2.B — Data-Fetching Strategy

  • Library: $apiMain wrapper (Nuxt useFetch-based) via common/services/main/v1/ai-assist.ts.
  • Cache key: per existing list (no custom SWR cache); refetch on drawer open + after save/delete.
  • Optimistic updates: no — Save awaits 202, then refetch list (status starts IN_PROGRESS).
  • Status polling on the list view: re-fetch list on interval/visit while any row is IN_PROGRESS (matches existing knowledge-list refresh).

Detail 2.C — UI State Matrix

SurfaceLoadingEmptyErrorPartialSuccess
Config form (division/agents)directory fetch spinnerno divisions/agents → disabled Savedirectory load error + Retry (ERR-1)division chosen, agents loading≤15 selected → Save enabled
15-cap countern/a"0/15 selected""Please limit selection to your top 15 experts" + Save disabled"N/15 selected""15/15 selected" valid
Resources listlist fetch skeletonempty-state + "Add source" CTAlist load error + Retryrows w/ mixed statusrows w/ ACTIVE + Last Updated
Status badgeError/"Last Successful Sync" (red)Training in Progress (orange)Active (green)
Source detaildetail fetchn/aload erroragent·division read-only table

Detail 2.D — Data Integrity Matrix

Write pathTransaction scopePartial failure behaviorIdempotency key + TTLConsistencyDuplicate handlingStale-read
Create source + agent rowsChatbotGptRecord.transaction (rows atomic)rollback rows; 422; AI-Service post outside trx → retry via syncone-per-division guardstrong (DB) for rows; eventual for vectorsdup source → 422 already_existlist may briefly show IN_PROGRESS before vectors ready
Daily sync re-postper-source (no cross-source trx)one source fails → others proceed; alertsource_id + sync dateeventual (Data & AI store)re-post is upsert by participant_ids"Last Updated" advances only on success
Delete sourcesoft-delete row + vector hard-deleteif vector delete fails → keep soft-deleted, retrysource_ideventualre-delete is no-oprow hidden immediately

Detail 2.E — Concurrency Collision Map

ResourceWritersCollisionResolutionOn conflict
ai_knowledge_sources (one per division)two Admins creating same division sourceboth pass pre-check, both insertDB partial-unique guard (Decision 1)2nd insert → 422 already_exist (NEG-3)
Same source: daily sync vs Admin deletecron worker + Adminsync re-posts a deleting sourceworker skips soft-deleted rows; check deleted_at before postsync no-ops
Status: poll vs webhook (future)UpdateStatus + webhookboth write statuslast-writer-wins on updated_at; webhook authoritative when presentidempotent by terminal status

Detail 2.F — Async Job / Event Consumer Spec

Job/ConsumerTriggerInputRetryDLQ/retentionConcurrencyIdempotencyTimeoutPoison handling
ConversationHistoryDailySyncWorker (new)sidekiq-cron 0 1 * * * Asia/Jakartanone (queries active sources)sidekiq_options retry: 3 (match workers)Sidekiq dead set (default)queue application_maintenance (per schedule.yml peers)per source_id + dateper-source AI-Service call timeout (follow AiService client)log + Rollbar; skip source, continue
ConversationHistoryStatusSyncWorker (poll, reuses UpdateStatus)periodic while any IN_PROGRESScompany_idretry: 1 (match update_status rescue)n/a (rescues + returns true)lowterminal-status idempotentclient timeoutswallow + Rollbar (existing behavior)
training-status webhook consumer (new, blocked OQ#4)inbound POST{company_id,status,...}per webhook contractper contractn/a(company_id,source_id,status)n/areject 422 on unknown company

Detail 2.F.1 — Responsibility Boundary Matrix

Step (execution order)Owning squad / serviceInbound triggerOutbound effectFailure handlerPRD anchor
1. Config + persist source/agents/divisionBOT (chatbot)Admin SaveDB rows + AI-Service post422 + rollbackS01, S05, NEG-3
2. Fetch resolved rooms (90d) by agentInbox/Platform (ChatService)Data & AI ingestionroom data to Data & AIretry in ingestion§15, S10
3. Quality gate / whisper exclude / text-only / recencyData & AIingestionfiltered contentskip non-qualifyingS07, S08, S09, S13
4. PII maskingData & AIingestionmasked textroom skipped + status failS06, NEG-4
5. Vector index (type:conversation)Data & AIingestionvectors + statusstatus=failedS06
6. Status reflect (poll/webhook)BOT (chatbot)poll / webhookupdate status/updated_atretain prior; alertS06/ERR-1, S10/ERR-1
7. Daily rolling sync + purge >90dBOT trigger + Data & AI purgecronre-ingest/purgeretain index; alertS10, S12
8. Reference room-id link permissionBOT (chatbot)answer renderroom link if permitted403 cross-divisionS11/ERR-1, OQ#5

The PRD allocates masking/training/webhook to Data & AI (§15) — this matrix matches it. The division_id scoping (step 1) is the chatbot-side change vs the existing one-per-company guard.

Detail 2.F.2 — State Surface Contract

EntityState field / eventDefaultUpdated byRead viaStale window
Conversation History sourceai_knowledge_sources.statusIN_PROGRESS on createStatusSync poll (+ future webhook)GET …/ list, detailup to poll interval (~minutes)
Source freshnessai_knowledge_sources.updated_at ("Last Updated")create timedaily sync worker on successlist/detail≤ 24h

Detail 2.G — Cross-Layer Contract Verification

EndpointBE response schemaFE expected schemaMatch?Gaps
POST …/omnichannel-agent-conversations{ status, code, message, data:[{id, agent_id, agent_name, ai_knowledge_source_id}] } snake_caseform sends {division_id, agents[]}; reads data[].idyescompany_id server-side (not body); add division_id to params; 422 codes for >15 / dup-division specified (§3.B)
GET …/ listrows {id,name,source_type,status,updated_at}list expects status enum + updated_atyesconfirm status string mapping ACTIVE/IN_PROGRESS/FAILED ↔ badge
DELETE :id{message} / 422 in-usedialog expects in-use error codepartialdefine in-use error code/message envelope
GET /v1/divisions(/:id/agents) (directory)proposed { data:[{division_id, name, agents:[{id,name}]}] }division-first multiselectn/a — new endpointverified: no existing GET directory (only internal PATCH /v1/chat/divisions); build in chunk 2b — no contract mismatch, net-new

Any "no"/"partial" is a blocker — resolved during chunk 1 (contract confirmation) or moved to §5.

Detail 2.H — End-to-End Data Flow

Admin selects division + ≤15 agents → ConversationHistoryForm → POST omnichannel-agent-conversations → CreateUseCase (validate, persist rows+division_id, transaction) → PostKnowledgeSources → AI-Service (mask+index, async) → 202 IN_PROGRESS → list shows "Training in Progress" → StatusSync poll → status=ACTIVE + Last Updated → daily sync maintains 90d window → Agent asks Airene → indexed conversation answer (+ permission-scoped room reference).

  • Side effects: analytics events (§12); Rollbar/Slack alerts on failure; PaperTrail audit on source rows.
  • Ownership per step: see §2.F.1.

Detail 2.I — Scope Boundaries

  • BE create: app/core/use_cases/.../create_omnichannel_agent_conversation_knowledge_source.rb (extend contract); migration for division_id; new ConversationHistoryDailySyncWorker + config/schedule.yml entry; extend DELETE use case in-use guard.
  • BE NOT touched: AI-Service internals; PII masking; vector store; auto_train_agent_conversation (manual tool stays).
  • FE create: ConversationHistoryForm.vue, SourceDetailDrawer.vue; ai-assist.ts add_conversation(); endpoint.ts entry; wire tile in AddKnowledgeDrawer.vue.
  • FE NOT touched: existing PDF/URL/text flows; Inbox Copilot rendering (reference link is additive).
  • Shared modules: AddKnowledgeDrawer.vue is shared with PDF/URL/text — additive wiring only (impact: low; existing types unaffected).

Detail 2.J — Asset Inventory

AssetTypeSourceFormat & sizesPath
"employee" source tile iconicon@mekari/pixel3 (already referenced L442)Pixel icon font/SVGbundled
status badge dot(DS)@mekari/pixel3 MpBadgen/abundled

No new bespoke assets; reuse Pixel design-system primitives. Any new illustration → design review.


3. High-Availability & Security

HA: the heavy ingestion runs in the Data & AI isolated service (PRD §6) — no chatbot pod load. Chatbot stays stateless; create/list/delete are standard Grape requests; the daily sync is a sidekiq-cron worker on the existing application_maintenance queue. If the AI-Service is down, create returns IN_PROGRESS and the sync/poll retries; prior index is retained on sync failure.

Performance Requirement

  • Frontend: drawer interactions < 100ms; list render reuses existing virtualization; no measurable bundle delta beyond one form component + Pixel imports.
  • Backend: create is a small transactional write + one async AI-Service post; reuses existing endpoint capacity. Initial 15-agent training reaches ACTIVE within ~1h (Data & AI SLA, PRD §6).
  • Load: daily sync iterates ACTIVE sources (bounded; ≤1 per division per company) at 01:00 Jakarta off-peak — matches delete_old_logs_job / partition_chatbot_gpt_maintenance window.

Monitoring & Alerting (PRD §12)

  • Events: knowledge_source_add_click, conv_history_config_save, conv_history_training_status, conv_history_daily_sync.
  • BE: Rollbar on use-case/worker exceptions (existing Rollbar.error); Sidekiq queue latency metric already shipped (SendSidekiqQueueLatencyToDatadogWorker).
  • Alerts: conv_history_training_status=failed 3× consecutive for one source → Slack BOT channel; conv_history_daily_sync=failed → Slack + escalate to Data & AI (PRD §12).
  • Dashboard owner: BOT squad (Hadiningbot).

Logging

  • BE structured logs via existing Rollbar/Papertrail; log company_id, source_id, status, agent_count. Never log raw chat content or agent PII payloads.
  • FE: no PII in client logs.

Security Implications

  • Threat model: (a) cross-tenant data leak via reference room links; (b) over-broad agent selection; (c) PII leakage into vectors (owned by Data & AI masking); (d) unauthorized config by non-admins.
  • AuthZ at every boundary via set_role(%w[owner supervisor admin]) + company/organization scope.

Role × Endpoint Authorization Matrix

RoleEndpoint(s)MethodsTenant scopeUI visibilityConstraintAudit
Admin/Super Adminall conversation endpointsPOST/GET/DELETEown company/orgfull configone-per-division; ≤15PaperTrail
Agentroom-by-id (reference)GET (read)own permitted roomsreference link onlyroom-view permission (S11)n/a
Non-Qontak360nonetile hidden (NEG-1)plan flagn/a
Data & AI servicestatus webhook (future)POSTper company_idwebhook auth (OQ#4)sync logs
  • Ownership validation: ai_knowledge_sources.company_id == current_user['company_id'] enforced in use case/repository (current_user_helpers.rb).
  • Input validation: agents array ≤15, each {id,name} filled (Dry contract); division_id filled.
  • Injection: parameterized AR queries; outbound AI-Service URL is fixed config (no SSRF on user input).
  • Secrets: AI-Service base URL/keys via existing config/env (no hard-coding).
  • Reference-link permission (PRD OQ#5/S11): validate requesting user's room-view permission on every reference open; deny cross-division (403).
  • Static analysis: brakeman (BE), lint:ts (FE).
  • Compliance: see §3.D.

Detail 3.A — Failure Mode Catalog (merged)

SurfaceFE behavior on failureBE response on failureCodes consistent?
Directory loadload error + Retry, Save disabled (ERR-1)4xx from directory readyes
Create > 15 agentsinline validation, Save disabled (S04/ERR-1)422yes
Create dup divisioninline error (NEG-3)422 already_existyes
Delete in-usedialog blocks + message (S03/ERR-1)422 in-useyes
Training/masking failrow shows Error / Last Successful Sync (S06/ERR-1)status=FAILED (poll/webhook)yes
Daily sync failLast Updated unchangedretain prior index + alert (S10/ERR-1)yes
Cross-division referencepermission error toast (S11/ERR-1)403yes

Detail 3.A.1 — Branch & Skip Catalog

Branch triggerWhere checkedDownstream effectAuditUser-visible?
Not Qontak360 (plan flag off)FE tile gate + BE plan checktile hidden / create rejected (NEG-1)n/ayes (hidden)
Division already sourcedBE unique guard + FE pre-checkblock create (NEG-3)source rowyes (error)
Agent has 0 qualifying roomsData & AI ingestionagent ignored, others proceed (NEG-2)sync logno
Internal whisper / non-textData & AI ingestionexcluded (NEG-4, S08, S13)sync logno
Agent deactivateddaily syncno new ingest; ages out 90d (S12)sync logno

Detail 3.B — Error Response Catalog (BE)

Shape: { "message": [...], "code": <http>, "errors": {...} } (existing ErrorException).

EndpointCodeHTTPMessageWhenUser-facing?
createagent_limit_exceeded422"Please limit selection to your top 15 experts">15 agents (S04)yes
createalready_exist422division already has a sourceNEG-3yes
createnot_found (type)422source type missingmisconfigno
deletein_use422remove from AI Agents firstS03/ERR-1yes
referenceforbidden403no permission for this roomS11/ERR-1yes

Detail 3.C — Error Message Catalog (FE)

ErrorUser-facing messageSurfacei18n key
cap exceeded"Please limit selection to your top 15 experts"inline + Save disabledhard-coded (no i18n)
directory load"Couldn't load divisions/agents. Retry."inline + Retryhard-coded
save failed"Failed to add Conversation History source."toast (variant error)hard-coded
delete in-use"Remove this source from all AI Agents before deleting."dialoghard-coded

Detail 3.D — Compliance & Data Governance (triggered: PII)

FieldClassificationLegal basisRetentionEncryptionAccess auditRight-to-delete
Raw chat content (pre-mask)PII (customer)UU PDP / existing consent (PRD §15)not persisted post-processing (transient)in transit (HTTPS)Data & AI logsdiscarded after masking
Masked conversation vectorsde-identifiedlegitimate interest / consentrolling 90dat rest (vector store)Data & AIpurged on source delete (hard)
agent_id/name/division_idinternal identifieroperationaluntil source deleteDB at restPaperTrailsoft→hard delete on source delete

Masking correctness (100% standard phone/email) is the Internal Alpha gate (PRD §14) and an open risk owned by Data & AI (OQ#1).

Detail 3.E — Accessibility

WCAG AA: keyboard nav through division select + agent checkboxes; counter aria-live; validation message aria-describedby; focus trap in drawer; color-independent status (badge text + color).


4. Backwards Compatibility and Rollout Plan

Compatibility

  • BE: additive — new column (nullable, backfilled), extended contract (additive params), reused endpoints. Existing PDF/URL/text sources unaffected. No API version bump.
  • FE: additive — wires an existing tile + new components; existing flows untouched.
  • Migration: none for data (new conversation_history capability; no backfill of vectors). The division_id column backfills from existing agent rows where determinable, else stays null until re-config.

Rollout Strategy

  • Deploy order: BE first (migration + extended create + sync worker behind flag), then FE (wire tile). FE depends on BE create accepting division_id.
  • Feature flag: SystemPreference(group_code:'rollout', code:'conversation_history') default OFF, enabled per Qontak360 account. Single flag gates both BE create acceptance and FE tile.
  • Stages (PRD §11/§14): Internal Alpha (Mekari CS) → Closed Beta (5–10 Qontak360) → Open Beta/Limited GA (Service Suite + Pro-above) → GA.
  • Stage gates: Alpha: 100% mask standard phone/email + <1h/15-agent training; Beta: >70% suggestion acceptance; Limited GA: no perf degradation in 24h sync window.
  • Rollback: toggle flag OFF (hides tile, rejects new creates); existing sources stay until explicitly deleted. Sync worker no-ops when flag off.
  • Stop conditions: widespread masking/index failures unresolved within SLA → PM disables flag for affected accounts (PRD §12).

Detail 4.A — Cross-Layer Rollout Compatibility Matrix

ScenarioFEBEWorks?Mitigation
Pre-deployOldOldyesbaseline (tile un-wired)
Backend firstOldNewyesnew column nullable; old FE never sends division_id
Frontend firstNewOldnoavoid — deploy BE first (FE needs division_id accept)
Both deployedNewNewyestarget
Backend rollbackNewOldpartialflag OFF hides FE tile; FE degrades to no-conv-history
Frontend rollbackOldNewyesBE accepts but no UI creates; harmless

Detail 4.B — Configuration Contract

LayerEnv var / flagTypeDefaultRequiredProvisionerSecret?
BESystemPreference rollout/conversation_historyflag (bool)OFFyesDB seed / adminno
BEAI-Service base URL/keys (existing)envexistingyesconfig/Vaultyes
BEdaily sync schedule (config/schedule.yml)cron0 1 * * * Asia/Jakartayesrepono
FEreads plan flag via APIn/ano

Detail 4.C — Test Plan (commands sourced from repo)

LayerCommandWhat it must prove
BE unit/use-caserspec spec/core/use_cases/.../create_omnichannel_agent_conversation_knowledge_source_spec.rb (AGENTS.md test section)≤15 cap, division guard, 202
BE workerrspec spec/workers/conversation_history_daily_sync_worker_spec.rb (rspec-sidekiq)enqueues per active source; failure isolation
BE lint/securityrubocop ; brakeman (AGENTS.md)style + no security regression
BE migrationrails db:migrate then rails db:rollback (AGENTS.md)division_id add + index up/down
FE unitpnpm test (vitest) (package.json scripts)counter/validation; tile wiring
FE e2epnpm test:e2e (playwright)add → IN_PROGRESS row; delete in-use blocked
FE lintpnpm lint:ts && pnpm lint:prettiertype + format
FE buildpnpm buildbundles

Detail 4.D — Agent Execution Plan

OrderLayerChunkFilesCommandsAcceptance criteria
1BEConfirm contracts & entities (read-only)ai_knowledge_sources.rb, entities/.../knowledge_source.rb, post_knowledge_sources.rbrspec spec/api/...ai_knowledge_sources_spec.rbexisting create/list/detail/delete specs green (baseline)
2BEMigration: add ai_knowledge_sources.division_id + partial-unique index (chatbot_gpt DB)db/chatbot_gpt_migrate/2026XXXX_add_division_id_to_ai_knowledge_sources.rb, db/chatbot_gpt_schema.rbrails db:migrate && rails db:rollback && rails db:migrate (chatbot_gpt)column + index_ai_knowledge_sources_on_company_division_type present; rollback clean
2bBEDirectory endpoint: GET /v1/divisions + GET /v1/divisions/:id/agents (build from Division/AgentsDivision)new Grape resource under app/api/frontend_service/v1/, new entityrspec spec/api/...divisions_spec.rbreturns divisions + nested agents, company-scoped, set_role enforced
3BEExtend create use case: division_id param + ≤15 cap + division-aware existence guard…/create_omnichannel_agent_conversation_knowledge_source.rbrspec …create_omnichannel…_spec.rb16 agents→422; dup (company,division)→422; valid→202
4BEExtend DELETE in-use guard + conditional delete_vector_db…/ai_knowledge_source/delete.rb, lib/ai_service/...rspec …delete_spec.rbin-use→422; unused→vectors deleted
5BEDaily sync worker + schedule.yml entryapp/workers/conversation_history_daily_sync_worker.rb, config/schedule.ymlrspec spec/workers/...worker re-posts per ACTIVE source; failure isolated + alert
6FEService + endpoint + wire tilecommon/services/main/v1/ai-assist.ts, endpoint.ts, components/AddKnowledgeDrawer.vuepnpm lint:ts && pnpm testtile selectable; service hits create endpoint
7FEConversationHistoryForm.vue (division-first, ≤15 counter, tooltip) — consumes chunk 2b directory endpointnew component + store/ai-assist actionpnpm testcounter 0–15; 16th disables Save + message; save → IN_PROGRESS (needs Figma frame — §5 blocker 3)
8FEResources list: status states + Last Updated + remove/deletepages/ai-knowledge/training-sources/index.vue, knowledge-list.vue, SourceDetailDrawer.vuepnpm test:e2ebadges per status; delete in-use blocked
9bothReference room-id link + permission (S11, Could-Have)Inbox Copilot ref render + room authzrspec + pnpm test:e2ecross-division open → 403
10BE(Deferred/blocked) training-status webhooknew controller + use caserspeconly when OQ#4 contract published

Detail 4.E — Verification & Rollback Recipe

  • Pre-merge (per layer):
    • BE: 1) rubocop 2) brakeman 3) rspec 4) rails db:migrate (+ rollback check)
    • FE: 1) pnpm lint:ts && pnpm lint:prettier 2) pnpm test 3) pnpm test:e2e 4) pnpm build
  • Post-deploy signals: Datadog Sidekiq queue latency for application_maintenance stable; conv_history_training_status failure rate < threshold over first 24h; create endpoint 5xx < 0.1%.
  • Rollback (deploy-order-aware):
    1. Toggle SystemPreference rollout/conversation_history OFF (hides FE tile + rejects creates).
    2. If migration must be reverted: rails db:rollback (only the division_id migration; safe — nullable).
    3. Confirm existing PDF/URL sources unaffected and Sidekiq sync worker no-ops.

Detail 4.F — Resource & Cost Notes

  • Compute: negligible — one daily off-peak worker; create/list reuse existing endpoints.
  • DB: one nullable column + one partial index; minimal growth (≤1 source/division, ≤15 agent rows).
  • Network: AI-Service calls already in use; daily sync adds N (active sources) posts/day.
  • Storage: masked vectors live in Data & AI store (rolling 90d), not chatbot.

5. Concern, Questions, or Known Limitations

Blockers (gate §7 to no):

  1. [REV-3] Training-status webhook contract (PRD OQ#4, Data & AI). States, payload, retry/auth undefined. Baseline ships on the existing poll (UpdateStatus); the webhook (chunk 10) is blocked.
  2. [REV-4] PII masking spec/SLA (PRD OQ#1, Data & AI). 100%-mask gate for standard phone/email is the Internal Alpha gate; masking lives entirely in the Data & AI pipeline.
  3. [REV-2] Design DRAFT (PRD OQ#7, Design). No Figma frame for the config form (division-first multiselect, 15-cap validation, expertise tooltip) or the training-status list states. The qontak-designer prototype uses a "Use as source" toggle + fixed read-only agent list and only Active/Inactive states — it does not match the PRD. Do not build chunk 7/8 UI against it.
  4. Divisions+agents directory endpoint unverified RESOLVED (R1 review; re-confirmed R2, REV-1). Verified there is no GET directory endpoint (only internal sync PATCH /v1/chat/divisions, app/api/internal_service/v1/chat/division.rb:20). Now specified as a new endpoint (GET /v1/divisions(/:id/agents)) with a proposed contract in §2.4 and execution-plan chunk 2b. No longer a contract mismatch.
  5. [REV-5] Source-scoping decision (Decision 1) — needs PM sign-off (technical spec now concrete). The PRD mandates one-Division-one-source; existing code enforces one-per-company. The technical resolution is specified (add nullable ai_knowledge_sources.division_id + partial-unique (company_id, division_id, type_id), §2.3 DDL; division-aware existence guard, chunk 3). The only open item is PM + eng confirmation of the scoping choice before chunk 2/3 land.

Open questions (non-blocking but tracked): Copilot quota model (PRD OQ#3 — usages currently quota=1); Jira epic/keys (PRD OQ#2 — stories are TEMP placeholders); "resolved + ≥5 msgs" as a useful-resolution proxy (PRD OQ#6); [REV-11] create posts to AI-Service outside the DB transaction — a crash between COMMIT and post can leave a source IN_PROGRESS until the next 01:00 sync (≤24h); consider an immediate post-retry on create (rfc-reviewer R2).

Known limitations: text-only this phase (S13); daily (not real-time) freshness; 15-agent cap; reference link is Could-Have (S11) and may slip; recency/quality/whisper filters are Data & AI's and not independently testable from chatbot.


6. Comment logs

DateComment(s) FromAction Item(s)
2026-06-20RFC drafted (rfc-starter)Circulate to BOT + Data & AI + FE; resolve §5 blockers
2026-06-20rfc-reviewer R1 (6.5/10, HOLD) — see conversation-history-review.mdActed on in-control items: §2.4 grounded JSON examples + verified directory endpoint is net-new (REV: ACV); §2.3 concrete one-per-division DDL on ai_knowledge_sources (REV: scoping). Cross-team items (design frames, webhook, masking SLA) remain.
2026-06-20rfc-reviewer R2 (7.5/10, HOLD) — see conversation-history-review.mdDelta re-review at chatbot fa6dd8b79 / chatbot-fe f5b80d5a. Re-grounded every citation — all valid. Confirmed R1 fixes hold (REV-1/6/7/8 closed); cross-layer mismatch resolved → 6.5 cap lifted, ACV 5.5→7.0. New finding REV-11 (non-atomic create→AI-Service post) tracked above. Remaining blockers all cross-team/sign-off: design frames (REV-2), webhook (REV-3), masking SLA (REV-4), scoping sign-off (REV-5).

7. Ready for agent execution

  • no
  • Missing execution-readiness gates (must close before yes):
    • Design References (FE half): config-form + training-status-state Figma frames missing (§5 blocker 3) — cannot build chunks 7–8 UI against the divergent prototype.
    • Inbound webhook contract: undefined (§5 blocker 1) — chunk 10 blocked; poll baseline is fine.
    • Source-scoping decision: technical spec now concrete (§2.3 DDL); needs PM+eng sign-off on the scoping choice (§5 blocker 5).
    • Compliance dependency: masking SLA owned by Data & AI (§5 blocker 2).
  • Closed in R1 review: cross-layer contract gaps — §2.4 now carries grounded request/response JSON for create/list/detail/usages (verified field names incl. the qouta typo); the divisions+agents directory is verified-absent and specified as a new endpoint (§2.4 + chunk 2b), removing the §2.G "unverified" row; the one-per-division DDL is now concrete (§2.3).
  • Already green: PRD-to-Schema derivation, Repo Reading Guide + Source Verification (verified against real files), DDL, reused/extended/new API tags + payload examples, mermaid diagrams (topology via component + sequence + state + branch), state surface contract, failure/branch catalogs, rollout + rollback recipe, agent execution plan (chunks 1–9 + 2b buildable; FE chunks 7–8 need the design frames; chunk 10 deferred to the webhook contract).

Remaining blockers: 1 (webhook contract, Data & AI), 2 (masking SLA, Data & AI), 3 (design frames, Design), 5 (scoping sign-off, PM+eng). Once closed, re-run checklist.md / rfc-reviewer.