Opinión

LMS y CRM de 205.000 líneas de código con agentes de IA: el precio de la puesta en producción, tres estados de un lead y un carácter \r

Dos sistemas en producción interconectados —una plataforma educativa LMS (121.853 líneas de TS/TSX, 47 routers, 288 procedimientos) y un CRM con un registro único de 334 herramientas (83.251 líneas)— se escribieron sin equipo de desarrolladores mediante agentes de IA sobre una única instancia de PostgreSQL. Escribir el código tomó unas tardes, pero el verdadero precio lo marcó el pulido para producción: 14 defectos críticos en la fase de revisión adversarial, otros 5 al verificar las propias correcciones, y un carácter invisible de retorno de carro \r que desactivó silenciosamente la práctica interactiva en 138 de 146 lecciones. Integrar de forma fiable agentes de IA con un negocio real exigió dividir el trabajo con el lead en tres estados independientes bajo bloqueo FOR UPDATE, reconstruir el historial de la conversación en el servidor y guardar el propio registro de la reunión antes de llamar a las API externas de Zoom y Google Calendar.

LMS y CRM de 205.000 líneas de código con agentes de IA: el precio de la puesta en producción, tres estados de un lead y un carácter \r
Hamidun News.
Escuchar artículo
  • Volumen de la base de código sin equipo de desarrolladores: 205.104 líneas de TypeScript/TSX (121.853 en el LMS repartidas en 532 archivos y 83.251 en el CRM repartidas en 367 archivos)
  • Superficie de la API y de la fachada agéntica: 47 routers y 288 procedimientos tRPC en el LMS (93 herramientas) + 334 herramientas en el registro del CRM (257 expuestas en la fachada)
  • Alcance de un único bug invisible de CRLF: 138 de 146 lecciones prácticas con cero envíos exitosos por culpa de un carácter `\r` final al analizar el texto en el servidor
  • Cosecha de la revisión adversarial antes del lanzamiento: 14 vulnerabilidades peligrosas en la primera ronda + 5 defectos introducidos por las propias correcciones

Hoy escribir un sistema grande con agentes de IA sin plantilla de programadores se ha convertido en una tarea barata. Lo caro es todo lo que empieza a la mañana siguiente de la demo: la protección contra la manipulación del historial por parte del cliente, los bloqueos en la base de datos cuando los gestores hacen doble clic y los caracteres especiales invisibles en los secretos.

Mis dos sistemas —la plataforma educativa LMS (121.853 líneas de TS/TSX, Next 16.2.

6, React 19, Drizzle ORM, 58 migraciones, 16 itinerarios, 211 lecciones y 55 simuladores) y el CRM que opera junto a ella (83.251 líneas, Prisma 7.6, 48 modelos de base de datos, BullMQ y cuatro agentes de IA conectados)— conviven sobre una misma instancia de PostgreSQL.

Construimos nuestro propio stack por dos razones: ningún LMS comercial ofrece de fábrica el primitivo que permite que la red neuronal primero interprete el prompt débil de un estudiante y solo después lo guíe según un preámbulo oculto, y el CRM debía integrarse sin duplicar bases de datos sobre un bot de embudo que ya funcionaba en Telegram. Por cierto, repintar las 289 variables de color al bifurcar el CRM tomó una tarde, mientras que limpiar los datos de demostración heredados (205 solicitudes de prueba, un margen de respaldo del 38% y insignias falsas de "Tareas 7") se extendió durante dos meses.

¿Cómo rompió un carácter invisible de retorno de carro \r 138

lecciones, la deduplicación y el OAuth?

El bug más traicionero del LMS no dejaba errores en los registros. Los estudiantes veían el bloque `:::practice` renderizado correctamente, pulsaban "Enviar" y recibían "Bloque práctico no encontrado". Las estadísticas de la base de datos mostraron que en 138 de 146 lecciones hubo exactamente cero intentos exitosos. El contenido se creaba en Windows con saltos de línea `CRLF` (`\r\n`): el frontend normalizaba el texto antes de renderizarlo, pero el analizador del servidor cortaba por `\n`, dejando un `\r` invisible al final de la línea:

```typescript // Sin normalización, el \r final rompe el ancla $ en JS, // ya que el punto (.) no captura \r, y el campo task se analiza vacío: md = md.replace(/\r\n?/g, "\n"); const match = line.match(/^(task|starter|goal|model|system)\s:\s?(.)$/i); ```

El mismo `\r` cambiaba el hash de control del texto al dividirlo en fragmentos, obligando al sistema a pagar de nuevo por la generación de embeddings, y la tercera vez apareció al final de un secreto en el `.env`, lo que provocaba que Google OAuth devolviera `invalid_client` (detectado con una comprobación de longitud de cadena `awk -F= '/^GOOGLE_/ {print $1, length($2)}' .env` — 36 bytes en lugar de 35).

