Background worker service for automatically updating Polis math calculations and AI-generated cluster insights for conversations in the Agora Citizen Network.
The math-updater service is a background worker that periodically scans active conversations and triggers recalculation of Polis clustering mathematics. It processes voting data, computes opinion statistics, cluster assignments, and generates AI-powered cluster labels and summaries using LLM.
- Automated Math Updates: Periodically scans conversations and triggers Polis math recalculation
- Job Queue Management: Uses pg-boss for reliable job queue management
- Cluster Analysis: Processes Polis math results to generate cluster statistics and assignments
- AI-Powered Insights: Generates neutral, concise cluster labels and summaries using AWS Bedrock LLMs
- Configurable Intervals: Customizable scan intervals and update frequency
- Graceful Shutdown: Handles SIGTERM/SIGINT for clean service termination
The service consists of two main job types:
- Runs on a self-scheduling loop (default: every 2 seconds)
- Scans
conversation_update_queuetable for pending math updates - Queues
update-conversation-mathjobs for eligible conversations - Rate limiting: Respects 2-second minimum between updates per conversation
- Singleton protection: Uses pg-boss singleton keys to prevent duplicate jobs per conversation (2s-28s windows based on conversation size)
- Uses self-scheduling pattern: each job schedules the next run after completion
- Error-resilient: continues scheduling even if scan encounters errors
- Counter reconciliation: Recalculates accurate counters from actual database records (self-healing)
- Fetches voting data for a specific conversation
- Calls external Polis service to compute clustering mathematics
- Batch processing: Handles large conversations (19K+ opinions) via batching to avoid stack overflow
- Processes and stores math results (priorities, consensus, clusters, etc.)
- Optionally generates AI labels and summaries for clusters
- Updates conversation table counters and marks queue entry as processed
- Race-condition safe: Only marks processed if no newer update arrived during processing
Configuration is managed via environment variables. See env.example for required variables.
Primary Database (Required):
CONNECTION_STRING: PostgreSQL connection string for primary database- Used by pg-boss for job queue management (writes)
- Used for all database writes (opinion updates, counter updates, math results)
Alternative: AWS Production Mode
DB_HOST: Primary database host (e.g.,primary.region.rds.amazonaws.com)DB_PORT: Database port (default: 5432)DB_NAME: Database nameAWS_SECRET_ID: AWS Secrets Manager secret ID containing database credentialsAWS_SECRET_REGION: AWS region for Secrets Manager
Read Replica (Optional):
CONNECTION_STRING_READ: PostgreSQL connection string for read replica- Used for SELECT queries (fetching votes, reading conversation data)
- Falls back to primary if not configured
DB_HOST_READ: Read replica host (e.g.,replica.region.rds.amazonaws.com)DB_PORT_READ: Read replica port (default: 5432)AWS_SECRET_ID_READ: AWS Secrets Manager secret ID for read replica credentialsAWS_SECRET_REGION_READ: AWS region for read replica secrets
Important Notes:
- pg-boss always uses the primary database (via
CONNECTION_STRINGorDB_HOST)- pg-boss manages its own connection pool independently
- The
pgbossschema must exist on the primary database
- Business logic queries (via
dbobject) use read replica for SELECTs when configuredgetPolisVotes()reads from replica (acceptable ~1s staleness)- All writes (updates, inserts) automatically route to primary via
withReplicas()
- Replication lag: Typically <1 second, acceptable for math updates (2s minimum rate limit)
POLIS_BASE_URL: Base URL for the Polis math computation service
MATH_UPDATER_SCAN_INTERVAL_MS: How often to scan for conversations needing updates (default: 2000ms = 2 seconds, min: 2000ms)MATH_UPDATER_BATCH_SIZE: Number of jobs to fetch per batch from the queue. Also determines database connection pool size (batch size + 5) (default: auto-calculated as 2x concurrency)MATH_UPDATER_JOB_CONCURRENCY: Number of jobs that execute concurrently within each batch. Limits concurrent heavy database operations to protect the database server (default: auto-calculated from TOTAL_VCPUS)MATH_UPDATER_MIN_TIME_BETWEEN_UPDATES_MS: Minimum time between updates for a single conversation (default: 2000ms = 2 seconds, min: 2000ms)TOTAL_VCPUS: Total vCPUs available (used to auto-calculate concurrency settings) (default: 2)
AWS_SECRET_ID: AWS Secrets Manager secret ID (optional, for production)AWS_SECRET_REGION: AWS region for Secrets Manager (optional)AWS_AI_LABEL_SUMMARY_ENABLE: Enable/disable AI label and summary generation (default: true)AWS_AI_LABEL_SUMMARY_REGION: AWS region for Bedrock (default: "eu-west-1")AWS_AI_LABEL_SUMMARY_MODEL_ID: Bedrock model ID (default: "mistral.mistral-large-2402-v1:0")AWS_AI_LABEL_SUMMARY_TEMPERATURE: LLM temperature (default: "0.4")AWS_AI_LABEL_SUMMARY_TOP_P: LLM top_p parameter (default: "0.8")AWS_AI_LABEL_SUMMARY_MAX_TOKENS: Maximum tokens for LLM response (default: "8192")AWS_AI_LABEL_SUMMARY_PROMPT: Custom prompt for AI label/summary generation (see config.ts for default)
When enabled, the service uses AWS Bedrock to generate:
-
Cluster Labels: Short (1-2 words), neutral, agentive nouns describing each cluster's ideological position
- Examples: "Skeptics", "Technologists", "Redistributionists", "Pragmatists"
- Avoids policy-specific terms, geographic references, and abstract concepts
- Professional/academic tone that reflects generality and positionality
-
Cluster Summaries: Concise (≤300 chars), neutral summaries of each cluster's perspective
- Grounded in cluster's agreement/disagreement patterns
- Reflects cluster's stance relative to conversation context
- Covers all representative opinions without repetition
The AI prompt is carefully designed to:
- Detect sarcasm and irony
- Analyze each cluster independently
- Consider whether opinions are supported or rejected by the cluster
- Generate abstract, context-independent labels
- Produce neutral, professional summaries
- Node.js 16+
- PostgreSQL database
- Access to Polis service
- AWS credentials (if using AI features)
pnpm install# Development mode with auto-reload
pnpm start:dev
# Production build
pnpm build
pnpm startpnpm lint
pnpm format:check
pnpm format:write-
Initialization: Service connects to database and initializes pg-boss job queue with singleton policy
- Queue policy ensures only 1 job per conversation (created OR active) to prevent duplicate processing
-
Loop Kickoff: Sends initial
scan-conversationsjob with singleton key to start the self-scheduling loop -
Conversation Scanning: Scan job queries
conversation_update_queuetable for pending updates- Reads conversations where
processed_at IS NULL - Respects rate limiting via
last_math_update_at(2s minimum between updates) - Orders by
last_math_update_at ASC NULLS FIRST(prioritizes never-updated and oldest)
- Reads conversations where
-
Job Queueing: Eligible conversations are queued as
update-conversation-mathjobs- Each job includes captured
requestedAttimestamp for race-condition detection - Uses
singletonKey: update-math-${conversationId}per conversation - Dynamic
singletonSecondsbased on conversation size (2s-28s)
- Each job includes captured
-
Self-Scheduling: After each scan, the job schedules itself to run again after
MATH_UPDATER_SCAN_INTERVAL_MS- Uses
singletonKeyto prevent duplicate loops - Always reschedules, even if the scan encounters errors
- Creates a continuous, reliable scanning loop
- Uses
-
Counter Reconciliation (services/math-updater/src/conversationCounters.ts):
- Recalculates
opinion_count,vote_count,participant_countfrom actual DB records - Self-healing: fixes drift from soft deletes, moderation, user deletion
- Updates
lastReactedAtfor activity tracking - Logs any discrepancies found
- Recalculates
-
Math Processing: Worker jobs fetch votes, call Polis service, process results
- Batches large conversations (1000 opinions per batch) to avoid stack overflow
- Handles conversations with 100K+ votes and 19K+ opinions
-
Database Updates: Math results are stored in database, updating:
- Conversation counters (opinion_count, vote_count, participant_count, lastReactedAt)
- Opinion priorities, consensus levels, divisiveness scores
- Cluster assignments for participants
- Cluster statistics (agreement/disagreement counts per opinion)
- Representative opinions for each cluster
-
AI Enhancement: If enabled, generates AI-powered cluster labels and summaries
-
Queue Completion (race-condition safe):
- Marks queue entry as
processedAt = NOW()only ifrequestedAtunchanged - If
requestedAtchanged during processing → new update arrived → this is now stale - Always updates
last_math_update_atfor rate limiting - Newer updates automatically picked up by next scan
- Marks queue entry as
The service interacts with several database tables:
conversation_update_queue: Tracks pending math updates with rate limitingconversation_id(PRIMARY KEY): Deduplicates queue entriesrequested_at: When update was requested (used for race-condition detection)processed_at: NULL = pending, NOT NULL = processedlast_math_update_at: Tracks actual processing time (enables 2s rate limiting)
conversation: Stores conversation metadata, counters, and current math content reference- Counters:
opinion_count,vote_count,participant_count,lastReactedAt - Updated by math-updater via counter reconciliation
- Counters:
opinion: Stores opinions with math-computed statistics (priority, consensus, divisiveness)vote: Stores user votes on opinionspolis_content: Stores raw Polis math resultspolis_cluster: Stores cluster metadatapolis_cluster_user: Maps users to clusterspolis_cluster_opinion: Stores representative opinions for each cluster
- Database connection errors are logged and cause service shutdown
- Math computation errors are logged but don't crash the service
- AI generation errors are caught and logged, allowing math updates to complete
- Job failures are handled by pg-boss retry mechanism
The service logs important events:
- Service startup and shutdown
- Job registrations and scheduling
- Conversation scans with slugIds:
[Scan] Found 3 conversation(s) needing math updates: [SIP3Kg, sfoFIQ, 15I-Jw] - Enqueued vs skipped conversations:
[Scan] Successfully enqueued 2 conversation(s): [sfoFIQ, 15I-Jw] - Counter reconciliation discrepancies:
[Counter] Fixing counters for R3NBkA: diff { opinions: -2, votes: -3 } - Math processing times for large conversations (113K votes: 50-85 seconds)
- AI label/summary generation
- Errors and warnings
Key Metrics to Watch:
- Counter drift frequency (should be occasional, not every update)
- Large conversations processing time (>30s indicates heavy load)
- Singleton job rejections (normal behavior, prevents duplicate work)
- Queue depth (pending updates in
conversation_update_queue)
Use structured logging output to monitor service health and performance.
This service is licensed under the AGPL v3 license. See COPYING for details.
api: Main API service that triggers manual math updatespython-bridge: Python bridge for Polis math computationshared-backend: Shared database schema and utilities
Contributions must comply with the Fiduciary Licensing Agreement (FLA).