- Schema: SyncRouteRuleMap junction table, ruleIntent on TenantRule,
ForwardFormat/Frequency/ApprovalMode columns on SyncRoute, INTEREST
match type
- Worker: remove global AUTO_APPROVE forwarding; per-route rule evaluation
in ingest processor — POSITIVE rules auto-forward that route, NEGATIVE
rules block it, fallback to approvalMode
- API: PATCH /routes/:id, rule assign/unassign endpoints, approve now
accepts targetGroupIds, new GET /messages/:id/routes for popup data
- Web: 5-column route table with settings + rules dialogs, ForwardTarget
popup with rule-based pre-selection, ruleIntent toggle + emoji dropdown
(👍/👎 only) + INTEREST type in rules manager
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract quotedPlatformMsgId from Baileys contextInfo.stanzaId in normalizer
- Thread quotedPlatformMsgId through IngestJobData → ingest processor → Message record
- Widen ingest processor group/tenant selects to include name and organizationId
- Add replyToMessageId, sourceGroupName, organizationId to StreamMessagePayload and XADD
- Update StreamRecord, parseFields, and streamMessages in AI stream service with new fields
- Add optional ?tenant= and ?source= SSE filters; cursor advances for filtered entries
so Last-Event-ID resume remains accurate across reconnects
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Publish every non-spam message to tower:stream:messages immediately
after DB insert in ingest.processor — before any admin approval or
routing decision
- Remove early tags.length === 0 drop in main.ts so no-rule-match
messages are now stored and streamed (monitor-all behaviour)
- Wire classify worker, review/p1/dnc queues into main.ts
- Add streams/message-stream.ts with XADD wrapper (capped at ~50k entries)
- Add docs/DATA_MODELS.md — full data model reference for all 29 models
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap groupFetchAllParticipating in try/catch to prevent unhandled rejection on connect
- Catch errors from async onMessage/onGroups callbacks via Promise.resolve().catch
- Return null from normalizer when key.id is missing (prevents empty upsert key collision)
- Extract parseRedisUrl to redis-connection.ts to eliminate duplication
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Idempotent message persistence via prisma.message.upsert. Uses URL parsing
to pass connection options to BullMQ, avoiding ioredis version conflicts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>