feat: add Redis Stream handoff for AI dev + monitor all messages

- 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>
This commit is contained in:
2026-06-19 13:56:10 +05:30
parent 6de78fe270
commit d21bd6bcb4
4 changed files with 1241 additions and 6 deletions
+21 -1
View File
@@ -1,7 +1,9 @@
import { Worker, Queue } from 'bullmq';
import Redis from 'ioredis';
import { IngestJobData, ForwardJobData, IndexJobData } from '@tower/types';
import { parseRedisUrl } from './redis-connection';
import { approveMessage } from '../core/approve-message';
import { publishToMessageStream } from '../streams/message-stream';
import { createLogger } from '@tower/logger';
const logger = createLogger('ingest-processor');
@@ -12,6 +14,7 @@ export async function processIngestJob(
pool?: any,
forwardQueue?: Queue<ForwardJobData>,
indexQueue?: Queue<IndexJobData>,
redis?: Redis,
): Promise<void> {
// Defensive: drop messages from non-CLAIMED groups
const group = await prisma.group.findUnique({
@@ -87,6 +90,22 @@ export async function processIngestJob(
update: {},
});
// Publish to AI-dev stream immediately — before any routing decision.
// Admin approval controls forwarding only; it must never block the stream.
if (redis) {
publishToMessageStream(redis, {
messageId: msg.id,
tenantId: job.tenantId,
content: job.content,
senderJid: job.senderJid,
senderName: job.senderName,
sourceGroupId: job.sourceGroupId,
tags: job.tags,
effectiveAction: job.effectiveAction ?? null,
timestamp: new Date().toISOString(),
}).catch((err: unknown) => logger.warn({ err, messageId: msg.id }, 'Failed to publish to message stream'));
}
// For REJECT, create an approval record so it's searchable as rejected
if (job.effectiveAction === 'REJECT') {
await prisma.approval.upsert({
@@ -133,10 +152,11 @@ export function createIngestWorker(
pool?: any,
forwardQueue?: Queue<ForwardJobData>,
indexQueue?: Queue<IndexJobData>,
redis?: Redis,
): Worker<IngestJobData> {
return new Worker<IngestJobData>(
'tower-ingest',
async (job) => processIngestJob(job.data, prisma, pool, forwardQueue, indexQueue),
async (job) => processIngestJob(job.data, prisma, pool, forwardQueue, indexQueue, redis),
{ connection: parseRedisUrl(redisUrl) },
);
}