# Nebula API Plataforma de comunicaciones omnicanal. Integra WhatsApp (Gupshup), Facebook Messenger, Instagram y más en una API unificada con WebSocket en tiempo real. --- ## Changelog ## [2.25.3] - 2026-10-05 ### Corregido - **Una referencia a un dato de la propia pantalla se escribe en la forma que Meta acepta** (solo destino WhatsApp). `{{entrada.PANTALLA.clave}}` escrito dentro de `PANTALLA` salía como `${screen.PANTALLA.data.clave}` y Meta rechazaba el flow con `INVALID_SCREEN_DYNAMIC_DATA`; ahora sale como `${data.clave}` (`${form.clave}` si es una respuesta, forma que no se probó contra Meta). Las referencias a otra pantalla no cambian, ni el documento guardado. - **Una pantalla que no es la inicial y declara datos de entrada que ninguna navegación le envía se informa antes de llamar a Meta** (solo destino WhatsApp). Meta rechaza el flow entero con `INVALID_NAVIGATE_ACTION_PAYLOAD: Following fields are expected in the next screen's data model but missing in payload`, sin decir la pantalla. El aviso nombra la pantalla y las claves y dice qué hacer. No avisa en la pantalla inicial ni en un flow con intercambio con endpoint. Un flow que hoy llega a Meta y falla por esto pasa a fallar antes, con ese mensaje. ## [2.25.2] - 2026-10-02 ### Corregido - **Un `If`, otro `Switch` o un `RichText` dentro de una condición de `Switch` se informa antes de llamar a Meta** (solo destino WhatsApp). Meta los rechaza con `ONE_OF_SCHEMA_VALIDATION_FAILED: Invalid value found for property 'type'`, que señala una posición pero no dice la causa. Un `Switch` dentro de un `If` y un `If` dentro de un `If` siguen pasando. Se rechazan solo los tres tipos probados contra Meta; la rama `default` se revisa igual que las condiciones, pero no se probó por separado. ## [2.25.1] - 2026-10-02 ### Corregido - **Una navegación a una pantalla que no existe ahora se informa antes de traducir.** El traductor la descartaba en silencio: en Nebula Chat el flow se daba por válido, y en una pantalla terminal, dentro de la rama de un `If` o en un enlace incrustado la navegación no se revisaba. La validación del modelo cubre ahora botones del pie, items de `NavigationList` y enlaces incrustados en todos los destinos, con la ruta exacta y el mensaje `navega a una pantalla que no existe: "X"`. La regla propia de WhatsApp se retira para no duplicarla. - **Meta rechazaba un flow completo, sin ubicación, por una clave de `Switch` que empieza con un número** (`INVALID_FLOW_JSON … Cannot read property 'type' of undefined`). En el destino WhatsApp, la validación rechaza ahora antes de llamar a Meta toda clave de caso fuera de `^[a-z][a-z0-9_]*$`, con la ruta y la clave afectada. Los identificadores nuevos de opciones y campos se generan dentro de ese conjunto (con prefijo `i_` si empezarían con un dígito); el slug del nombre del flow y los identificadores ya guardados no cambian. - **Las issues del proveedor conservan el texto técnico y los punteros que devuelve Meta**, y el error de rechazo mantiene el formato `flow json rejected by Meta: …: invalid flow json` que reconoce Sirio. ### Notas de actualización Sin migraciones. Las reglas nuevas corren solo al validar o publicar un borrador: no afectan a flows ya publicados ni al guardado. Un borrador de Nebula Chat con una navegación rota, que antes se validaba, ahora falla. La regla de claves de `Switch` es deliberadamente conservadora: puede frenar claves que Meta tal vez acepte (por ejemplo `Si` o `5`). Un flow con una clave inválida se corrige cambiando a la vez el valor del caso y el identificador de la opción que lo origina. ## [2.25.0] - 2026-09-25 ### Agregado - **La respuesta del asistente de Orion puede llegar en streaming a Nebula Chat.** Con `aiStream: { enabled: true }` en el nodo `ai_agent`, y solo en canales que declaran la capacidad `streaming` (hoy, Nebula Chat), el visitante ve la respuesta a medida que se escribe: el evento efímero `agent_message_stream` (`delta`, `discard` y `end`) no se guarda, y el `agent_message` final lleva el mismo `streamId` para reemplazar al borrador, e `incomplete: true` si el turno se cortó después de mostrar texto. El validador rechaza `aiStream` con un proveedor que no es `orion-ai-studio`. En cualquier otro canal, la respuesta llega completa como hoy. ### Corregido - **El proveedor de IA recibía el último mensaje del visitante dos veces.** El historial incluía el mensaje entrante, ya guardado, y después se le volvía a agregar. Ahora el id del evento entrante llega hasta el motor y se excluye. - **Una carrera en el hub de tiempo real podía tirar el proceso.** Si una conexión se daba de baja mientras se le enviaba un mensaje, el envío iba a un canal ya cerrado (`send on closed channel`). Ahora se envía con el lock de lectura tomado. ### Notas de actualización Sin migraciones. `aiStream` está apagado por defecto: nada cambia hasta encenderlo en un flow, y encenderlo requiere que Orion tenga su streaming publicado. `pendingEventId` pasa a ser una clave reservada del bucket del nodo: un flow que declare un campo con ese nombre no se va a poder volver a publicar. ## [2.24.1] - 2026-09-25 ### Cambiado - **Nebula espera al menos 35 segundos a Orion AI Studio.** Orion corta el turno a los 30 segundos con un `504 Gateway Timeout`. Si Nebula cortara antes, Orion terminaría el turno igual, lo cobraría, y la respuesta no llegaría a nadie. Con `provider: orion-ai-studio` en el nodo `ai_agent`, el motor espera el máximo entre el `timeoutSeconds` del nodo y 35 segundos. Los demás proveedores siguen con 25 segundos de default. ### Notas de actualización Sin migraciones ni cambios en la API. No hace falta tocar ni republicar flows: los nodos de Orion guardados con `timeoutSeconds: 25` (el valor que ponía el editor) esperan 35 segundos igual. El validador sigue aceptando 25 como mínimo configurable con Orion, así que esos flows se pueden republicar sin errores. ## [2.24.0] - 2026-09-22 ### Cambiado - **Una referencia a una pantalla que no existe se rechaza al validar y al publicar.** Antes nadie comprobaba que la pantalla nombrada existiera. Al borrar o renombrar una pantalla, lo que la leía quedaba apuntando a la nada: en WhatsApp fallaba recién al publicar, con un error de Meta que no decía dónde, y en Nebula Chat se resolvía a nulo en silencio, así que una condición caía siempre del mismo lado. Ahora, en el documento de Nebula Flows, esa referencia es un error en los dos destinos. Se revisa en textos (`{{entrada.PANTALLA.clave}}`, `{{respuesta.PANTALLA.campo}}`), en árboles con `"screen"` (condición de un `If`, un `Switch` y opciones traídas de un dato), en el título de cada pantalla y en los textos del mensaje de apertura. Al validar llega como issue con `layer: "model"` y `path` al lugar exacto. Al publicar la respuesta es `400 INVALID_FLOW_JSON`. Que la pantalla exista pero no declare la clave, o no tenga el campo, sigue aceptado: la API no tiene avisos que no bloqueen, y la clave puede llegar inferida por la navegación o por un `setData`. ### Notas de actualización Sin migraciones. No corre al guardar ni al leer un flow ya publicado: esos flows siguen igual. Una referencia rota se rechaza recién la próxima vez que se valide o se publique. ## [2.23.2] - 2026-09-14 ### Corregido - **El cliente veía `HANDOFF:` o `END_CONVERSATION` crudos cuando el asistente los ponía al final.** Hay modelos que escriben la frase de cortesía arriba del marcador y no abajo: deepseek-v4-flash-0731 lo hizo en 12 de 108 respuestas del harness de conversaciones, y la conversación no se derivaba ni se cerraba. Ahora los dos marcadores también cuentan como último renglón. Con `HANDOFF:` al final, el texto de arriba sale antes de derivar; con `END_CONVERSATION` al final no sale, porque la despedida ya la dice el nodo `end`. Sobre las mismas respuestas del asistente de Mintaka Propiedades con ese modelo: marcadores visibles de 7 a 0, derivación de 7/12 a 11/12 y cierre de 0/3 a 3/3. ### Notas de actualización Sin migraciones ni cambios en la API. Un marcador en un renglón del medio, o citado dentro de una frase, sigue sin disparar nada. ## [2.23.1] - 2026-09-13 ### Corregido - **El asistente no usaba la regla de los botones aunque le llegaba.** Los comportamientos escritos para Nebula insisten en que `HANDOFF:` y `END_CONVERSATION` van solos en el mensaje, y el modelo lo generalizaba al renglón de `OPTIONS:`, que va debajo de la pregunta. La regla ahora lo aclara: a diferencia de esos dos, va debajo de la pregunta en el mismo mensaje y nunca sin la pregunta escrita arriba. Medido contra el asistente de Mintaka Propiedades en producción, preguntando el tipo de propiedad: botones 2 de 8 veces antes, 5 de 8 después, sin cambios en la derivación ni en el cierre. - **El cliente veía `OPTIONS:` crudo cuando el asistente lo escribía en el mismo renglón que la pregunta.** `¿Te contacto? OPTIONS: Sí | No` ahora sale como botones, con cualquier espacio Unicode entre el signo y el marcador. Pegado al signo, o citado dentro de una pregunta, sigue sin ser marcador. ### Añadido - **Harness de conversaciones del nodo de IA**, como test que solo compila con `-tags harness`: devuelve las reglas y el saludo que el engine mandaría para unas props del nodo, y qué le llega al cliente de cada respuesta del asistente. Lo orquesta `backend/tests/harness_canal/` del repo de Orion. No lo corre la suite. ### Notas de actualización Sin migraciones. La regla de las opciones suma 188 caracteres al bloque que Orion acota en 2.000; el flow de Mintaka, el único con `aiSend` en producción, queda en 1.668. ## [2.23.0] - 2026-09-13 ### Cambiado - **Con `aiSend` el asistente también arma sus propios botones y listas.** Un asistente que puede mandar contenido del canal tiene que poder proponer opciones de respuesta, y hasta ahora eso pedía `aiOptions`, una segunda prop que el editor no ofrece: con `aiSend` solo, la regla de `OPTIONS:` no viajaba y el renglón, si el asistente lo escribía igual, salía enumerado en el texto. Ahora `aiSend.enabled` suma la regla de las opciones y el engine las dibuja como botones o como lista según el canal. Sin techo declarado, con `aiSend` el techo es el de una lista (10) y no el default de `aiOptions` (5): una pregunta de siete respuestas cerradas, como el tipo de propiedad, sale como lista y no enumerada. Un `aiOptions.maxOptions` declarado con la prop encendida sigue mandando. Los templates y los flows no se arman: los canales los aprueban antes, así que siguen saliendo del catálogo. ### Notas de actualización Sin migraciones. Un flow activo con `aiSend` empieza a mandar la regla de las opciones en el turno siguiente, y el validador la cuenta en el tope de 2.000 caracteres de Orion. En producción solo el flow de Mintaka Propiedades tiene `aiSend`, sin prompt propio, y queda en unos 1.500. Sirio 0.35.1 actualiza el texto del control en el editor. ## [2.22.2] - 2026-09-13 ### Corregido - **Un template con encabezado multimedia salía sin su archivo cuando el envío no lo mandaba.** El archivo del encabezado no viaja con la aprobación: hay que ponerlo en cada envío. Pero quien manda no siempre puede — el nodo `template` del chatbot solo expresa los valores `{{N}}` del cuerpo, y una integración que manda `parameters: ["Ana"]` tampoco tiene dónde ponerlo. Sin el componente `header`, **Meta descarta el mensaje y el proveedor no devuelve error**: el POST responde 2xx con su propio id, nunca llega el status de WhatsApp y el mensaje no aparece del otro lado. Para el chatbot el envío había salido bien, así que seguía al nodo siguiente. Ahora el encabezado cae al archivo con el que se aprobó el template, que es el que el proveedor ya tiene guardado y lo mismo que hace cualquier despachador que lea el `containerMeta`. El archivo del envío le sigue ganando: el agente que adjunta otra imagen desde el compositor manda la suya. Y el encabezado se arma aunque el envío no traiga ningún parámetro — antes se cortaba antes de llegar, así que un template con imagen y sin variables en el cuerpo, que es el caso más común, no lo evaluaba nunca. Vale para `IMAGE`, `VIDEO` y `DOCUMENT`. Un template multimedia sin archivo guardado sigue saliendo sin encabezado: no hay nada que mandar. ### Notas de actualización Sin migraciones. Completa la 2.22.1, que hizo que el archivo quedara guardado; esta lo usa al enviar. Los templates multimedia dados de alta desde Nebula antes de la 2.22.1 siguen sin archivo y hay que volver a darlos de alta; los cargados desde el panel de Gupshup lo recuperan en el próximo import del canal. ## [2.22.1] - 2026-09-12 ### Corregido - **Un template con encabezado multimedia llegaba sin su archivo.** El archivo no viaja con la aprobación: quien despacha el mensaje tiene que ponerlo en cada envío, y lo saca del `containerMeta` que Gupshup guarda del template —`mediaUrl` primero, `mediaId` después—. Los templates cargados desde el panel de Gupshup tienen ese campo; los que dábamos de alta desde Nebula salían solo con `exampleMedia`, que es el handle con el que Meta revisa: se consume en la revisión y vence a los pocos días. Sin ninguno de los dos el template se manda sin encabezado y **el mensaje llega igual, sin la imagen y sin error** — por eso no había nada que mirar en los logs. El alta y la edición mandan ahora también `mediaUrl`, con la URL del archivo en nuestro storage: la misma que sube el editor, pública y permanente. No figura en la documentación de la Partner API, pero Gupshup lo acepta, lo guarda en el `containerMeta` y lo devuelve al listar los templates, que es de donde lo lee quien despacha. Vale para los tres formatos: `IMAGE`, `VIDEO` y `DOCUMENT`. - **El import borraba el archivo del encabezado.** `parseContainerMeta` reconstruía el encabezado multimedia sin ejemplo —descartaba el `mediaUrl` que sí traen los templates cargados desde el panel— y el upsert reemplaza la definición local por esa, así que cada sincronización del canal dejaba sin archivo a todos sus templates multimedia. Ahora esa URL se recupera, y si el proveedor no devuelve ninguna se conserva la que la fila ya tenía. Lo que manda el proveedor gana; solo se rellena el hueco. ### Notas de actualización Sin migraciones. Los templates multimedia dados de alta desde Nebula antes de esta versión **no se arreglan solos**: en Gupshup no tienen `mediaUrl` y de la copia local ya se había borrado el archivo, así que hay que volver a darlos de alta con su imagen — duplicando y publicando, porque un template aprobado no se puede editar—. Los cargados desde el panel de Gupshup recuperan su archivo en el próximo import del canal. ## [2.22.0] - 2026-09-11 ### Cambiado - **Un flow que el cliente no podría abrir ya no se manda**: `POST /outgoing/send` responde **`422 FLOW_NOT_PUBLISHED`**, no escribe el evento y no llama al proveedor. Hasta la 2.21.0 salía sin error y el visitante de Nebula Chat, al tocarlo, recibía un `404 FLOW_NOT_FOUND` de `GET /nebula-chat/flows/{flowId}`: el mensaje quedaba en la conversación como un formulario que no existe. **Qué cuenta como publicado depende de quién ejecuta el flow del otro lado.** En Nebula Chat se puede mandar exactamente lo que después sirve la API —un flow `published` de ese canal—, así que la comprobación comparte la regla con `DocumentForVisitor`. En WhatsApp vale la de Meta, que además de `published` deja mandar un flow `throttled` con un tope de diez mensajes por hora; y **un `flowId` que Nebula no tiene registrado en ese canal pasa**, porque Meta puede conocer flows armados fuera de Nebula y ahí la autoridad es el proveedor, igual que con la ventana de 24 horas. El mismo rechazo vale para `POST /threads` con un flow como primer mensaje y para el nodo `flow` del chatbot, que en vez de mandarlo rutea por `out-fallback`. El cliente tiene que ofrecer solo flows con `status: published` y leer este código como «ese flow ya no se puede mandar», no como una falla del envío: le toca cuando el flow se deprecó entre que se listó y que se mandó. ### Corregido - **Una lista mandada desde el catálogo le llegaba al cliente sin su pregunta.** El cuerpo de un `button_list` puede estar guardado como `body` —el nombre que usa el nodo del chatbot, y con el que se armaron snippets por la API— o como `text`, que es el campo que leen el render y los senders de WhatsApp y de Meta. Mandado tal cual, el cliente recibía el botón solo, sin la pregunta que le da sentido. La regla que ya aplicaba el motor del chatbot —si hay `body` con algo escrito, esa es la pregunta; si no, `text`— pasó a `snippet.Normalize`, y por ahí pasan tanto el envío desde el compositor como el motor a través de su adapter: **el mismo snippet dice lo mismo lo mande el bot o una persona**. El snippet guardado no se toca. ## [2.21.0] - 2026-09-11 ### Añadido - **El asistente del chatbot propone botones y listas.** El nodo `ai_agent` mandaba la respuesta del modelo como texto y nada más, así que una pregunta de opción cerrada obligaba a tipear la respuesta. Con `aiOptions` el asistente declara las opciones en un renglón —`OPTIONS: Departamento | Casa | PH`— y el motor decide cómo salen según el canal: hasta 3, botones; de 4 a 10, una lista. El modelo no elige el tipo de mensaje porque no sabe por qué canal habla. - **El asistente manda templates, flows y snippets del canal nombrándolos.** Con `aiSend` escribe `TEMPLATE: visita_confirmada | Av. Santa Fe 1234 | jueves`, `FLOW: agendar_visita` o `SNIPPET: nombre | valor`, y el motor lo arma igual que los nodos `template` y `flow`. Se busca por el nombre único en el canal y se valida en cada envío; lo que no cierra deja el texto solo. El marcador no le llega nunca a la persona: se saca del texto aunque la prop esté apagada. - **El asistente consulta el catálogo de su canal con una credencial propia.** `GET /ai-catalog` le da a la herramienta de Orion los templates, flows y snippets que puede mandar, cada uno con el renglón exacto que tiene que escribir. La credencial es una por canal y solo lee: se genera con `POST /channels/{channelId}/ai-catalog-token`, se revoca con `DELETE` y se guarda solo el hash. No es el `X-Channel-Token`, que manda mensajes en nombre del cliente. Trae la **migración 091** (`ai_catalog_tokens`). ### Corregido - **Un flow con dos caminos hacia la misma pantalla lo rechazaba Meta.** Cuando dos caminos con campos distintos llegan a la misma pantalla, esa pantalla declara la unión y cada Footer mandaba solo lo suyo, y Meta exige que cada navegación mande todo lo que el destino declara (`INVALID_NAVIGATE_ACTION_PAYLOAD`). Ahora cada Footer completa con un vacío del tipo declarado —`""`, `[]` o `false`— lo que no es suyo, y si al destino llega además un ítem de lista navegable, que no lleva payload, no se declara nada. Se verificó contra Meta antes del release. - **Tres rechazos de Meta llegaban solo al publicar**, sobre flows que la API daba por buenos: un id de pantalla con números (`PATTERN_MISMATCH`, que la referencia de Meta no documenta), dos RichText en la misma pantalla aunque solo los acompañe un Footer, y una condición que compara una lista —un ChipsSelector o un CheckboxGroup devuelven una lista aunque dejen elegir una sola opción—. Ahora los frenan las reglas de WhatsApp con mensaje y ruta al componente, y `ValidateFlowJSON` suma las dos primeras para el Flow JSON crudo. Nada de esto corre en un canal de Nebula Chat. ### Cambiado - **El tope de 1024 caracteres del cuerpo de un interactivo sale de las capacidades del canal.** Gupshup lo declara para `quick_reply`, `button_list` y `flow`; en Nebula Chat, que no tiene tope, una respuesta larga del asistente conserva el botón de handoff. - **Un `flow_reply` que llega a un nodo `ai_agent` es un turno del asistente**, con las respuestas como texto y los campos en el bucket. Antes salía por `out-fallback`. - **El historial que recibe el modelo incluye templates, flows y respuestas de formularios**, también los que mandaron otros nodos. ## [2.20.2] - 2026-09-10 ### Corregido - **Un template aprobado volvía a «borrador» después de un import.** Meta manda estados que no responden «se puede enviar o no» —`ARCHIVED` cuando un template lleva mucho sin usarse, `FLAGGED` y `APPEAL_REQUESTED` alrededor de una apelación— y `mapGupshupStatus` los mandaba a `draft` por su rama `default`. Un template aprobado en mayo aparecía como borrador que nunca se envió, y el autor no tenía cómo saber que en realidad estaba archivado. El criterio de no tocar lo que no se entiende ya lo aplicaba `statusFromWhatsApp` para el mismo problema por la vía del webhook. Tenerlo en un lado y no en el otro hacía que **el mismo template terminara distinto según por dónde llegara la noticia**: si Meta avisaba por webhook no pasaba nada, y si alguien sincronizaba, se degradaba. Ahora los dos caminos se comportan igual y el valor crudo sigue guardándose en `provider_status`. La corrección anterior sumó `PAUSED` e `IN_APPEAL` a la lista de aprobados; esta ataca la causa en vez de ir agregando estados de a uno cada vez que aparece el siguiente. - **Editar un chatbot activo no dejaba registro de lo publicado.** `Activate` cortaba en la primera línea si el flow ya estaba activo, así que un `PUT` de la definición se aplicaba y el motor lo servía —lee `chatbot_flows.definition` directo— pero `chatbot_flow_versions` se quedaba con la foto anterior. La tabla se describe como registro inmutable de lo publicado y se salteaba todos los cambios en caliente: **un rollback restauraba un grafo que ya no era el que estaba corriendo**, con nodos apuntando a flows o templates que se hubieran reemplazado desde entonces. Ahora, sobre un flow activo, se valida y se congela **solo si la definición o el trigger cambiaron** respecto de la última versión. Sin cambios sigue siendo un no-op, que es lo que mantiene la operación idempotente. La comparación es por contenido y no por bytes: el mismo documento vuelve de la base con las claves en otro orden. ## [2.20.1] - 2026-09-10 ### Corregido - **La bandeja no podía dibujar lo que ella misma había mandado.** Un interactivo enviado por referencia —`snippetId` o `templateId`, que es la forma documentada— se guardaba con la referencia y nada más. El mensaje salía bien, pero al releer el hilo `render` se armaba desde ese contenido: un `quick_reply` sin `text` ni `options` daba `{"version": 1}` y un `template` sin `components` daba `null`. Quien lo había mandado veía una burbuja vacía, y caer a `content` no ayudaba porque ahí estaba la misma referencia. Ahora la referencia **se expande antes de persistir**, que es lo que ya hacían `comment` y `reaction` dos pasos antes en la misma función. Lo que se guarda es contenido de Nebula y no el payload del proveedor: el template conserva sus `components` y sus `parameters` por separado, y la traducción a Meta la sigue haciendo el sender de Gupshup. El `contentType` del evento sale de la misma resolución: un snippet declara si es `quick_reply` o `button_list`, y gana el del snippet por sobre el que declare quien envía. Guardar el declarado dejaba el evento etiquetado distinto de lo que se mandó, y `render` se despacha por ese tipo. De yapa, una referencia que no resuelve —snippet inactivo, template que no se puede enviar— ahora falla **antes** de escribir el evento. Antes dejaba una fila `pending` de un mensaje que no se iba a enviar nunca. ## [2.20.0] - 2026-09-10 ### Añadido - **El visitante sabe a qué sucursal le escribe.** `GET /public/company` devuelve `channel.name` cuando se entró por la clave publicable. Un tenant puede tener varios canales —la misma inmobiliaria con una sucursal, una región o un país distinto— y con solo el nombre de la empresa dos canales se ven idénticos. Por la puerta del `X-Tenant-Id` el campo **se omite**: no hay ningún canal que nombrar, y mandarlo vacío se leería como «un canal sin nombre». Del canal sale el nombre y nada más: `settings` lleva la clave publicable y las credenciales del proveedor adentro. - **La apertura de sesión acepta tres pistas nuevas, y no hacen lo mismo.** `POST /nebula-chat/sessions` toma `code`, `email` y `phone`, los tres opcionales. - **`code` ata la sesión a ese contacto.** Lo emitió el tenant y se lo dio a esa persona: es la misma clase de credencial que el `resumeToken` —establece identidad conversacional y **no autoriza operaciones de negocio**—. Son ocho caracteres alfanuméricos, únicos por tenant, sin `I`, `O`, `0` ni `1` porque el código se dicta por teléfono y se copia a mano. **Un código que no existe no falla y no se avisa**: responder «ese código no existe» convertiría la apertura en un oráculo para averiguar cuáles sí existen. - **`email` y `phone` no identifican a nadie.** Son datos sabidos —cualquiera puede escribir los de otro—: quedan anotados en el contacto del visitante y de ahí el matcher que ya existía propone una **fusión que confirma una persona del tenant**. **No son una elección**: son campos independientes, los dos pueden viajar en la misma apertura, y ninguno pisa lo que el contacto ya tenía. Cuando llega más de una pista, el orden es `bindingToken`, después `code`, después `resumeToken`. El código va por encima del token guardado porque ese navegador puede ser de otra persona que usó la misma computadora. - **Los contactos nuevos nacen con su código** (migración 090: la columna y su índice único por tenant). Se genera con `crypto/rand` —es una credencial— y un choque se reintenta: que crear un contacto falle por un código repetido sería cambiar lo principal por lo accesorio. **Los contactos anteriores quedan sin código a propósito**; alcanza con que se puedan generar. ### Corregido - El correo de la apertura se recorta antes de anotarse. Un valor de puros espacios terminaba escrito en la ficha que mira quien atiende. ## [2.19.0] - 2026-09-10 ### Añadido - **`GET /public/company`: la identidad de la empresa antes de tener sesión.** Devuelve el nombre, el logo y el contacto del tenant para las pantallas anteriores al login. Acepta dos formas de nombrarlo, porque **los dos que preguntan saben cosas distintas**: `X-Publishable-Key` para quien entra a conversar, que tiene la clave del canal y no sabe de qué tenant es, y `X-Tenant-Id` para la aplicación del vendedor, que tiene su tenant fijo y ninguna clave de canal. Si vinieran las dos gana la clave publicable: identifica un canal que existe, mientras que un tenant id suelto es lo que quien pregunta escribió. **Es un endpoint propio y no un campo más de la sesión** porque la sesión llega tarde: se abre cuando la persona manda su primer mensaje, y la pantalla que necesita el nombre y la marca es anterior. Adelantarla acuñaría una identidad y gastaría cupo por cada visita que mira y no escribe. **Devuelve una lista blanca y nunca el objeto `Tenant`.** El camino es público —la clave viaja dentro del binario de una app y en el JavaScript de una página—, así que `modules`, `status`, `ownerId`, `createdBy` y `timezone` no salen por acá; hay un test que falla si alguien le agrega un campo al tipo. Los cupos son 600 por IP y 6000 por tenant a la hora, más holgados que los de sesión porque se consulta una vez por apertura de la aplicación. **La ruta vive fuera de `/nebula-chat/`** porque la usan dos productos: un endpoint colgado del namespace de uno de ellos manda a buscarlo donde no está. - **El tamaño de la imagen de un flow se elige por forma** —1:1, 4:3, 3:2, 16:9, 9:16 u original— y ya no en píxeles. Meta acepta `aspect-ratio` y resuelve el alto sola, que además es lo único que se adapta al ancho de la pantalla y a si la pantalla va a una o dos columnas. ### Corregido - **Las imágenes de un flow viajan a Meta en base64.** Meta no resuelve URLs: el `src` de un componente `Image` que no fuera base64 crudo **se publicaba sin error y llegaba al teléfono en blanco**, sin aviso en ninguna capa. Ahora la API lo convierte al validar o al publicar, y **sobre una copia**: `ToMeta` sigue sin tocar la red y el `flow_json` guardado conserva la URL con la que se sigue editando. Se rechaza con la ruta al componente lo que Meta no dibuja —un formato que no sea JPEG o PNG, algo que pese más de 300 KB, o una dirección que devuelva HTML en vez de un archivo, que es el caso del enlace a un visor—, y el formato se mira en el contenido y no en el header, que puede venir vacío o mentir. **No confundir el tope de 300 KB con los 15360 KB de Fotos y Documentos**, que son el límite de lo que sube el contacto desde su teléfono. ### Mantenimiento - El slice del test de campos nace con su capacidad, que es lo que pedía `prealloc`. ## [2.18.0] - 2026-09-10 ### Añadido - **El visitante de Nebula Chat sabe quién le responde, y si es una persona.** Cada `agent_message` viaja con `agentName` y `agentType` (`human`, `chatbot`, `ai_agent`, `system`). **Los dos van juntos y el nombre solo no sirve**: el chatbot manda su rótulo por ese mismo campo, así que un cliente que dibuje el nombre sin mirar el tipo termina diciendo «te atiende Cierre automático». - **La foto de quien responde viaja con cada mensaje**, en `agentAvatarUrl`. Resuelta por mensaje y no una vez por sesión, para que el historial conserve con qué avatar respondió cada uno; el costo lo paga una cache en proceso con TTL de cinco minutos. - **La sesión del visitante dice cómo salir del bot.** `POST /nebula-chat/sessions` devuelve `humanHandoffKeywords`: las palabras que, escritas por el visitante, pasan la conversación a una persona. Son configurables por canal, así que una interfaz que las escriba a mano miente el día que un tenant las cambie. - **`agent_changed`**: el visitante se entera cuando su conversación cambia de manos. Sin esto la interfaz seguiría diciendo quién atendía antes, que es peor que no mostrar ninguno porque no se nota. - **`thread_closed`**: el visitante se entera de que su conversación terminó. Lo importante no es que terminó, es que **la siguiente empieza de cero**: el índice del upsert que resuelve el thread lleva `WHERE status <> 'closed'`, así que su próximo mensaje crea un thread nuevo y del lado del agente aparece una conversación sin contexto. - **`message_ack`**: acuse de recibo de lo que manda el visitante, con el `eventId` que él mismo mandó. Solo se emite si vino ese id, con el mismo criterio que el `message_accepted` de los agentes. Confirma que se persistió, **no que alguien lo leyó**. - **`agent_typing`**: el transporte del indicador de escritura, con `expiresAt` y sin un `typing_stopped` —un evento de «dejó de escribir» que se pierde deja los tres puntitos para siempre—. **Todavía no lo emite ningún cliente**: lo tienen que mandar las aplicaciones de quien atiende. - **PowerPoint entra a la lista blanca de subidas del visitante** (`application/vnd.ms-powerpoint` y el tipo de `.pptx`). Al lado de Word y Excel, la ausencia era un olvido y no una política. ### Corregido - El conversor a Meta emite `refresh_on_back` solo si el documento es dinámico. ### Mantenimiento - El módulo de Go pasa a llamarse `altair` con la mudanza al monorepo. ## [2.17.3] - 2026-09-09 ### Documentación - *(flows)*: El badge desde un dato queda verificado en un telefono real - *(planes)*: Tres pendientes mas en el cuaderno, cada uno con su verificacion - *(flows)*: El reporte de validacion documenta detail y sobre que documento corre ### Nuevas funcionalidades - *(me)*: El avatar y el tope de conversaciones viajan en GET /me - *(sales)*: El listado de actividades acepta un rango de vencimiento - *(contacts)*: El contacto queda de quien atiende, y dice de quien es ## [2.17.2] - 2026-09-07 ### Correcciones - *(flows)*: El mensaje del tope de archivo citaba el limite viejo ### Documentación - *(flows)*: Registra la medicion de reglas faltantes como pendiente a analizar ### Mantenimiento - *(flows)*: Descarta la fase 4 del plan y elimina su verificador ## [2.17.1] - 2026-09-06 ### Correcciones - *(errors)*: Separa el codigo del asignatario y corrige el 401 en la referencia ## [2.17.0] - 2026-09-06 ### Nuevas funcionalidades - *(flows)*: La traduccion pasa por el validador de Flow JSON antes de subirse ## [2.16.4] - 2026-09-06 ### Correcciones - *(flows)*: Una rama then vacia y una pantalla sin titulo dejan de llegar a Meta ### Documentación - *(flows)*: El plan registra lo verificado en produccion y dos hallazgos nuevos - *(api)*: La tabla de rotulos y el veredicto sobre content.title - *(proceso)*: El mensaje del tag de release necesita --cleanup=verbatim ## [2.16.3] - 2026-09-06 ### Correcciones - *(channels)*: Capabilities declara los tipos con el nombre del evento - *(realtime)*: El rechazo de un send_message devuelve la clave de idempotencia ### Documentación - *(api)*: La subida de un adjunto no es idempotente, y los contadores de cartera vienen siempre ## [2.16.2] - 2026-09-05 ### Correcciones - *(flows)*: Una pantalla sin salida deja de pasar, y el tope de archivo es el de Meta - *(flows)*: Un tope de caracteres no mide un valor que viene de un dato ### Documentación - *(flows)*: Plan de alineacion de validaciones y renombre del plan de lista navegable - *(api)*: Las reglas que cumple cualquier cliente de esta API ### Mantenimiento - *(flows)*: Verificador de los fixtures compartidos con nebula_app ## [2.16.1] - 2026-09-05 ### Nuevas funcionalidades - *(sales)*: El resumen de cartera dice desde cuando espera el mas viejo - *(threads)*: El listado trae el ultimo mensaje de cada conversacion - *(activity)*: Las stats por agente se acotan por permiso y agrupan por dia ### Documentación - *(api)*: Render en el modelo de evento, y el flow y el template dicen la verdad ## [2.16.0] - 2026-09-04 ### Correcciones - *(nebula-chat)*: Las variables de un template se aplican antes de mandarlo ### Nuevas funcionalidades - *(templates)*: Filtrar el listado por sendable y por review - *(render)*: Una sola forma para dibujar un mensaje, igual en todos los canales ## [2.15.0] - 2026-09-04 ### Correcciones - *(chatbot)*: El validador pregunta si el template se puede enviar ### Nuevas funcionalidades - *(templates)*: Sendable y review, en vez de un status que respondia dos cosas ### Refactorizaciones - *(templates)*: Las reglas de WhatsApp dejan de ser las del modelo - *(templates)*: El modelo tiene reglas propias y valen en todos los canales ## [2.14.1] - 2026-09-04 ### Correcciones - *(realtime)*: La desconexion y el arranque dejan de cerrar sesiones vivas - *(routing)*: La carga del agente se cuenta en el tenant que se rutea ### Refactorizaciones - *(activity)*: Retira el listado de sesiones activas, que no usaba nadie ## [2.14.0] - 2026-09-04 ### Correcciones - *(realtime)*: ActiveThreads del agente sale del estado y no del historico - *(activity)*: El tiempo por agente mide cobertura y no suma de duraciones ## [2.13.2] - 2026-09-04 ### Correcciones - *(presence)*: El heartbeat y el estado propio no dependen de contact_center ## [2.13.1] - 2026-09-04 ### Correcciones - *(tenant)*: El chequeo de nil va antes de desreferenciar el acceso ### Nuevas funcionalidades - *(channels)*: El canal dice cuantos agentes tiene asignados ## [2.13.0] - 2026-09-04 ### Documentación - *(sla)*: La escalacion delega en el ruteo por diseño, y son cinco campos ### Refactorizaciones - *(sla)*: Una sola escalacion de reasignacion, sin campos de destino (**BREAKING**) ## [2.12.0] - 2026-09-04 ### Correcciones - *(users)*: Operar sobre otra persona sin tenant activo da 400 y no 500 ### Documentación - *(routing,sla)*: El contrato dice contra que resuelve el rol, y que dos campos no hacen nada ### Nuevas funcionalidades - *(users)*: La respuesta dice que rol tiene la persona en cada tenant ## [2.11.0] - 2026-09-03 ### Documentación - *(planes)*: Como recrear dev sin que se vuelva a borrar - *(users)*: El alta ya no pide role, y vacio crea un usuario comun ### Nuevas funcionalidades - *(users)*: Los filtros del listado rechazan lo que no existe - *(tenant)*: El dueño tiene que estar activo y no puede ser superadmin ### Refactorizaciones - *(auth)*: El rol global es uno solo, y el alta de usuario deja de pedirlo ## [2.10.5] - 2026-09-03 ### Documentación - *(roles)*: El contrato dice los roles que quedaron, no los que se retiraron ## [2.10.4] - 2026-09-03 ### Correcciones - *(templates)*: El aviso de aprobacion de Meta llegaba en un formato que se descartaba ## [2.10.3] - 2026-09-02 ### Nuevas funcionalidades - *(messaging)*: Abrir una conversacion con un contacto que nunca escribio ## [2.10.2] - 2026-09-02 ### Correcciones - *(logs)*: Guardar el mensaje del error en el visor ## [2.10.1] - 2026-09-02 ### Correcciones - *(templates)*: Mandar siempre example a Gupshup - *(templates)*: Emparejar cada ejemplo con su numero de variable ### Documentación - *(planes)*: Las seis fases de modulos y roles, en produccion - *(planes)*: Cabeceras al dia con lo que ya esta en produccion - *(api)*: La ventana vencida en Facebook e Instagram no tiene reemplazo ## [2.10.0] - 2026-09-02 ### Nuevas funcionalidades - *(auth)*: Se retira el modelo viejo de roles. `users.role` queda en `superadmin` o `user` —lo unico global que sobrevive— y lo que cada persona puede hacer en un tenant sale de su membresia. El token deja de emitir `tenant_ids`, que la claim `tenants` reemplaza con el rol de cada uno; la claim vieja se sigue leyendo para las sesiones abiertas antes del despliegue. Migracion 087, reversible desde `users.role_legacy` - *(auth)*: El alta de usuarios acepta el rol de la membresia (`tenants[].roleId`) y toma `user` por defecto - *(routing)*: El ruteo filtra candidatos por el rol de la membresia y no por el rol global. La cascada de la politica por defecto —agentes, luego supervisores, luego owners— sigue funcionando igual; el dueño resuelve a `owner` aunque su membresia lleve el rol de sistema `admin` - *(tenant)*: Crear un tenant ya no exige que el dueño tenga `role = owner`: ser dueño es figurar en `tenants.owner_id` y nada mas ### Correcciones - *(tenant)*: La membresia nace con rol y no lo pierde al editarla. Editar los tenants de un usuario borraba el rol de todos los que ya tenia, y lo dejaba sin un solo permiso; ahora se conserva el de las membresias que se mantienen ### Documentación - *(planes)*: V2.9.0 en produccion, con lo que el release enseño ## [2.9.0] - 2026-09-02 ### Correcciones - *(nebula-chat)*: El alta sin settings y el entorno local - *(nebula-chat)*: La API no arma la direccion del link dirigido - *(nebula-chat)*: El token de continuidad dura 3 anios, no 1 - *(contacts)*: La identidad guarda por que canal entro la persona - *(contacts)*: El contacto nuevo nace con su identidad como preferida - *(flows)*: El nodo del chatbot lee las declaraciones de un flow propio - *(push)*: Registrar las rutas de dispositivos en el arranque ### Documentación - *(nebula-chat)*: Plan para cerrar el canal y que reciba mensajes - *(nebula-chat)*: El canal deja de disenarse como web chat - *(nebula-chat)*: Sin verificacion de origen, y el rate limit pasa a obligatorio - *(nebula-chat)*: La politica de origen vive en el canal, no en la URL - *(nebula-chat)*: Sin identificador corto y sin creacion tardia del contacto - *(nebula-chat)*: Fase 3b, la identidad de la persona - *(nebula-chat)*: La identidad es por persona y por tenant, no por canal - *(nebula-chat)*: Cerrar la documentacion del canal - *(nebula-chat)*: El registro del plan suma el hueco del nodo del chatbot - *(nebula-api-docs)*: El contrato de la clave de idempotencia - *(planes)*: El plan macro del producto y los planes de idempotencia y chatbot - *(planes)*: El estado de la idempotencia pasa a "en revision" - *(planes)*: La idempotencia de envio queda mergeada a dev - *(planes)*: RF-13 suma la recoleccion del nodo ai_agent - *(chatbot)*: El contrato de los datos del bot para el vendedor - *(planes)*: RF-13 queda ejecutado, con lo que se decidio al implementarlo - *(planes)*: El plan macro registra el nucleo comercial ya construido - *(planes)*: El plan macro reparte cada requerimiento entre API y app - *(planes)*: El filtro de cartera es scope mas assignedUserId, no un scope solo - *(planes)*: RF-07 queda listo en el plan macro - *(planes)*: La vista de conjunto del programa queda versionada - *(planes)*: La atribucion de origen queda cerrada - *(planes)*: Pedido de filtrar la cartera por los cuatro criterios - *(planes)*: Sumar el pedido del contacto resuelto en GET /deals - *(sales)*: El push cerrado en el plan macro y en el artefacto - *(sales)*: El push mergeado a dev - *(sales)*: La API publica pasa a futurible - *(planes)*: Las lentes de cartera pasan de pedido a plan ejecutable - *(sales)*: El contrato de las lentes cerrado en el plan macro y el artefacto - *(planes)*: El PR de la fase 4 en el indice del plan macro - *(planes)*: Modulos por tenant y roles con permisos - *(planes)*: Lo que falta de offline, y la renovacion de tokens de canal - *(planes)*: El perfil de Facebook, y los errores que el log se traga - *(estado)*: Traer la version viva del artefacto y sumarle lo de Facebook - Modulos por tenant y roles con permisos - *(planes)*: El artefacto refleja los modulos y los roles - *(planes)*: Modulos y roles quedan cerrados en dev - *(planes)*: La fase 6 de modulos y roles toma la 087, no la 085 ### Nuevas funcionalidades - *(channels)*: El provider nebula se puede dar de alta y resolver - *(realtime)*: El mensaje del visitante entra al pipeline de entrantes - *(realtime)*: El visitante de Nebula Chat se autoriza contra el canal - *(realtime)*: Apertura de sesion de Nebula Chat con clave publicable - *(realtime)*: El visitante que vuelve es el mismo, no uno nuevo - *(contacts)*: Revocacion de la credencial del visitante y telefono en el alta - *(realtime)*: Link dirigido para atar una sesion a un contacto - *(realtime)*: Adjuntos del visitante de Nebula Chat - *(nebula-chat)*: Interactivos y templates - *(flows)*: Nebula Chat publica flows sin traducirlos a Meta - *(contacts)*: Identidad preferida del contacto, elegida al fusionar - *(contacts)*: Cambiar la identidad preferida desde el update - *(contacts)*: La lista de sugerencias trae el nombre de los dos contactos - *(outgoing)*: Una clave del cliente evita que un reintento duplique el mensaje - *(realtime)*: Send_message acepta la clave de idempotencia y la acusa - *(threads)*: La conversacion expone cuando se cierra la ventana de 24 horas - *(threads)*: La conversacion dice por que proveedor entro - *(chatbot)*: El nodo ai_agent extrae los datos que declara - *(chatbot)*: Los datos del bot llegan al agente que toma el thread - *(contacts)*: El listado filtra por responsable y por bandeja compartida - *(contacts)*: Reasignacion masiva de contactos a un vendedor - *(sales)*: La oportunidad comercial y el catalogo de estados - *(sales)*: El catalogo de estados se administra desde la API - *(sales)*: Las actividades, la proxima accion sobre un cliente - *(sales)*: Los contadores de la cartera del vendedor - *(contacts)*: Las etiquetas del contacto, con su catalogo por tenant - *(customfields)*: El catalogo de campos configurables por tenant - *(customfields)*: Los valores en el contacto y en la oportunidad - *(sales)*: La atribucion de origen y el catalogo de motivos de perdida - *(push)*: El registro de dispositivos para avisos moviles - *(push)*: La entrega a Firebase y a Apple - *(push)*: Los avisos de mensaje nuevo y de asignacion - *(tenant)*: Cada tenant declara su zona horaria - *(push)*: Los avisos que nadie dispara - *(contacts)*: La lente de la cartera, con `new` como primera - *(sales)*: Las cuatro lentes, y el contador comparte su SQL con la lista - *(sales)*: La oportunidad trae el contacto resuelto - *(offline)*: Version con ETag, secuencia del cliente y renovacion de credenciales - *(tenant)*: Cada tenant declara que modulos tiene habilitados - *(auth)*: El rol pasa a ser de la membresia, con permisos - *(role)*: Roles personalizados por tenant - *(routing)*: La herencia de contact center se apaga por no tener el modulo ### Refactorizaciones - *(nebula-chat)*: Se llama vinculacion, no link - *(auth)*: La jerarquia de usuarios pasa a compararse por permisos - *(tenant)*: El alcance sobre un tenant sale de los permisos - *(sales)*: El alcance de la cartera sale de un permiso, no del rol - Los ochenta y cuatro sitios deciden por permiso, no por rol ### Testing - *(nebula-chat)*: Cobertura del canal y contrato con el motor del chatbot - *(outgoing)*: Cobertura de la idempotencia de envio y del lock - *(contacts)*: El simulador de tres proveedores crea los canales que usa - *(sales)*: Cierra la cobertura del modulo comercial ## [2.8.11] - 2026-08-27 ### Correcciones - *(flows)*: El cierre devuelve las respuestas de todo el camino ### Testing - *(flows)*: El payload de cierre no se cuelga con un flow que vuelve atras ## [2.8.10] - 2026-08-26 ### Nuevas funcionalidades - *(routing)*: Filtro que rutea solo al agente asignado al cliente ## [2.8.9] - 2026-08-25 ### Correcciones - *(flows)*: Registrar en Tauris el Flow JSON de Meta y no el documento del autor - *(flows)*: Frenar una lista de opciones puesta donde va texto ## [2.8.8] - 2026-08-22 ### Correcciones - *(chatbot)*: Los datos precargados del nodo flow se interpolan y se castean ## [2.8.7] - 2026-08-21 ### Correcciones - *(flows)*: Un texto que mezcla literal y referencia sale como nested expression ## [2.8.6] - 2026-08-21 ### Correcciones - *(flows)*: Rechazar una condicion que combine varias comparaciones ### Documentación - *(flows)*: Plan para armar la lista navegable desde un dato de entrada ## [2.8.5] - 2026-08-21 ### Correcciones - *(flows)*: Parentizar las comparaciones y rechazar el cierre fuera de una terminal ## [2.8.4] - 2026-08-21 ### Correcciones - *(flows)*: Leer un dato de entrada de otra pantalla desde un texto ## [2.8.3] - 2026-08-20 ### Nuevas funcionalidades - *(flows)*: Leer un dato de entrada de otra pantalla ## [2.8.2] - 2026-08-20 ### Correcciones - *(flows)*: Un selector de archivo ya no viaja en el payload de una navegacion ### Nuevas funcionalidades - *(flows)*: Verificar los topes de Meta solo en canales de WhatsApp ## [2.8.1] - 2026-08-19 ### Correcciones - *(flows)*: Un disparador vacio ya no borra el copy del mensaje ## [2.8.0] - 2026-08-19 ### Nuevas funcionalidades - *(flows)*: Nebula Flows, el documento propio del editor - *(flows)*: Conversor de Nebula Flows al Flow JSON de Meta - *(flows)*: Validar un borrador sin publicarlo ## [2.7.5] - 2026-08-19 ### Correcciones - *(flows)*: Guardar el borrador sin validarlo ni subirlo a Meta ## [2.7.4] - 2026-08-13 ### Nuevas funcionalidades - *(ai)*: Alinear el nodo ai_agent con el contrato de Orion AI Studio ## [2.7.3] - 2026-08-12 ### Documentación - Corregir el orden del flujo de release - Anotar que recrear un tag no vuelve a disparar el build - Corregir la nota sobre el disparo del build por tag ### Nuevas funcionalidades - *(auth)*: Rol manager, un owner delegado por tenant ## [2.7.2] - 2026-08-12 ### Correcciones - *(gupshup)*: Respetar los topes de WhatsApp en mensajes interactivos - *(templates)*: Conservar el motivo de un envio fallido y los estados de Meta - *(gupshup)*: Aceptar el shape de parametros de template que manda el composer ### Nuevas funcionalidades - *(gupshup)*: Enviar los parametros de encabezado y botones de un template - *(gupshup)*: Permitir footer en quick reply y en listas - *(chatbot)*: Footer configurable en los nodos quick_reply y button_list - *(templates)*: Subir el archivo de muestra de los encabezados multimedia ## [2.7.1] - 2026-08-11 ### Correcciones - *(gupshup)*: Enviar templates por la Partner API y no por el endpoint self-serve ### Notas de actualización Sin migraciones. `GUPSHUP_MESSAGING_API_URL` deja de leerse; puede quedar seteada en los manifiestos sin efecto. Arregla que **todos** los envíos de template de WhatsApp fallaran con 401: el `appKey` del canal es un token de Partner App y la ruta de templates lo mandaba al endpoint self-serve, que no lo reconoce. El resto de los mensajes no estaba afectado. Los parámetros de **header y botones todavía no se traducen** — solo los del cuerpo, igual que antes del arreglo. Un template con header multimedia o botón dinámico ahora es rechazado por Meta con su propio error en vez de con un 401 genérico. ## [2.7.0] - 2026-08-11 ### Nuevas funcionalidades - *(sla)*: Cerrar la conversacion al incumplirse el sla de abandono - *(routing)*: Afinidad de agente por contacto ### Notas de actualización Migración `062_contact_preferred_user`: agrega `contacts.preferred_user_id` (`TEXT NULL`, FK a `users` con `ON DELETE SET NULL`) más un índice parcial. Aditiva, sin backfill. Ningún canal cambia de comportamiento con el deploy. Las dos funcionalidades son opt-in: - **Cierre por SLA**: requiere un PUT de la config de SLA del canal con `escalation.type = close_thread`, y exige `abandonSeconds` configurado. Solo actúa sobre la métrica `abandon` — en `first_response` y `resolution` el breach sigue emitiendo solo el evento. Reusa el `noAgentClosingMessage` del canal (v2.6.15). - **Afinidad de agente**: requiere un PUT de la routing policy con el criterio `contact_affinity`. La fuente histórica sale de `agent_thread_sessions` y arranca casi vacía — se llena con el uso, así que las primeras semanas la fuente útil es el `preferredUserId` declarado en el contacto. ## [2.6.15] - 2026-08-11 ### Nuevas funcionalidades - *(routing)*: Cerrar el thread cuando la cascada no encuentra candidato ### Notas de actualización Migración `061_channel_no_agent_closing_message`: agrega `channels.no_agent_closing_message` (`TEXT NOT NULL DEFAULT ''`). Aditiva, sin backfill. Ningún canal cambia de comportamiento con el deploy. El cierre por falta de agentes es opt-in: requiere un PUT de la routing policy del canal con `on_no_match: close`. El valor `fail` sigue aceptado y queda deprecado. ## [2.6.14] - 2026-08-03 ### Correcciones - *(chatbot)*: Autogenerar el token de correlacion del nodo flow ### Notas de actualización Sin migraciones ni variables de entorno nuevas. El nodo `flow` mandaba `flowToken` tal cual venía de la prop, y como nadie la completa por default viajaba vacío: el sender lo omite por `omitempty` y la respuesta de Meta vuelve sin token. El engine no lo necesita —correlaciona por estado de sesión— pero el hilo del messenger sí, y sin token asocia pregunta y respuesta por orden: un bot que manda dos flows seguidos puede mostrar la respuesta pegada al flow equivocado. Ahora se genera solo cuando falta, con prefijo `bot_` (el messenger usa `msgr_`). Meta devuelve el token en `nfm_reply.response_json`, que es de donde el parser ya lo lee. Además, la prop pasa a interpolarse: era el único campo de texto del nodo que viajaba crudo, así que un autor que escribiera `{{context.turno_id}}` recibía el literal. Lo que define el autor sigue ganando sobre el autogenerado. Sin cambio para quien ya definía un token a mano. ## [2.6.13] - 2026-08-03 ### Correcciones - *(media)*: No propagar al frontend la URL del proveedor - *(media)*: Mapear el `url` entrante del webhook de media ### Documentación - *(chatbot)*: Actualizar el contrato de Orion AI Studio ## [2.6.12] - 2026-08-03 ### Correcciones - *(media)*: Preservar la referencia del medio en adjuntos entrantes ### Documentación - No fijar la cantidad de replicas de la API en CLAUDE.md ### Notas de actualización Sin migraciones ni variables de entorno nuevas. Los eventos de media entrantes ahora incluyen claves adicionales en el `content`: `providerMediaId`, `mimeType`, `filename` y `checksum` cuando el proveedor los manda, y `attachmentStatus` con el estado de almacenamiento (`stored` / `pending` / `failed`). Son aditivas y no rompen consumidores existentes: no hay unmarshal estricto en el backend y el frontend descarta claves desconocidas. Los adjuntos que llegaron antes de esta versión quedaron con `attachmentUrl` vacío y sin referencia al medio, así que no son recuperables. Esta versión evita que se sigan perdiendo, pero todavía no descarga el binario por ID: falta confirmar el endpoint y la autenticación de la API de media de Gupshup. Para eso, revisar tras el despliegue los logs `attachment without downloadable url` y su campo `has_provider_media_id`. ## [2.6.11] - 2026-08-03 ### Correcciones - *(chatbot)*: Migrar tambien el audit trail de versiones al trigger nuevo ### Nuevas funcionalidades - *(chatbot)*: Renombrar el trigger any a new_conversation y priorizarlo sobre keyword (**BREAKING**) ## [2.6.10] - 2026-08-03 ### Documentación - *(skill)*: Endpoint de stats de chatbot y dailyCompliance ### Nuevas funcionalidades - *(activity)*: Agregado de stats de chatbot por tenant - *(sla)*: Serie diaria de compliance y desglose por abandono ### Rendimiento - *(chatbot)*: Resolver las metricas por flow en un solo pase ### Testing - Reparar la suite de integracion - *(chatbot)*: Fijar las reglas de unicidad por tipo de trigger ## [2.6.9] - 2026-08-03 ### Correcciones - *(activity)*: Contar cada respuesta una sola vez y por turno ## [2.6.8] - 2026-08-03 ### Correcciones - *(activity)*: Atribuir la respuesta a quien la mando ## [2.6.7] - 2026-08-03 ### Correcciones - *(chatbot)*: Firmar los mensajes del nodo ai_agent con su propio autor - *(chatbot)*: Entregar las respuestas largas del ai_agent ## [2.6.6] - 2026-08-03 ### Correcciones - *(activity)*: Corregir la atribucion de las stats por agente - *(channels)*: Validar el tenant al asignar canales a un usuario ### Mantenimiento - Configuracion de dev server para el preview local ### Nuevas funcionalidades - *(messaging)*: Marcar el tipo de autor en los eventos salientes - *(activity)*: Comando de backfill de snapshots de stats por agente ## [2.6.5] - 2026-08-03 ### Nuevas funcionalidades - *(channels)*: Mensaje de despedida al cerrar una conversacion ## [2.6.4] - 2026-08-03 ### Correcciones - *(chatbot)*: Preservar las claves del bucket en los patches de los steps ### Nuevas funcionalidades - *(ai)*: Medir la latencia del proveedor por turno ## [2.6.3] - 2026-08-02 ### Correcciones - *(chatbot)*: Permitir que el handoff del bot asigne el thread ## [2.6.2] - 2026-08-02 ### Correcciones - *(routing)*: Encolar el thread en vez de reportar que no hay agentes - *(ai)*: Distinguir el 409 de idempotencia del asistente sin publicar ### Nuevas funcionalidades - *(chatbot)*: Saludo de apertura del nodo ai_agent ## [2.6.1] - 2026-08-02 ### Correcciones - *(chatbot)*: Turnos, timers y cancelacion del nodo ai_agent - *(chatbot)*: Puertos declarados sin cablear y cierre del assign_agent ## [2.6.0] - 2026-08-01 ### Nuevas funcionalidades - *(chatbot)*: Validate acepta el estado del canvas sin guardar ## [2.5.0] - 2026-08-01 ### Nuevas funcionalidades - *(ai)*: El alcance del ApiToken de Orion se detecta solo, y el nodo `ai_agent` deja de pedir el asistente cuando la credencial ya esta atada a uno ### Notas de actualización - **`props.botId` pasa a `props.assistantId`** en el nodo `ai_agent`, y el endpoint que compone el runtime pasa de `/v1/bots/...` a `/v1/assistants/...` — Orion renombro `bot` a `assistant` en su v1.1.0 y el AgentToken a **ApiToken** (`orion_api_`). Los nombres y los tokens viejos siguen andando del lado de Orion; Nebula usa los nuevos. Un flow guardado con `botId` deja de validar: hay que reabrirlo y volver a elegir el asistente. - **Al crear o rotar una credencial `orion-ai-studio` la API llama a Orion** para detectar el alcance del token (`GET /v1/assistants/me/capabilities`). Un `401` corta el alta con `REJECTED_API_KEY`; si Orion no responde, la credencial se guarda con `scope: "unknown"` y el nodo sigue pidiendo el `assistantId`. - **`assistantId` es obligatorio en el nodo salvo con `scope: "assistant"`.** Con ese alcance el runtime resuelve por el alias `me`. - `GET /tenants/{id}/ai-credentials` suma `scope`, `assistantId` y `assistantName` a cada credencial de Orion. Requiere la version correspondiente de `nebula_app`. - La migracion `053` agrega esas tres columnas. No toca filas existentes; las credenciales de Orion cargadas antes quedan sin alcance hasta la proxima rotacion. ## [2.4.0] - 2026-08-01 ### Nuevas funcionalidades - *(ai)*: Proveedor `orion-ai-studio` para credenciales y para el nodo `ai_agent` — el agente publicado en Orion define comportamiento, skills y modelo; el nodo solo elige credencial y `botId` ### Correcciones - *(ai)*: Buscar el check de `provider` por definicion y no por nombre al migrar ### Notas de actualización - **Nuevo proveedor `orion-ai-studio`** en `POST /tenants/{id}/ai-credentials` y en `props.provider` del nodo `ai_agent`. La key de la credencial es el AgentToken de Orion (`orion_agt_...`). - Con ese proveedor el nodo `ai_agent` **exige** `credentialId` y `botId`, **rechaza** `endpointUrl` (lo compone el runtime sobre la base de Orion), no pide `model` ni `systemPrompt`, y `timeoutSeconds` debe ser 0 (default 25s) o >= 25. - La migracion `052` amplia el CHECK de `tenant_ai_credentials.provider`. No toca filas existentes: el vocabulario nuevo es un superconjunto del anterior. - `ORION_AI_STUDIO_BASE_URL` es opcional — vacia usa la base productiva de Orion AI Studio. Solo hace falta para staging u on-prem. - Las credenciales de Orion ya se pueden crear por API; la pantalla en `nebula_app` llega en una version posterior. ## [2.3.0] - 2026-08-01 ### Nuevas funcionalidades - *(ai)*: N credenciales de IA por tenant, cada una con nombre, proveedor, key propia, endpoint permitido y sus propios caps mensuales - *(chatbot)*: `credentialId` por nodo `ai_agent` — cada nodo puede usar una credencial distinta del tenant ### Correcciones - *(migrate)*: Que una migracion fallida no se marque como aplicada ### Mantenimiento - *(ai)*: Retirar las keys globales de Nebula - *(observability)*: Retirar el sink de OpenSearch ### Notas de actualización - **Nebula deja de tener keys de IA a nivel plataforma.** Se retiran el toggle `use_nebula_keys`, la tabla `nebula_ai_credentials`, las env vars `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` y los endpoints `/admin/nebula-ai-credentials` y `/tenants/{id}/ai-credentials/use-nebula`, que pasan a devolver 404. Ese camino nunca llego a estar activo en produccion: el pipeline jamas inyecto esas env vars. - **`GET /tenants/{id}/ai-credentials` cambia de forma:** devuelve `{credentials: [...]}` en vez de los tres slots fijos. Se suman `POST`, `PATCH /{credId}`, `PUT /{credId}/key` y `DELETE /{credId}`. Borrar una credencial en uso devuelve 409 con los flows que la referencian. Requiere la version correspondiente de `nebula_app`. - **`GET /tenants/{id}/ai-usage` agrupa por credencial**, no por proveedor: `currentMonth` pasa de map a array. Los caps son por credencial, asi que arrancan en cero. - El vocabulario de `provider` en credenciales y en `ai_usage_logs` pasa al del nodo (`openai-compatible` / `anthropic` / `gemini` / `custom`). El slot `openai` deja de existir. - Los flows que no traen `props.credentialId` siguen funcionando sin tocarlos: resuelven la credencial marcada como default de su proveedor. - Si una credencial declara `allowedEndpoint`, el `endpointUrl` del nodo tiene que caer dentro; si no, el turno rutea a `out-fallback` **sin mandar la key**. - Migraciones 049, 050 y 051. La 039 se corrige de UUID a TEXT (`tenants.id` es TEXT): fallaba siempre y la tabla nunca existio. En produccion figura como aplicada y no se reejecuta. - **Una migracion fallida ya no se auto-fuerza:** el estado sucio deja el pod sin arrancar en vez de seguir sobre un esquema incompleto. Recuperarse exige resolver el esquema a mano y recien despues `migrate force`. - Los logs de aplicacion viven en la tabla `logs` de PostgreSQL. Las env vars `OPENSEARCH_*` ya no se leen. ## [2.2.2] - 2026-07-30 ### Documentación - Como se libera, y por que el tag va sobre main y no sobre dev - Versionar AGENTS.md y corregir el flujo de release que documentaba ### Nuevas funcionalidades - *(channel)*: Autenticar contra Tauris con el secret de cada suscripcion - *(channel)*: Generar secrets validos para Tauris desde la UI ### Notas de actualización - El GET de canales deja de devolver `connectionString` (no tenía consumidores) y pasa a devolver `secretMasked` y `hasSecret` por suscripción. El valor completo del secret sale solo por `GET /channels/{id}/subscriptions/{type}/secret`. - Los rechazos de validación de `subscriptions` pasan de 500 a 400. - Requiere `NEBULA_MASTER_KEY` en el entorno; sin ella los secrets se guardan en texto plano y el módulo de IA no se monta. Reemplaza a `AI_CREDENTIALS_KEY`, que sigue aceptándose para los entornos que ya tengan datos cifrados con ella. - La migración 048 elimina `connectionString` del JSONB de `subscriptions`. No es reversible. - Las suscripciones Tauris ya existentes quedan sin secret y su dispatch falla con 401 contra un Tauris actualizado. `scripts/backfill-tauris-secrets.sh` genera **un secret por instancia** —no por suscripción, porque el valor vive en el `ApiKey__Secret` de la instancia y varias suscripciones comparten una— los marca con `secretGenerated` para que la UI advierta que nadie los llevó todavía, e imprime el mapeo URL → secret. Correrlo sin argumentos es dry-run. ## [2.2.0] - 2026-07-29 ### Documentación - *(flows)*: Documentar el registro de flows en Tauris ### Nuevas funcionalidades - *(flows)*: Registrar en Tauris los flows publicados ## [2.1.3] - 2026-07-28 ### Nuevas funcionalidades - *(auth)*: Endpoints de reset y cambio de password - *(flows)*: Defaults de presentacion por flow ## [2.1.2] - 2026-07-27 ### Documentación - *(chatbot)*: Regla 12 de validacion, limites de texto del nodo flow ### Nuevas funcionalidades - *(chatbot)*: Validar los limites de texto del nodo flow al guardar ## [2.1.1] - 2026-07-27 ### Correcciones - *(chatbot)*: No mandar el nombre tecnico del flow como texto del mensaje ### Documentación - *(chatbot)*: Presentacion del mensaje del flow - *(changelog)*: Actualizar changelog a v2.1.1 [skip ci] ### Mantenimiento - Anclar el pattern `server` del gitignore a la raiz ## [2.1.0] - 2026-07-27 ### Correcciones - *(chatbot)*: Cerrar el thread cuando el flow llega al nodo end ### Documentación - *(chatbot)*: Documentar que el nodo end cierra el thread - *(changelog)*: Actualizar changelog a v2.1.0 [skip ci] ## [2.0.61] - 2026-07-27 ### Correcciones - *(flows)*: Validar el limite de 20 caracteres de NavigationList description ### Documentación - *(changelog)*: Actualizar changelog a v2.0.61 [skip ci] ## [2.0.60] - 2026-07-27 ### Correcciones - *(flows)*: Propagar los validation_errors de Meta como 400 INVALID_FLOW_JSON ### Documentación - *(flows)*: Reglas del schema de Meta para el generador de flows del front - *(flows)*: Presupuestos por screen y limites de caracteres no bloqueantes - *(flows)*: Documentar la denylist de propiedades y como extenderla - *(changelog)*: Actualizar changelog a v2.0.60 [skip ci] ### Nuevas funcionalidades - *(flows)*: Validar las reglas del schema de Meta antes de llamar al provider - *(flows)*: Completar el validador con los presupuestos y required de Meta ### Refactorizaciones - *(flows)*: Denylist extensible de propiedades rechazadas por Meta ## [2.0.59] - 2026-07-27 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.59 [skip ci] ### Nuevas funcionalidades - *(logs)*: Persistir logs en PostgreSQL como reemplazo de OpenSearch - *(logs)*: Endpoint GET /admin/logs para consultar los logs persistidos ## [2.0.58] - 2026-07-24 ### Correcciones - *(flows)*: Exigir endpoint para flows data_exchange antes de publicar ### Documentación - *(changelog)*: Actualizar changelog a v2.0.58 [skip ci] ## [2.0.57] - 2026-07-24 ### Correcciones - *(flows)*: Loguear y propagar el error del proveedor al publicar y deprecar ### Documentación - *(changelog)*: Actualizar changelog a v2.0.57 [skip ci] ## [2.0.54] - 2026-07-22 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.54 [skip ci] ### Nuevas funcionalidades - *(flows)*: Soportar componentes completos de WhatsApp Flows y respuestas en Tauris ## [2.0.53] - 2026-06-30 ### Correcciones - *(channel)*: Limitar health check a subs nuevas o cambiadas ### Documentación - *(changelog)*: Actualizar changelog a v2.0.53 [skip ci] ## [2.0.52] - 2026-06-03 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.52 [skip ci] ### Nuevas funcionalidades - *(templates)*: Bloquear edicion de templates gupshup (eliminar + crear) ## [2.0.51] - 2026-06-03 ### Correcciones - *(templates)*: No sobrescribir el nombre visible al importar/sincronizar ### Documentación - *(changelog)*: Actualizar changelog a v2.0.51 [skip ci] ## [2.0.50] - 2026-06-03 ### Correcciones - *(templates)*: Mapear status FAILED de gupshup a rejected ### Documentación - *(changelog)*: Actualizar changelog a v2.0.50 [skip ci] ## [2.0.49] - 2026-06-03 ### Correcciones - *(templates)*: Persistir el id de gupshup para editar/sincronizar/eliminar ### Documentación - *(changelog)*: Actualizar changelog a v2.0.49 [skip ci] ## [2.0.48] - 2026-06-03 ### Correcciones - *(templates)*: Enviar exampleHeader para header text cuando hay samples ### Documentación - *(changelog)*: Actualizar changelog a v2.0.48 [skip ci] ## [2.0.47] - 2026-06-03 ### Correcciones - *(templates)*: Construir el body de gupshup con su formato flat real ### Documentación - *(changelog)*: Actualizar changelog a v2.0.47 [skip ci] ## [2.0.46] - 2026-06-03 ### Correcciones - *(templates)*: Enviar campo vertical requerido por gupshup ### Documentación - *(changelog)*: Actualizar changelog a v2.0.46 [skip ci] ## [2.0.45] - 2026-06-03 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.45 [skip ci] ### Nuevas funcionalidades - *(templates)*: Validar example de variables antes de enviar a gupshup ## [2.0.44] - 2026-06-03 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.44 [skip ci] ### Nuevas funcionalidades - *(templates)*: Separar nombre visible del slug del proveedor y exponer rechazos ## [2.0.43] - 2026-05-29 ### Correcciones - *(sla)*: Excluir threads del chatbot del monitor de first-response - *(routing)*: No encolar threads cuando el canal no tiene miembros ### Documentación - *(changelog)*: Actualizar changelog a v2.0.43 [skip ci] ## [2.0.42] - 2026-05-28 ### Correcciones - *(subscriber)*: Tauris dispatch path es /webhook/nebula/ ### Documentación - *(changelog)*: Actualizar changelog a v2.0.42 [skip ci] ### Nuevas funcionalidades - *(metrics)*: Latencia outgoing + reduccion de latencia del chatbot ## [2.0.41] - 2026-05-27 ### Correcciones - *(enricher)*: Alinear EnrichedPost con el contrato RelatedPost del SMI - *(subscriber)*: Alinear relatedPost SMI con modelo RelatedPost.cs ### Documentación - *(changelog)*: Actualizar changelog a v2.0.41 [skip ci] - *(changelog)*: Actualizar changelog a v2.0.41 [skip ci] ## [2.0.40] - 2026-05-27 ### Correcciones - *(enricher)*: Normalizar timestamps Graph API al formato RFC3339 con Z ### Documentación - *(changelog)*: Actualizar changelog a v2.0.40 [skip ci] ## [2.0.39] - 2026-05-27 ### Correcciones - *(meta)*: Corregir relatedPost en dispatch a SMI ### Documentación - *(changelog)*: Actualizar changelog a v2.0.39 [skip ci] ## [2.0.38] - 2026-05-27 ### Correcciones - *(enricher)*: Sanitizar URL y body en errores Graph API - *(enricher)*: Preservar URL de attachments no-photo en EnrichComment ### Documentación - *(changelog)*: Actualizar changelog a v2.0.38 [skip ci] ### Refactorizaciones - *(incoming)*: Orquestar enrichments de comment con un solo roundtrip ### Rendimiento - Paralelizar Graph API fetches y ampliar log previews ## [2.0.37] - 2026-05-26 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.37 [skip ci] ### Nuevas funcionalidades - *(incoming)*: IsWorkingComment + fetch del comment padre en bypass ## [2.0.36] - 2026-05-26 ### Correcciones - *(incoming)*: Enrich related post antes del bypass dispatch ### Documentación - *(changelog)*: Actualizar changelog a v2.0.36 [skip ci] ## [2.0.35] - 2026-05-22 ### Correcciones - *(channel)*: OAuth IG guarda user_id legacy + cleanup en externos por sub ### Documentación - *(changelog)*: Actualizar changelog a v2.0.35 [skip ci] ## [2.0.34] - 2026-05-22 ### Correcciones - *(subscriber)*: Extender fallback profileFullname a Tauris/AGS ### Documentación - *(changelog)*: Actualizar changelog a v2.0.34 [skip ci] ## [2.0.33] - 2026-05-22 ### Correcciones - *(subscriber)*: Fallback profileFullname al PSID + body preview en dispatch ### Documentación - *(changelog)*: Actualizar changelog a v2.0.33 [skip ci] ## [2.0.32] - 2026-05-22 ### Correcciones - *(media)*: Propagar caption de image y video punta a punta - *(oauth)*: Agregar business_management a scopes default de Facebook - *(channel)*: Rutear sync de canales SMI al endpoint correcto - *(oauth)*: Incluir pages_manage_engagement en scopes default de Facebook - *(channel)*: Apuntar apiUrl SMI a Graph API por provider Meta ### Documentación - *(changelog)*: Actualizar changelog a v2.0.28 [skip ci] - *(changelog)*: Actualizar changelog a v2.0.29 [skip ci] - *(changelog)*: Actualizar changelog a v2.0.30 [skip ci] - *(changelog)*: Actualizar changelog a v2.0.31 [skip ci] - *(changelog)*: Actualizar changelog a v2.0.32 [skip ci] ### Nuevas funcionalidades - *(webhook)*: Trazabilidad de eventos descartados y soporte IG legacy - *(observability)*: Logger contextual via middleware + refactor pipeline ## [2.0.27] - 2026-05-08 ### Correcciones - *(subscriber)*: Filtrar status_update/reaction/template_status_update para SMI ### Documentación - *(changelog)*: Actualizar changelog a v2.0.27 [skip ci] ## [2.0.26] - 2026-05-08 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.26 [skip ci] ### Nuevas funcionalidades - *(channel)*: Desbloquear PUT/POST y validar subscribers ## [2.0.25] - 2026-05-07 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.25 [skip ci] ### Nuevas funcionalidades - *(incoming)*: Bypass dispatch para subs externas y dispatch de status/reaction/templates ## [2.0.24] - 2026-05-07 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.24 - *(changelog)*: Incluir feat(channel) Voice mode en v2.0.24 [skip ci] ### Nuevas funcionalidades - *(subscription)*: Agregar ruteo de eventos por tipo de suscripcion - *(channel)*: Agregar modo Voice independiente para subs SIP ## [2.0.23] - 2026-04-27 ### Correcciones - *(aiagent)*: Proteger fakeUsageWriter con mutex en tests ### Documentación - *(changelog)*: Actualizar changelog a v2.0.23 [skip ci] - *(changelog)*: Incluir fix de race en v2.0.23 [skip ci] ### Nuevas funcionalidades - *(chatbot)*: Sandbox runtime mock para nodo ai_agent - *(aicredentials)*: Endpoint GET /tenants/:id/ai-usage ## [2.0.22] - 2026-04-27 ### CI/CD - *(lint)*: Pinear golangci-lint a v2.11.4 con retry ### Documentación - *(skill)*: Documentar out-timeout, out-fallback y aplanado - *(changelog)*: Actualizar changelog a v2.0.21 [skip ci] - *(changelog)*: Actualizar changelog a v2.0.22 [skip ci] ### Nuevas funcionalidades - *(chatbot)*: Out-timeout, out-fallback y aplanado de flow - *(chatbot)*: Nodo ai_agent con caps y botPhase - *(chatbot)*: Contenido dinamico en nodos qr/bl/template/flow ## [2.0.20] - 2026-04-23 ### Correcciones - *(chatbot)*: Disparar timeout y enviar timeoutMessage en end - *(activity)*: Poblar provider_breakdown y serializar slices vacios ### Documentación - *(skill)*: Documentar evento WS session_stats - *(changelog)*: Actualizar changelog a v2.0.20 [skip ci] ### Nuevas funcionalidades - *(realtime)*: Reemitir session_stats en assign/close/reopen ## [2.0.19] - 2026-04-22 ### Correcciones - *(lint)*: Suprimir gosec G120 en test handler controlado ### Documentación - *(changelog)*: Actualizar changelog a v2.0.19 [skip ci] ### Nuevas funcionalidades - *(flows)*: Agregar displayName editable en flows ## [2.0.18] - 2026-04-21 ### Correcciones - *(chatbot)*: Timeout de nodos cae al end del flow por default - *(chatbot)*: Simplificar timeout — siempre rutea al nodo end del flow - *(chatbot)*: Validator exige exactamente 1 start + 1 end por flow - *(security,runtime)*: 7 issues criticos del review del PR 498 - *(review)*: 10 medium + 8 low hardening items del review PR 498 - *(flows)*: Subir JSON como multipart ### Documentación - *(claude)*: Documentar dual-client Redis + tres rutas DSN PostgreSQL - Eliminar planes completados ya consolidados - *(plan)*: Aplicar correcciones ACD plan — rol supervisor + cascada routing - *(plan)*: Resolver decision #5 - policy del admin edita inclusion de supervisor en primary - *(claude)*: Actualizar TODOs post-plan ACD + agregar abandono chatbot - *(skill)*: Actualizar nebula-api-docs con modulos post-plan ACD - Brief para frontend priorizado (critico + chatbot + features nuevos) - Plan pre-deploy con analisis read-only de DB produccion - *(changelog)*: Actualizar changelog a v2.0.18 [skip ci] ### Nuevas funcionalidades - *(auth)*: Introducir rol supervisor con permisos granulares por canal - *(skill+presence)*: Catalogo de skills y tracking de presencia Redis - *(routing)*: Policy engine + dispatcher push con fallback cascade - *(sla)*: Motor de SLA con monitor periodico, escalation y compliance - *(chatbot)*: Handoff via policy engine con category y required_skills - *(chatbot)*: Port out-timeout + status abandoned para cliente que no responde - *(sla)*: Evaluar metrica abandon en el monitor periodico - *(sla)*: Emitir breach/warning/recovered por WebSocket - *(sla)*: Compliance agregada multi-canal del tenant - *(chatbot+messaging)*: Idle timeouts para sesiones y threads zombie - *(chatbot)*: TimeoutMessage configurable en nodo end - *(api)*: Exponer campos ACD/SLA + maxConcurrent + auto-ack ### Refactorizaciones - *(auth)*: Consolidar helpers de autorizacion en internal/auth/permissions - *(auth)*: Migrar checks de role inline a helpers de permissions - *(chatbot)*: Eliminar campo legacy department del assign_agent ## [2.0.17] - 2026-04-19 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.17 [skip ci] ### Nuevas funcionalidades - *(db)*: POSTGRES_READ_HOSTS para enrutar reads directo a la replica ## [2.0.16] - 2026-04-18 ### Correcciones - *(db)*: Bypasear pooler en migrations via POSTGRES_MIGRATION_HOSTS ### Documentación - *(changelog)*: Actualizar changelog a v2.0.16 [skip ci] ## [2.0.15] - 2026-04-18 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.15 [skip ci] ### Nuevas funcionalidades - *(redis)*: Soportar replica read-only para cache tolerante ## [2.0.14] - 2026-04-18 ### Correcciones - *(chatbot)*: No bloquear assign_agent cuando department esta vacio ### Documentación - *(changelog)*: Actualizar changelog a v2.0.14 [skip ci] ## [2.0.13] - 2026-04-18 ### Correcciones - *(chatbot)*: Matchear quick_reply y button_list contra payload real ### Documentación - *(changelog)*: Actualizar changelog a v2.0.13 [skip ci] ## [2.0.12] - 2026-04-18 ### Correcciones - *(chatbot)*: Corregir payload de quick_reply, button_list y flow ### Documentación - *(changelog)*: Actualizar changelog a v2.0.12 [skip ci] ## [2.0.11] - 2026-04-18 ### CI/CD - *(pipeline)*: Publicar image-metadata con digest inmutable ### Correcciones - *(chatbot)*: Corregir data race en TestWebhookDispatcher_postsAndSignsPayload - *(tenant)*: Agregar GET /tenants/me para que agents listen sus tenants - *(users)*: Scope la lista GET /users por X-Tenant-Id cuando esta presente - *(chatbot)*: Ocultar threads del inbox mientras el bot los maneja - *(chatbot)*: Corregir columna en FindActiveByThread (started_at no created_at) - *(chatbot)*: Skip inbox rooms en lifecycle events y delivery_status del bot - *(realtime)*: Agents ya no se suscriben a rooms realtime:* - *(broadcast)*: Excluir tenant_ room cuando el bot maneja - *(realtime)*: Respetar tenantId del payload de auth WS - *(auth)*: Emitir tenant_ids como array vacio en lugar de null - *(chatbot)*: Enviar templateId al outgoing en nodo template - *(realtime)*: Desacoplar inbox de rooms realtime admin ### Documentación - *(chatbot)*: Plan de ACD + policies + SLA post user_channels - *(realtime)*: Documentar separacion threads:* vs realtime:* - *(changelog)*: Actualizar changelog a v2.0.11 [skip ci] ### Mantenimiento - *(claude)*: Actualizar permisos locales de sesion - *(repo)*: Dejar de trackear .claude/settings.local.json ### Nuevas funcionalidades - *(chatbot)*: Agregar fallback port al nodo assign_agent y stub en sandbox - *(channel)*: Asignacion user<->channel + filtrado de inbox por membership ### Rendimiento - *(gupshup)*: Habilitar HTTP/2 y TCP keep-alive en sender - *(channel)*: Agregar cache L1 en memoria sobre Redis - *(messaging)*: Publicar broadcasts realtime async ## [2.0.10] - 2026-04-17 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.10 [skip ci] ### Nuevas funcionalidades - *(chatbot)*: Agregar modulo de flujos conversacionales ## [2.0.9] - 2026-04-14 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.9 [skip ci] ### Nuevas funcionalidades - *(observability)*: Enriquecer message de logs de trace ## [2.0.8] - 2026-04-14 ### Documentación - *(events)*: Documentar content type flow_reply - *(changelog)*: Actualizar changelog a v2.0.8 [skip ci] ### Mantenimiento - *(observability)*: Silenciar workers idle ### Nuevas funcionalidades - *(observability)*: Trazar pipeline incoming en Info - *(observability)*: Trazar pipeline outgoing en Info - *(realtime)*: Trazar ciclo de vida WS con trace_id persistente - *(realtime)*: Trazar mensajes WS cliente->servidor ### Refactorizaciones - *(webhook)*: Loggear evento per-item en Info ## [2.0.7] - 2026-04-13 ### Correcciones - *(observability)*: Corregir buffer compartido en OpenSearchHandler ### Documentación - *(changelog)*: Actualizar changelog a v2.0.7 [skip ci] ## [2.0.6] - 2026-04-09 ### Documentación - *(changelog)*: Actualizar changelog a v2.0.6 [skip ci] ### Nuevas funcionalidades - *(observability)*: Agregar trazabilidad completa del pipeline incoming ## [2.0.5] - 2026-04-08 ### Correcciones - *(db)*: Agregar soporte multi-host en DSN de PostgreSQL ### Documentación - *(changelog)*: Actualizar changelog a v2.0.5 [skip ci] ### Estilos - *(lint)*: Suprimir gosec G117 en token refresh ## [2.0.4] - 2026-04-08 ### Correcciones - *(db)*: Corregir Update que usaba pool read-only ### Documentación - *(changelog)*: Actualizar changelog a v2.0.4 [skip ci] ### Estilos - *(db)*: Ordenar import gofumpt en main.go ### Mantenimiento - *(ci)*: Eliminar lint/test del pipeline principal ### Nuevas funcionalidades - *(channel)*: Agregar worker de refresh de tokens ## [2.0.3] - 2026-04-08 ### Correcciones - *(meta)*: Corregir doble slash en URL de Instagram comment reply - *(db)*: Agregar target_session_attrs=read-write al DSN ### Documentación - *(changelog)*: Actualizar changelog a v2.0.3 [skip ci] ### Refactorizaciones - *(db)*: Separar pools de lectura y escritura ## [2.0.2] - 2026-04-07 ### Correcciones - *(subscriber)*: Alinear payload SMI con contrato .NET ### Documentación - *(changelog)*: Actualizar changelog a v2.0.2 [skip ci] ### Estilos - *(meta)*: Agrupar parametros del mismo tipo ## [2.0.1] - 2026-04-07 ### Correcciones - *(ci)*: Corregir nombre del pipeline de infra - *(ci)*: Usar pipeline ID en vez de nombre para trigger - *(ci)*: Usar REST API para trigger de pipeline de infra - *(ci)*: Eliminar stage TriggerDeploy (infra se auto-triggerea) ### Documentación - *(changelog)*: Actualizar changelog a v2.0.1 [skip ci] ### Mantenimiento - Trigger test ### Nuevas funcionalidades - *(sandbox)*: Agregar soporte sandbox para storage y Meta API ## [2.0.0] - 2026-04-01 ### CI/CD - *(pipeline)*: Agregar pipeline PR y versionado SemVer - *(pipeline-pr)*: Quitar job changelog del pipeline PR - *(pipeline-pr)*: Ejecutar tests después de lint ### Correcciones - *(infra)*: Corregir pipeline, prefijo S3 y Graph API - *(infra)*: Actualizar Go a 1.26 en Dockerfile y pipeline - *(lint)*: Manejar error de Write en ruta changelog - *(pipeline)*: Usar checkout con fetchTags para resolver versión - *(pipeline)*: Agregar Build como dependencia directa para propagar imageVersion - *(webhook)*: Evitar dispatch duplicado en canales Gupshup - *(cors)*: Agregar middleware CORS faltante al router - *(cors)*: Agregar X-Tenant-Id a headers permitidos - *(webhook)*: Renombrar /webhooks/ a /webhook/ - *(webhook)*: Resolver canal con providerName correcto - *(channel)*: Buscar canal Gupshup por appName - *(threads)*: Usar expresion de columnas en upsert - *(channel)*: Disparar integracion Gupshup al editar suscripciones - *(realtime)*: Resolver tenant desde thread para superadmin y admin - *(realtime)*: Resolver canal por ID y dedup broadcast - *(activity)*: Usar COALESCE en upsert de webhook stats - *(events)*: Agregar reply_to_event_id al SELECT de ListByThread - *(incoming)*: Mover status_update y reaction antes del dedup check - *(gupshup)*: Corregir matching de provider IDs para delivery status y reactions - *(gupshup)*: Timestamp V3 puede ser numero o string - *(channel)*: Resolver Gupshup por appId en vez de appName - *(migrate)*: Limpiar params de pool del DSN - *(template)*: Retornar 502 en errores de provider - *(template)*: Usar appKey del canal para ListTemplates - *(gupshup)*: Usar appKey del canal en todos los endpoints - *(template)*: Corregir import de containerMeta de Gupshup - *(meta)*: Corregir key accessToken en settings - *(meta)*: Usar accountId para Instagram en sender - *(webhook)*: Separar app secret por provider - *(storage)*: Buffear body HTTP antes de upload S3 para soportar retries - *(activity)*: Pasar actor_display_name en acciones de thread - *(gupshup)*: Usar v3 en ambas suscripciones webhook - *(gupshup)*: Agregar path /webhook/gupshup a URL del primary webhook - *(gupshup)*: Corregir parametro modes y valores en registro webhook - *(meta)*: Filtrar mensajes echo de bot en webhooks ### Documentación - *(changelog)*: Generar changelog v2.0.0 - *(changelog)*: Actualizar changelog v2.0.1 [skip ci] - *(changelog)*: Actualizar changelog v2.0.1 [skip ci] - *(changelog)*: Actualizar changelog v2.0.1 [skip ci] - *(changelog)*: Actualizar changelog v2.0.3 [skip ci] - *(changelog)*: Actualizar changelog v2.0.4 [skip ci] - *(changelog)*: Actualizar changelog v2.1.0 [skip ci] - *(skills)*: Agregar skill nebula-api-docs y enriquecer CLAUDE.md - *(changelog)*: Actualizar changelog v2.1.1 [skip ci] - Ajustar sync de skills en CLAUDE.md - *(changelog)*: Actualizar changelog v2.1.2 [skip ci] - *(changelog)*: Actualizar changelog v2.2.0 [skip ci] - *(changelog)*: Actualizar changelog v2.2.0 [skip ci] - *(changelog)*: Actualizar changelog v2.2.1 [skip ci] - *(changelog)*: Actualizar changelog v2.2.2 [skip ci] - *(changelog)*: Actualizar changelog v2.2.3 [skip ci] - *(changelog)*: Actualizar changelog v2.2.4 [skip ci] - *(changelog)*: Actualizar changelog v2.2.5 [skip ci] - *(changelog)*: Actualizar changelog v2.2.6 [skip ci] - *(changelog)*: Actualizar changelog v2.3.0 [skip ci] - *(api)*: Documentar nuevos endpoints de upload y filtros - *(changelog)*: Actualizar changelog v2.3.1 [skip ci] - *(changelog)*: Actualizar changelog v2.3.2 [skip ci] - *(api)*: Documentar content types de send_message - *(changelog)*: Actualizar changelog v2.3.3 [skip ci] - *(changelog)*: Actualizar changelog v2.3.4 [skip ci] - *(api)*: Actualizar referencia de events, outgoing y websocket - *(changelog)*: Actualizar changelog v2.4.0 [skip ci] - *(api)*: Documentar endpoints de activity stats messages, agents y words - *(changelog)*: Actualizar changelog v2.5.0 [skip ci] - *(api)*: Actualizar skill activity con ejemplos por variante - *(changelog)*: Actualizar changelog v2.5.1 [skip ci] - *(api)*: Agregar incomingPipeline a KPIs en skill - *(changelog)*: Actualizar changelog v2.5.2 [skip ci] - *(changelog)*: Actualizar changelog v2.5.3 [skip ci] - *(changelog)*: Actualizar changelog v2.5.4 [skip ci] - *(changelog)*: Actualizar changelog v2.6.0 [skip ci] - *(changelog)*: Actualizar changelog v2.6.1 [skip ci] - *(skill)*: Reestructurar nebula-api-docs y documentar nuevas features - *(changelog)*: Actualizar changelog v2.6.2 [skip ci] - *(changelog)*: Actualizar changelog v2.6.2 [skip ci] - *(skill)*: Actualizar templates y flows en nebula-api-docs - *(changelog)*: Actualizar changelog v2.6.3 [skip ci] - *(changelog)*: Actualizar changelog v2.6.4 [skip ci] - *(changelog)*: Actualizar changelog v2.6.5 [skip ci] - *(skill)*: Reorganizar sub-recursos de channel - *(skill)*: Agregar referencia de suscripciones externas - Actualizar skill con agents_online y dispatchEnabled ### Mantenimiento - Agregar git-cliff para changelog - *(pipeline)*: Usar environment prod - *(k8s)*: Ajustar recursos y escalar a 3 réplicas - Mejorar sync de skills con rm + cp y validacion - Actualizar dependencias y agregar migrate embebido - *(db)*: Eliminar indexes redundantes o sin uso - Agregar binario server a gitignore - *(release)*: Actualizar changelog para v2.6.6 [skip ci] - *(release)*: Actualizar changelog para v2.7.0 [skip ci] - *(release)*: Actualizar changelog para v2.7.1 [skip ci] - *(release)*: Actualizar changelog para v2.7.2 [skip ci] - *(release)*: Actualizar changelog para v2.7.3 [skip ci] - *(release)*: Actualizar changelog para v2.7.4 [skip ci] - *(release)*: Actualizar changelog para v2.7.5 [skip ci] - *(release)*: Actualizar changelog para v2.7.6 [skip ci] - *(release)*: Actualizar changelog para v2.7.7 [skip ci] - *(release)*: Actualizar changelog para v2.7.8 [skip ci] - *(config)*: Eliminar variable META_APP_SECRET - *(release)*: Actualizar changelog para v2.7.9 [skip ci] - *(release)*: Actualizar changelog para v2.7.10 [skip ci] - *(release)*: Actualizar changelog para v2.7.11 [skip ci] ### Nuevas funcionalidades - Inicializar Nebula API Go - *(infra)*: Agregar Dockerfile, manifiestos K8s y pipeline Azure DevOps - Completar paridad .NET en channel lifecycle e integraciones - *(storage)*: Usar keys estructurados para S3 - *(api)*: Agregar ruta raíz con mensaje de bienvenida - *(api)*: Estandarizar responses y mejorar activity logging - *(auth)*: Agregar status al PUT /users - *(realtime)*: Soporte multi-pod para WebSocket - *(channel)*: Agregar PUT y DELETE por channelId - *(realtime)*: Routing de broadcast por assignee - *(threads)*: Agregar filtro contact_id, ruta events y upload de adjuntos - *(storage)*: Agregar endpoints de upload por dominio sin key manual - *(media)*: Validar formatos y tamaños en upload - *(templates)*: Alinear content types y agregar modulo de templates - *(meta)*: Agregar enrichment de posts y suscripcion comments Instagram - *(flows)*: Agregar CRUD de WhatsApp Flows con Gupshup Partner API - *(messaging)*: Agregar comment reply, comment DM, flow content types y filtro self-comments - *(reply)*: Persistir replyToEventId y agregar quote reply en 3 providers - *(activity)*: Agregar stats worker con daily aggregation y distributed lock - *(activity)*: Conteo de mensajes near-realtime con Redis counters - *(activity)*: Stats de rendimiento por agente con response time y thread duration - *(activity)*: Palabras frecuentes en conversaciones con batch worker - *(activity)*: Registrar endpoints de stats y wirear modulos en server - *(dashboard)*: Agregar outgoing provider y filtro channel_id a KPIs - *(activity)*: Medir latencia pipeline incoming - *(stats)*: Trackear latencia y success rate del outgoing provider - *(messaging)*: Soporte reactions y broadcast delivery status - *(snippet)*: Modulo CRUD para plantillas quick_reply y button_list - *(gupshup)*: Migrar webhooks V2 a V3 Cloud API - *(template)*: Importar templates existentes de Gupshup - *(logging)*: Enriquecer logs de messaging para OpenSearch - *(oauth)*: Agregar profile picture a pages de Facebook - *(oauth)*: Agregar profile picture a pages de Facebook - *(incoming)*: Enriquecer perfil de sender en mensajes Meta - *(contacts)*: Mejorar matching por campo - *(channel)*: Agregar auth por token y cache Redis - *(contacts)*: Agregar cache Redis, identities en response y merge con seleccion de campos - *(incoming)*: Agregar dispatchEnabled por suscripcion - *(activity)*: Agregar tracking de sesiones y stats de agentes - *(channel)*: Renombrar modo Nebula a Tauris - *(subscriber)*: Implementar translator SMI ### Refactorizaciones - *(messaging)*: Eliminar content type comment_dm - *(webhook)*: Procesar eventos inline sin Redis stream - Mover template, flow y snippet a channel/ - *(channel)*: Anidar endpoints de sub-recursos bajo /{channelId} - *(meta)*: Separar parser Facebook e Instagram - Centralizar infraestructura K8s y consolidar a PROD v2.0.0 ### Rendimiento - Tunear pools de conexiones y buffers para escalar a 100K evt/10min - *(incoming)*: Optimizar pipeline webhook Meta de ~231ms a ~25ms ### Testing - *(all)*: Agregar tests unitarios e integration (~134 tests)