¿Por qué no se puede confiar en los datos que llegan del navegador y

cómo está construido el registro de 334 herramientas del CRM?

La revisión adversarial previa al lanzamiento reveló 14 defectos graves, y la verificación de los parches escritos para corregirlos reveló otros 5. En la primera versión de la práctica, el propio navegador enviaba al servidor el texto de la tarea y el array `history`, lo que permitía suplantar una respuesta desde las DevTools, y 8 de los 55 simuladores se aprobaban con 71 a 90 puntos simplemente copiando el enunciado en la respuesta. Trasladamos el ensamblado del preámbulo y la reconstrucción de las últimas 6 respuestas estrictamente al servidor, y aumentamos el tiempo de espera del evaluador de 30 a 60 segundos.

En el CRM, toda la arquitectura se construye alrededor de un único registro de 334 herramientas (257 expuestas en la fachada agéntica y 77 herramientas del sistema cerradas mediante el flag `facadeExposed: false`). Toda llamada pasa por el pipeline único `invoke()`: la identidad `acting_user` se extrae únicamente de la sesión (distinguiendo los errores `missing` e `invalid` cuando se envía un ID de sesión `api-...` o un ID negativo de chat grupal), y los argumentos se limpian con la expresión regular `SECRET_KEY_RE` antes de escribirse en el `audit_log`.

¿Por qué dividir un único botón de lead en tres estados y cómo detectar reuniones huérfanas en

Zoom?

En ventas reales, el vendedor escribe al cliente en su Telegram personal mientras, en paralelo, el bot calienta al mismo contacto. Por eso hubo que dividir el único botón "Tomar el lead" en tres estados independientes: "Lo tomo" (responsable asignado), "Contactado" (constancia del primer contacto) y "Interceptado" (el bot se calla y cede la conversación a la persona). Para que dos gestores no pisen la misma tarjeta con un doble clic, el CRM localiza el registro más antiguo por `telegram_id` y lo bloquea con `SELECT ...

FOR UPDATE`. Y al crear reuniones en Zoom y Google Calendar, la primera línea siempre es un `INSERT` en la propia base de datos con estado `creating` y los campos `zoom_state` / `gcal_state`, de modo que un fallo en la API externa no deje reuniones huérfanas sin registrar en la nube.

Esta mecánica de simuladores interactivos y registros agénticos funciona dentro de [Hamidun Academy](https://academy.hamidun.com) (con una prueba gratuita de 72 horas) y en el entorno de [Nomad AI](https://nomadnet.ai) / [agent.nomadnet.ai](https://agent.nomadnet.ai), y analizamos la arquitectura de nuestro CRM y LMS en [Vibe Club](https://hamidun.com/club).

Historia

Preguntas frecuentes sobre la noticia

¿Cómo rompió un carácter invisible de retorno de carro \r 138 lecciones, la deduplicación y el OAuth?

El bug más traicionero del LMS no dejaba errores en los registros. Los estudiantes veían el bloque `:::practice` renderizado correctamente, pulsaban "Enviar" y recibían "Bloque práctico no encontrado". Las estadísticas de la base de datos mostraron que en 138 de 146 lecciones hubo exactamente cero intentos exitosos. El contenido se creaba en Windows con saltos de línea `CRLF` (`\r\n`): el frontend normalizaba el texto antes de renderizarlo, pero el analizador del servidor cortaba por `\n`, dejando un `\r` invisible al final de la línea: ```typescript // Sin normalización, e

¿Por qué dividir un único botón de lead en tres estados y cómo detectar reuniones huérfanas en Zoom?

En ventas reales, el vendedor escribe al cliente en su Telegram personal mientras, en paralelo, el bot calienta al mismo contacto. Por eso hubo que dividir el único botón "Tomar el lead" en tres estados independientes: "Lo tomo" (responsable asignado), "Contactado" (constancia del primer contacto) y "Interceptado" (el bot se calla y cede la conversación a la persona). Para que dos gestores no pisen la misma tarjeta con un doble clic, el CRM localiza el registro más antiguo por `telegram_id` y lo bloquea con `SELECT ... FOR UPDATE`. Y al crear reuniones en Zoom y Google Cale

ZK
Zhemal Khamidun — CPO de Alpina Digital, product manager de AlpinaGPT. Crea productos de IA desde 2019. Hamidun News es un proyecto personal que creció hasta ser un agregador en producción con más de 6.000 lectores.
✦ Autonomous AI Execution · Nomad Agent

Delegate Deep Research, Coding & Multi-Step Workflows to Nomad Agent

Deploy autonomous task execution with Nomad Agent (agent.nomadnet.ai) — automated web research, data synthesis, code generation, and MCP tool orchestration.

Launch Nomad Agent →Nomad AI Workspace
¿Qué te parece?
Cargando comentarios…