Changelog

Product changelog

Every Simmetric Chat release, from the newest version to the oldest.

Unreleased

Added

  • Modelo de propiedad de los widgets (2026-10-05 "wown", quick 261005-scn). Los widgets ahora tienen propietarios (su creador) y usuarios (concesiones por widget): - La unión widget_users (migración aditiva) + el tope users.widgetQuotaLimit por propietario; NULL/fail-closed 0 — las instalaciones intactas ven cero cambio de comportamiento (el idioma del retículo maxSponsoredUsers de la Phase-206). - El CRUD de widgets rearmado para PROPIETARIOS no administradores: POST /api/widgets va por el nuevo permiso widget:manage + el tope asignado por la administración (superar → 409 { error, quota: "widgets" }); las barreras de licencia a nivel de instalación (widget_enabled / max_widgets) siguen sin cambios. GET list/detail, PUT settings + las vinculaciones de workspace alcanzan creator | widget-user | admin (los ID ajenos permanecen como 404 no enumerantes); DELETE sigue siendo creator | admin. - La nueva sección de administración en el área de widgets (/widgets?tab=quotas, la pestaña de cuota): filas por usuario — user | owned | max-widgets — con escrituras assign / clear (null), búsqueda paginada, registrado en auditoría (user.widget_quota.updated). - La nueva pestaña "Usuarios" por widget (WidgetDetail, en modo de edición): el PROPIETARIO selecciona los usuarios del widget (una PUT de conjunto de reemplazo, la sonda del directorio de concesiones /widgets/owner-user-search — una superficie de elección de identidad, sin correos); los usuarios con concesión obtienen el brazo de solo lectura. - Pruebas: la matriz de servidor de 30 pines (widgetOwnership.test.ts), los brazos de frontend (WidgetUsersTab/WidgetQuotasTab), el recorrido e2e (widget-ownership.spec.ts).

Fixed

  • Compartir un workspace establece el rol editor por defecto, no el de lector (2026-10-05 "viewer-default"). El síntoma reportado — "como usuario con concesión, no puedo crear chats nuevos ni escribir en los chats existentes del workspace compartido" — se remontó (vía una nueva sonda de integración de 4 pines, granteeChatWriteAccess.test.ts) al ROL de la concesión, no a las barreras: con WORKSPACE_ROLE_ENFORCEMENT=true, una fila WorkspaceAccess editor pasa GET /chats (200) Y POST /chat (200) server-side; una fila VIEWER pasa el brazo de lectura y se evalúa 403 en el brazo de escritura POR DISEÑO, y un receptor sin fila obtiene el 404 no enumerante (SC-4). AMBAS superficies de concesión (el diálogo de compartir del propietario WorkspaceAccessDialog y el panel de Settings SettingsWorkspaceAccess) preseleccionaban lector en los selectores de rol individuales Y masivos — un administrador que concedía sin cambiar el menú producía concesiones de solo lectura. La configuración por defecto de la UI también contradecía la intención a nivel de esquema: el comentario de migración de la Phase 189 D-12 fija el relleno de las filas heredadas en editor ("NOT the spec's viewer suggestion"). Ambas superficies establecen editor por defecto (individual + masivo); los pines de cableado de roles (una prueba que elige explícitamente lector y luego verifica que lector atraviesa hasta el final) quedan intactos. Nota: los usuarios con concesión ANTERIORES a esta corrección conservan su rol persistido — una reconcesión (upsert del rol) los re-evalúa.
  • "Resume el archivo adjunto" ya no miente con la frase prefabricada de no-resultados (el cierre de la brecha adjunto-antes-de-ingesta, 2026-10-05). Adjuntar un archivo en el chat lo carga a través de la pipeline de ingesta ASÍNCRONA; el mensaje pregunta un instante después, el documento todavía tiene CERO chunks, así que la resolución por ruta de attachedDocumentId (ambos brazos del chat en chat.ts) no inyectaba NADA: el agente caía en rag_search, el documento reciente no estaba aún en la tienda de vectores, y la regla 3 de buildSystemPrompt respondía con la frase prefabricada "Non ho trovato informazioni su questo argomento nei documenti del workspace. Verifica che i file pertinenti siano stati caricati e indicizzati." — una afirmación falsa (el workspace puede estar lleno; solo el adjunto reciente no está listo). Ambos sitios de resolución ahora detectan el brazo de cero chunks e inyectan un aviso veraz (buildAttachmentNoticeChunk(new utils/attachmentNotice.ts)): el brazo non-empty-ragContext del orchestrator elimina rag_search, de modo que el modelo responde desde el aviso — "todavía en procesamiento, reintenta en un momento" (pending/processing) o "procesamiento fallido/cancelado, vuelve a cargar" — y el aviso prohíbe explícitamente la frase prefabricada. Consciente de estado y progreso (el progreso 0-100 del collector aparece solo cuando > 0); el escaneo DLP del contexto RAG montado en el aviso sin cambios. 5 pines de unidad sobre el ayudante (los brazos pending/processing/failed/cancelled + la instrucción anti-regla-3). Continuación de frontend — el esperador de ingest del chip de adjunto: el chip consulta cada 1,5s el nuevo endpoint GET /api/documents/:id/attachment-status (triple control IDOR de metadatos, la proyección { name, status, progress }, sin chunks), muestra un indicador giratorio + el progreso % mientras ingesta, RETIENE la barrera de envío (canSend + la vía de Enter vía attachmentPending) hasta "completed", y suelta el adjunto con un aviso emergente sobre failed/cancelled (404 → caída silenciosa; los errores de red transitorios siguen consultando — un tropiezo del servidor no debe destruir una ingesta sana). La barrera es una cortesía del cliente: el aviso del servidor queda como respaldo veraz de las pestañas obsoletas. 4 pines de endpoint en documentIdor.test.ts (la proyección access-user + los brazos D-04 de administración); 2 pines de ChatInputArea (pending deshabilita el envío + el chip de progreso; completed lo vuelva a habilitar).
  • Settings → "Acceso a los workspaces": las filas de concesión muestran de nuevo el nombre del usuario (Q6-fast settings parity, 2026-10-05). El diálogo de acceso a los workspaces había corregido este mismo defecto (la cadena username → id), pero el panel de Settings mantenía la cadena PRE-Q6 user ? displayName(user) : userId — y el circuito D-15 nunca envía un objeto user adjunto (T-189-11), de modo que cada fila del panel expandida caía en el UUID crudo. El panel ahora refleja la cadena del diálogo (el usuario adjunto forma heredada → grant.username → id) y su suite fija la verdadera forma del circuito: el simulador estático eliminó el campo user nunca enviado, una prueba nueva verifica que el nombre de usuario se renderiza y el UUID jamás (+1 prueba).
  • El punto de entrada del workspace compartido: las filas de la página Workspaces ahora ABREN (2026-10-05 "open-row"). Un receptor de concesión a nivel de workspace (lector/editor SIN acceso a proyectos) nunca podía alcanzar el workspace compartido en la UI del chat: los selectores de la barra lateral son proyecto-primero — el menú de proyectos va por GET /api/projects (lista solo los proyectos del creador / ProjectAccess, sin brazo de concesión de workspace) y el menú de workspaces está disabled hasta que se seleccione un proyecto, filtrado por projectId === selectedProjectId — mientras la página Workspaces listaba el workspace compartido (el filtro OR de GET /api/workspaces incluye el brazo de concesión accessGrants.some(userId)) pero sus filas eran solo de gestión, sin forma de entrar. WorkspaceRow gana una acción "Open" (primera en la columna de acciones, renderizada para CADA fila listada — la pertenencia a la lista implica al menos lector por el filtro OR del servidor; la barrera del cliente es solo UX, las rutas del chat reponen la barrera server-side por solicitud) que monta el workspace a través de ChatContext (setWorkspaceId(id) + navigate("/") — persiste lastWorkspaceId, así que la restauración al arranque vuelve al contexto al recargar sin los selectores). Un efecto espejo en App mantiene coherentes los menús de la barra lateral cuando el workspace activo cambia fuera de los selectores — SOLO cuando el proyecto padre está en la lista de proyectos del usuario; la superficie del chat es pilotada por el solo currentWorkspaceId. Los selectores de la barra lateral ganan una entrada estática "Compartidos conmigo" (el valor centinela __shared__ en el menú de PROJECT, renderizado solo cuando el conjunto de compartidos no está vacío): con un proyecto seleccionado la lista de workspaces se acota a ese proyecto como antes; elegir el centinela (o una selección vacía al arranque) hace que el menú de workspaces liste el conjunto invisible-por-proyecto (todos los workspaces alcanzables por el filtro OR cuyo padre del proyecto falta en GET /api/projects) — la lista de compartidos del receptor solo-workspace, antes inalcanzable porque el control requería primero un proyecto Y Radix Select no puede vaciar un ID de proyecto concreto una vez elegido (callejón sin salida unidireccional; la entrada nombrada es el camino siempre clicable de regreso). El marcador va guiado por el contenido a través del ayudante compartido workspaceSelectPlaceholderKey en sidebar/navModel.tsx (un centinela vacío obsoleto de una concesión revocada degrada a la formulación genérica en lugar de un alcance compartido habilitado-y-vacío; workspaceSelectDisabled barrera los montajes de AppNavOverlay — el AppSidebarNav integrado se renderiza solo sobre elementos anfitriones). Las claves i18n workspace.open + sidebar.selectSharedWorkspace + sidebar.sharedProjects añadidas a los 8 idiomas — la paridad verde. Pines nuevos: las filas de lector muestran Open y jamás delete/edit, un onOpen ausente no renderiza ningún control, el pin acompaante de página verifica la escritura lastWorkspaceId en el localStorage vía el ChatProvider real (WorkspacesPage), el brazo compartido se renderiza desde los elementos anfitriones sin proyecto y se esconde cuando está vacío (AppSidebarNav), la elección del centinela muestra el marcador compartido mientras un centinela obsoleto degrada, y la matriz de barreras del overlay fija habilitado/deshabilitado por brazo + marcador en el escritorio y el móvil (AppNavOverlay).
  • El diálogo "Acceso" de los workspaces (el botón superior de la barra lateral) ya no desborda su propio borde (2026-10-05). Cada bloque de ancho completo se escapaba del borde redondeado del diálogo: el DialogContent del kit de UI es una rejilla grid SIN grid-template-columns explícito, de modo que los hijos se sentaban en UNA única pista implícita auto que crece hasta la contribución min-content del elemento más grande — una fila de concesión con un nombre de usuario UUID irrepartible de 36 caracteres aportaba 503–549px contra la caja de contenido de 488px (sm:max-w-[520px]), ensanchando la pista y ESTIRANDO cada elemento de ancho completo (el título, la fila de selectores, la lista de casillas, las <li> de concesión) más allá del borde. La corrección de dos capas: la base del kit de UI fija ahora una columna explícita de mínimo-cero (grid-cols-[minmax(0,1fr)] — toda la familia de Dialogs encoge los elementos a la caja de contenido limitada en vez de empujar la pista, en toda la aplicación) y el sitio del diálogo añade break-words al título interpolado + flex-wrap a las filas de concesión/masivas y a cada <li> de concesión (los botones de nowrap se envuelven a su propia línea por debajo de ~360px de pantalla). Verificado con Playwright: cero desbordamiento 380→1280px (los nombres de concesión se truncan con elipsis según lo diseñado, nada cruza el borde); el nuevo WorkspaceAccessDialog.test.tsx fija ambas capas — 174 suites / 1487 pruebas frontend verdes (+1 suite, +4 pruebas).
  • Las filas de concesión de "Acceso" muestran el NOMBRE del usuario de nuevo (el seguimiento rápido de Q6, 2026-10-05). La respuesta del servidor D-15 lleva {userId, username} pero NO un objeto de usuario adjunto (eliminado con T-189-11 para dejar de filtrar correos/roles), mientras el diálogo mostraba el nombre ÚNICAMENTE desde ese campo opcional user muerto — cada fila de usuario asignado caía en el ID UUID. Las filas de concesión ahora resuelven el usuario adjunto (forma heredada) → username → id; WorkspaceAccessGrant gana el campo username que el circuito realmente envía. Pin nuevo: el nombre de usuario renderizado / el ID jamás filtrado / las filas heredadas con null como nombre de usuario conservan la cadena de reserva al ID (+1 prueba).
  • El enlace profundo de Settings restaurado (2026-10-05). Una carga completa de la página /settings (marcador, entrada de la barra lateral, entradas de navegación heredadas) dejaba de abrir el diálogo de ajustes desde la revisión R-5 "settings as dialog": al primer commit las consultas de auth seguían en vuelo (user = null) y el efecto close-on-logout — declarado después del abridor en el mismo componente — lo volvía a cerrar en el mismo lote (open-then-cancel), y el pathname tras la carga ya estaba normalizado así que nada reabría. La navegación suave (el menú del usuario) no estaba afectada. El abridor ahora dispara solo sobre una sesión CARGADA y la normalización conserva la cadena de consulta, de modo que los enlaces profundos ?tab= sobreviven al montaje del diálogo; los marcadores ?tab= también ENTRAN ahora en la sección apuntada (el mismo brazo que un clic en una entrada de la barra: la página del grupo se abre y la entrada marca data-active). e2e/settings-left-menu-nav.spec.ts está verde de nuevo.
  • También verificado como no-un-error: POST/DELETE /api/system/settings/branding/icon son las rutas del plugin ENTERPRISE (simmetric-enterprise/brandingRoutes, Phase 145, mountProtected + adminLicense("white_label")) — la compilación community responde 404 y la UI degrada con el aviso emergente documentado de mejora por diseño (la barrera grep D-12 whiteLabelExtractionGuard); la batería EE 37 suites / 737 pruebas verdes, la barrera community verde.
  • El panel de Settings ya no desconecta a los usuarios no administradores que lo abren. El enrutador de los ajustes estaba bloqueado de punta a punta con requireAdmin (el permiso admin:settings) mientras el retículo de visibilidad por rol (SETTINGS_TAB_PERMISSIONS / RoleSectionVisibility) seguía renderizando secciones — Perfil, Apariencia, proveedores LLM, Avanzado — a los usuarios ordinarios. Abrir el panel (o una sección) hacía que la carcasa de SettingsPage consultara GET /api/system/settings, recibiera 403, y la página confundía 403 con 401: mostraba "Session expired. Please log in again.", borraba el token y redirigía a /login 3 segundos después — destruyendo una sesión enteramente válida por un simple hueco de permiso. Corrección en dos partes (2026-10-04 "settings-panel logout"): - Servidor: GET /api/system/settings ahora es consciente de permisos. Los administradores (portadores de admin:settings) conservan la carga útil de consola completa byte-igual a la anterior; cada otro usuario autenticado recibe solo el subconjunto NON_ADMIN_SETTINGS_READ_KEYS (compartido) — las claves no sensibles consumidas por las superficies alcanzables de los no administradores (BRANDING_* para la pestaña Apariencia / la cabecera de la app, ALLOW_NON_ADMIN_UPLOAD para la página de cargas) — cada entrada readOnly: true (la regla antidesfase: nunca renderizar un formulario que el servidor rechazaría con 403). Los secretos (SMTP_PASS, LLM_API_KEY, JWT_SECRET, DATABASE_URL, VECTOR_DB_API_KEY, ...) nunca pueden alcanzar una respuesta no administrativa. La vista de la organización (?organizationId=) y todas las rutas de escritura (PUT /, POST /mail-test) quedan solo para la administración (fail-closed). - Frontend: solo un 401 (fallo de autenticación) destruye la sesión — en el brazo de redirección de 3 segundos de SettingsPage y en el brazo de error useMe de App.tsx. Un 403 ahora renderiza una banda distinta "No tienes permiso para ver estos ajustes." (settings.errorNoPermission, los 8 idiomas) y la sesión sobrevive.
  • La ingesta del Collector: los archivos cargados con una extensión de tipo texto (.txt .md .csv) pero con unos bytes mágicos PDF ya no se indexan como UTF-8 crudo (el incidente AI-ACT.txt del 2026-10-01). Un PDF renombrado como texto alimentaba el índice de vectores solo con las entrañas del PDF sin comprimir (el diccionario de Información, los metadatos XMP, los diccionarios de objetos, los elementos de estructura) — 3321 chunks, ninguno de texto normativo — mientras el cuerpo dentro de los flujos comprimidos FlateDecode degradaba a basura U+FFFD. parseFile ahora husmea la cabecera %PDF- (los primeros 1024 bytes, la ventana desde la basura de ISO 32000) en los archivos declarados texto y los dirige al analizador de PDF; los metadatos de los chunks ganan un sello routedAsPdf y un falso positivo del husmeo (un archivo de texto que solo menciona "%PDF-" temprano) vuelve al analizador de texto en vez de hacer fallar la ingesta.
v0.28.1
2026-10-02

Added

  • Conectores: los conectores existentes ahora pueden ser MODIFICADOS desde la interfaz de administración (Q1-quick) — el brazo preexistente PUT /api/connectors/:id por fin sale a la superficie: el menú de desborde de la tarjeta gana Edit (la primera entrada), que abre un diálogo prellenado sobre la superficie mutable (el nombre, la vinculación del archivo de conocimiento, los mensajes de bienvenida/respaldo, la localización de respaldo, la tasa limitada + el límite de sesiones diarias — vacío pone null, 0 = el valor centinela UNLIMITED del enrutador). isEnabled queda en el interruptor de la tarjeta, el pollMode de Telegram en los botones de webhook, y la plataforma/el botToken/el workspace siguen siendo inmutables por esquema. El brazo PUT también gana el evento de auditoría chat_connector.updated (a paridad con el brazo DELETE, D-21).
  • Phase 221 (INSTLIC) — la ranura de pegado de licencia de instancia (la mejora enterprise sin .env): una nueva superficie de administración GET/PUT/DELETE /api/license/instance (requireAdmin + el middleware de inquilino; el apilamiento por ruta deja GET /info y /diagnose intactas) más el panel Settings → Security → Licencia. El pegado verifica primero por sonda un JWT RS256 de nivel ENTERPRISE no reclamado (una clave rechazada nunca persiste: la enumeración cerrada { error, reason } — invalid / expired / plugin_bound / community_tier), guarda SOLO el cifrado AES-256-GCM en SystemConfig (la clave INSTANCE_LICENSE — la disciplina de costura de escritura de SMTP_PASS), invalida el caché de configuración Redis D-07, y se aplica a la instancia EN MARCHA por el MISMO resolutor que ejecuta el paso de arranque (setCachedLicense — los sobresalientes de límite inyectados por el cargador sobreviven; las banderas booleanas se activan de inmediato; borrar el pegado vuelve al estado del próximo arranque con paridad de arranque). La precedencia, el arranque y el runtime idénticos: la ranura → la fila de plugin verificada → la variable de entorno LICENSE_KEY → Community. El PUT /api/system/settings masivo rechaza la clave (la doctrina de la ruta de escritura dedicada para la retención del chat) y el listado de ajustes nunca expone el cifrado.
  • Phase 219 (SETP-01/02) — banderas de ver + modificar por rol para cada sección de Settings: RoleSectionVisibility.canModify aditiva (se resta del respaldo SETTINGS_SECTION_MODIFY_PERMISSIONS — jamás concede, admin:settings terminal), settingsModify en GET /api/roles/me/menu-sections, la barrera del servidor en las mutaciones de sección que devuelve 403 { error, section } (las mutaciones /api/providers → llm; POST /api/agency/users → team) y una columna Modificar en el editor de visibilidad de roles (PUT /api/roles/:id/visibility acepta canModify por entrada — dejar en el mismo estado cuando falta).

Fixed

  • Los diálogos de confirmación de eliminación de la barra lateral ya no desbordan (Q5-quick): las confirmaciones de eliminación de chat/carpeta eran cajas w-80 cuyo botón del kit de UI es whitespace-nowrap shrink-0 POR DISEÑO — las etiquetas de acciones de carpeta localizadas largas ("Mantieni N chat…" / "Elimina N chat permanentemente") escapaban horizontalmente de la caja, y los nombres de carpeta interpolados largos desbordaban el título. Las cajas son ahora w-[22rem] max-w-[calc(100vw_-_2rem)] (más anchas + acotadas a la pantalla), las filas de botones flex-wrap, las dos etiquetas de acción largas sobrescriben el nowrap (whitespace-normal text-left), y los títulos/textos interpolados llevan break-words. Las tres claves i18n que faltaban del diálogo de eliminación del chat (sidebar.deleteChat / .deleteChatConfirm / .deleteChatButton) se entregaron en los 8 idiomas — la interfaz italiana ya no muestra los respaldos insertados en inglés ahí. El seguimiento Q5: el primer límite nunca compilaba — calc(100vw-2rem) dentro de un valor arbitrario es CSS INVÁLIDO (Tailwind exige la idioma del subrayado calc(100vw_-_2rem) para los espacios alrededor de los operadores) y la regla nunca se emitía silenciosamente, dejando la caja de 352px sin límite en las pantallas estrechas. El utilitario corregido se entrega en el CSS construido (max-width:calc(100vw - 2rem) verificado en el paquete dist).
  • Resolución del modelo: los turnos del chat ya no aterrizan sobre el predeterminado fijado en código qwen2.5:3b cuando no existe un pin explícito (Q4-quick — la causa raíz del incidente de los cuatro chats: los chats nuevos servían un modelo local pequeño en lugar del predeterminado del workspace, 34-98s por turno). Dos correcciones: (1) los predeterminados de esquema de Chat.model y WorkspaceAgentConfig.model cambiaron de "qwen2.5:3b" al centinela "" (migración aditiva — ALTER COLUMN … SET DEFAULT, las filas existentes intactas): la cadena falsa (model || chat.model || undefined, orchestrator:388/1059) ahora cae a la cadena de configuración del agente del workspace exactamente como el centinela hermano "default"; (2) la preferencia de modelo por workspace en el localStorage ahora es SOLO-EXPLÍCITA — las elecciones de la paleta escriben explicit: true, el re-persist automátic de la resiembra desapareció (un predeterminado resuelto por cadena no es una elección del usuario y no debe volverse pegajoso) y los valores heredados sin marca se limpian una vez y nunca se usan como candidatos. Una preferencia obsoleta (p. ej. qwen2.5:3b, sigue siendo válida-aunque-lenta en el registro) ya no puede sombrear el predeterminado del workspace en cada chat nuevo.
  • Chat: "un mensaje abre N chats" — el cliente adoptaba el chatId del servidor solo en el evento SSE done (hasta ~90s más tarde en los turnos ricos en herramientas), de modo que un reintento del usuario en esa ventana re-enviaba sin chatId y el servidor creaba un chat NUEVO por reintento, mientras el turno en vuelo terminaba en segundo plano como una fila huérfana (Q3-quick). La ruta del flujo ahora emite un evento aditivo chat que lleva el chatId al INICIO del turno (antes de cualquier token); el cliente lo adopta de inmediato (protegido + idempotente — un envío que ya lleva un chatId nunca re-adopta) y actualiza la barra lateral en el acto. Los reintentos ahora aterrizan en el MISMO chat. Byte-igual para los clientes previos a Q3 (los eventos SSE desconocidos se ignoran).
  • Recuerdo de memoria: un par (usuario, workspace) con CERO recuerdos almacenados ahora salta todo el gancho de recuerdo pre-LLM (Q2-quick) — una cuenta indexada de Postgres (@@index([userId, workspaceId])) bloquea el gancho antes de cualquier trabajo: sin incrustación de consulta, sin ida y vuelta del collector, y sin más advertencia por turno [vector-store] Tabla "ws_user_memory_…" no encontrada de una tabla LanceDB que legítimamente no existe hasta la PRIMERA escritura de recuerdo (el ensureTable de vectorStore crea en la primera ingesta; el brazo de búsqueda falla abierto a []). Un tropiezo de la base en la cuenta falla abierto al recuerdo prebarrera (el contrato de mejor esfuerzo del gancho — nunca bloquear el chat).
  • shared: la guardia de regresión FEATURE_FLAGS quedó rezagada por el traslado de los conectores a la EE (quick 260930-l4q) — el pin subió de 12 → 13 por connectors_enabled.

Fixed

  • Copia de seguridad/restauración: un ensayo en seco o una restauración que falla con "Unable to decrypt (no key in chain matched)" (credenciales de destino escritas bajo una ENCRYPTION_KEY previa) ahora devuelve un error guiado que nombra la causa, las palancas de recuperación (LEGACY_PREVIOUS_ENCRYPTION_KEYS / una ENCRYPTION_KEY previa, luego el reinicio), el número de claves probadas y el manual de recuperación docs/ENCRYPTION_KEY_ROTATION.md — que ganó una sección de recuperación de Docker "Backup restore dry-run fails". Solo la longitud de la cadena de claves se divulga, nunca el material de las claves.
  • Phase 220 (SPLIT-01/02) — la coherencia de la escisión del repositorio: docs/REPO_BOUNDARIES.md (la matriz de auditoría módulo→repositorio para 4 repos con la doctrina de clasificación de 7 criterios y las excepciones por diseño) + la barrera de fronteras del repositorio: scripts/repo-boundary-check.cjs (una barrera de grep con lista de permitidos — el núcleo no debe referenciar los paquetes de plugin privados fuera de las dos costuras de carga y de los sitios de contrato documentados) + la autoprueba scripts/repo-boundary-check.test.cjs + el trabajo de CI repo-boundary-check cableado en build.needs
v0.25.0
2026-09-09

Added

  • UI: revisión completa de la carcasa (R-2 → R-5). El estado vacío del chat muestra una marca de palabra monocromática elegante (la animación de descomposición cian/magenta queda ahora confinada al tema hacker); la barra lateral se convierte en un raíl minimalista al estilo Claude (nuevo chat + lista de chats + menú del pie) con la navegación completa en un overlay cmdk a la medida del teclado (Cmd/Ctrl+/) que mantiene el filtrado RBAC exacto; el menú de usuario se mudó de la barra superior a un diálogo dedicado (cuenta, idioma, tema, ajustes, cerrar sesión) con un cajón a la izquierda en el móvil; Settings se abre como un gran diálogo maestro-detalle (menú a la izquierda + detalle a área completa con conmutador de raíl y miga de pan) en vez de una página enrutada — el área principal es ahora el chat solo en cada página. Los temas (claro "Pearl", oscuro, hacker, sistema) recibieron una pasada de coherencia con correcciones de contraste WCAG-AA para el texto atenuado y los acentos del chat, guardias prefers-reduced-motion para todos los overlays, y una marca aware del etiquetar blanco en el logotipo del texto.
  • Docker: el servicio de metasolicitud de búsqueda searXNG opcional añadido a la pila compose (infraestructura pura — nada depende de él aún; la etiqueta de la versión fijada, un comprobador de salud basado en wget, los ajustes auto-abastecidos por el punto de entrada de la imagen).
  • Docker: el plugin privado @simmetric-chat/saas ahora se monta en el servidor compose como el plugin enterprise (un montaje por vínculo del repositorio hermano completo; el saasLoader no-ops sobre MODULE_NOT_FOUND en el modo community).

Fixed

  • Sembrado: un fallo de arranque durante la siembra de usuarios podía barrer los roles y bloquear al administrador; las siembras ahora se autosan — la concesión idempotente vuelve a aplicar el rol de administrador al usuario admin superviviente en cada arranque (consignado en los elementos diferidos de STATE.md).
  • Tenencia: POST /api/auth/register ahora consolida la creación del usuario, la concesión del rol predeterminado y la pertenencia de la organización por defecto en una sola transacción, y cada vía de siembra/arranque re-ejecuta el ayudante de pertenencia idempotente en su rama "el usuario ya existe" — un fallo a mitad del arranque/solicitud ya no puede dejar a un usuario sin pertenencia (la revisión de la Phase 182 WR-01); el resucitador de lápidas ahora aplica el roleInOrg de quien llama para que la eliminación degrade los privilegios (WR-02).
  • Las compilaciones Enterprise: la sección Settings → Advanced → Backup (y las rutas /sso y /logs) ya no quedan bloqueadas detrás de la tarjeta "requires Enterprise" tras un inicio de sesión en la aplicación o un acceso SSO. La consulta del manifiesto de los módulos enterprise (GET /api/enterprise/modules) ahora está bloqueada por la autenticación y es reactiva a la sesión: antes se consultaba una vez, sin autenticación, al arranque de la aplicación y se cacheaba para toda la sesión (retry: false + staleTime: Infinity), de modo que en la pantalla de inicio devolvía 401 y las funciones Enterprise permanecían ocultas hasta una recarga completa CON el token almacenado. El gancho ahora se suscribe a useMe() y vuelve a consultar el manifiesto cuando aparece una sesión (el inicio con contraseña, la entrega SSO ?token=), y nunca consulta el endpoint sin sesión en absoluto.
  • Docker: el ayudante de abastecimiento de secretos de producción (docker/provision-encryption-key.sh) ya no mete al servidor en bucles de fallo cuando el .env raíz se despliega con un marcador de posición del plantilla .env.example sin llenar (<sostituire-con-valore-generato>) como valor de API_KEY_HMAC_SECRET/ENCRYPTION_KEY — un marcador de posición ahora se reconoce como "no establecido" (advertencia sonora) y el abastecimiento cae a las vías de restauración/generación. Cualquier otro valor inválido no vacío sigue fallando ruidosamente en la barrera del arranque.
  • Seguridad: el router de ayuda para las pruebas E2E (/api/__tests__, un servidor eco MCP de inicio/parada sin autenticación) ya no se monta cuando el servidor corre con NODE_ENV=production — los arranques de producción responden 404 ahí, aplicando el contrato documentado de solo dev/prueba del router (ningún endpoint de engendrado de procesos sin autenticación en producción).
  • Seguridad: el control del callback del collector de importación de archivos (PUT /api/archives/import/:jobId/callback) ahora compara la cabecera X-Collector-Secret en tiempo constante (crypto.timingSafeEqual con una guarda de longitud), alineando la disciplina de tiempo seguro ya usada por el callback de estado de los documentos y la barrera de secreto del collector — cerrándola última comparación de secreto en claro en el servidor.
  • Autenticación: POST /api/auth/login ahora valida su cuerpo a nivel de ruta con el esquema compartido loginSchema (safeParse) — un cuerpo inválido o vacío devuelve 400 { error, details } coherente con cada otra ruta de autenticación, en vez de un 401 con un mensaje Zod crudo. Las credenciales válidas-aunque-erróneas siguen devolviendo 401 "Invalid credentials".
  • Autenticación: login ahora ejecuta un bcrypt.compare señuelo contra un hash desechable fijo cuando el usuario no se encuentra, así que el tiempo de respuesta ya no distingue nombres de usuario existentes de inexistentes (el endurecimiento de la enumeración). Ambas vías siguen devolviendo el mismo 401 "Invalid credentials".
  • Autenticación: POST /api/auth/admin-reset-password ahora valida su cuerpo con el nuevo esquema compartido adminResetPasswordSchema (userId UUID + newPassword de 8-128 caracteres, safeParse → 400 { error, details }) — la última ruta de autenticación deja la validación de cuerpo desestructurada ad-hoc.
  • Autenticación: la rama de registro cerrado de POST /api/auth/register ya no duplica la lógica de autenticación en línea — usa verifyToken / isTokenRevoked / isAdmin / getCachedUserWithRoles importados estáticamente (sin un await import dinámico, sin reviva comprobación de cabecera muerta, la búsqueda del usuario en caché) preservando cada forma de respuesta y el orden revocación-antes-de-búsqueda.
  • Seguridad: el endpoint de agotamiento de caché del widget (POST /api/config/:widgetId/cache-bust) ahora compara la cabecera X-Api-Key en tiempo constante (crypto.timingSafeEqual con una guarda de longitud) y falla cerrado si la clave esperada no está establecida, en lugar de la comparación en claro previa con la forma fail-open expected && — el control de la clave del widget ahora corresponde a la disciplina de tiempo seguro usada por las barreras de secreto del servidor y el collector.
  • La pipeline de publicación: las cinco imágenes de Docker ahora se construyen solo amd64. Las compilaciones linux/arm64 emuladas con QEMU se atascaban hasta ~5h (el widget primero, luego la compilación de fuentes pgvector todo-en-uno) y agotaban el disco de las minutos pagados de un repositorio privado; setup-qemu eliminado, cada entrada de la matriz publica linux/amd64, y la matriz de compilación se deserializa a max-parallel: 2. Los usuarios de arm64 pueden auto-compilar localmente (los Dockerfiles son seguros para la compilación cruzada) o adjuntar un corredor arm64 autohospedado — véase la nota arm64 de docs/DEPLOYMENT.md.

Changed

  • Herramientas: una barrera de código muerto ahora corre en CI (knip) y falla por archivos, dependencias o exportaciones no utilizadas fuera de las listas de permitidos documentadas; una barrida de todo el repositorio eliminó ~3400 líneas de código muerto, la generación de la clave API del widget ahora vive detrás del flujo estándar de administración, y los scripts i18n informan las claves de traducción definidas-pero-nunca-usadas como un indicio de limpieza no bloqueante.
  • Widgets: el pin del modelo de respuesta por widget — la configuración de administración de cada widget ahora puede seleccionar el proveedor LLM + el modelo que sirve las respuestas del chat de ese widget (responseProviderId/responseModel, migración aditiva add_widget_response_model). El pin se resuelve server-side desde la fila de la base de datos del widget en cada solicitud de chat (nunca provista por el cliente — el proxy del widget y la interfaz del visitante quedan intactas), tiene prioridad sobre el predeterminado del workspace y los widgets sin establecer conservan la cadena de resolución workspace/global existente. Los chats del widget ahora registran el modelo que realmente los sirve.
v0.22.0
2026-08-30

Added

  • RAG: el filtrado de metadatos para rag_search — el agente ahora puede acotar la recuperación por tipos de documento (documentTypes: pdf/md/txt/csv/docx/xlsx) y fechas de ingesta (dateFrom/dateTo, ISO; un límite superior de solo fecha incluye todo el día), corrigiendo el modo de fallo "los documentos de 2023 para las preguntas de 2025". Los filtros atraviesan la búsqueda híbrida entrando en ambas patas de recuperación: la pata de vectores llega al collector como verdaderos prefiltros sobre los metadatos de ingesta estampados (los predicados JSONB de pgvector + las cláusulas must de la carga útil de Qdrant; LanceDB y Chroma ignoran las claves nuevas con una advertencia registrada), y la pata FTS gana predicados SQL parametrizados sobre la tabla de documentos. Un respaldo post-recuperación server-side contra la tabla authoritativa de documentos garantiza la corrección filtrada en cada proveedor. Los documentos injeridos de aquí en adelante son siempre filtrables; los vectores heredados (pre-estampado) se excluyen mientras un filtro está activo_y regained the filterability via admin re-embed — sin cambio de esquema, sin re-embed de los documentos existentes, y un comportamiento sin filtros sin cambios.
  • DLP: cuatro patrones integrados nuevos para los identificadores europeos (sobre todo italianos) más comunes, sembrados idempotentemente por una migración de solo datos y reflejados en el conjunto de reserva sin base de datos — it_vat_iva (Partita IVA; ancorado a la etiqueta, así que un número desnudo de 11 cifras nunca coincide), it_codice_fiscale (el código fiscal de 16 caracteres; solo en mayúsculas, las palabras de prosa como "foschi" nunca coinciden), iban (el prefijo del país + longitud ≥ 15; las formas compacta y agrupada por espacios, tiempo lineal/a prueba de ReDoS), y eu_phone (teléfono IT/EU; sembrado DESHABILITADO para revisión de la administración, ya que cualquier regex telefónica estructurada señala las secuencias de dígitos de orden/referencia). Los integrados son ahora 10 en total; los despliegues existentes reciben las filas nuevas al migrate deploy con la seguridad de repetición ON CONFLICT DO NOTHING.
  • Chat: los administradores ahora pueden revelar o re-ocultar los textos censurados por DLP en toda la conversación de una vez, vía un nuevo elemento "Show DLP texts" / "Hide DLP texts" en el menú más-acciones (+) de la entrada del chat (los iconos Eye/EyeOff, mostrados solo a los administradores puesto que solo ellos pueden expandir un aviso DLP). La preferencia dura toda la sesión, oculta por defecto y aplica a cada aviso, incluidos los mensajes recién llegados; el botón de ojo por aviso sigue funcionando para los ajustes manuales por mensaje entre las conmutaciones globales.
  • La configuración de los patrones DLP: los seis patrones DLP ahora son respaldados por la base de datos (la tabla dlp_patterns, sembrada idempotentemente por la migración para que los despliegues existentes reciban los integrados) en lugar de fijados en código. Los administradores los gestionan en Settings → Advanced → DLP Patterns: habilitar/deshabilitar cualquier patrón por fila, renombrar los integrados (su regex está congelada — los intentos de modificación devuelven 400), y añadir hasta 50 patrones personalizados (nombre snake_case, fuente regex con validación de compilación al guardar, banderas, reemplazo) con una vista previa de prueba en vivo que muestra los segmentos coincidentes y el texto censurado. Los escaneos que puedan cargar el conjunto de la base de datos —la entrada/salida del plugin de filtro y la descarga final a fin de respuesta— usan los patrones configurados a través de un cache TTL de 5 minutos con la puesta en caché de regex compilados por patrón, cayendo de vuelta al conjunto integrado cuando la base de datos es inalcanzable; la descarga progresiva token por token del flujo mantiene los patrones integrados (la limitación documentada de la v1). Todo el panel está localizado en los 8 idiomas.
  • El bypass de roles DLP: un nuevo ajuste del sistema DLP_BYPASS_ROLES (un arreglo JSON de nombres de roles, por defecto []) permite a los administradores eximir a los roles seleccionados de TODO escaneo y censura DLP — la entrada/salida del plugin de filtro y el núcleo de descarga progresiva del flujo (incluyendo el escaneo del contexto RAG y la censura del pensamiento, tanto para el chat JWT como para la cuenta de servicio del widget) honran todos la misma barrera. Cada corrida con bypass dispara un evento de auditoría fire-and-forget dlp.bypassed que registra QUIÉN pasó por alto (los nombres de roles intersecados) y la superficie de origen — nunca el contenido escaneado. La interfaz de administración: una selección múltiple de roles con una advertencia de exposición en Settings → Advanced → DLP, y una etiqueta de acción "Con bypass" en el panel del historial de coincidencias DLP, localizados en los 8 idiomas.
  • El etiquetado y filtrado de la fuente de auditoría DLP: cada registro del evento dlp.* lleva source: "chat" | "widget" en sus metadatos — el núcleo compartido de la corriente SSE deriva la marca de la cabecera X-Widget-Id del widget para que las conversaciones del widget se censuren como las del chat pero ahora discernibles en el panel del historial de coincidencias DLP (Settings → Advanced). El panel gana filtros del lado del cliente por fuente (Todo/Chat/Widget), el ID del chat y el usuario.
  • La interfaz de administración para la recoleadora de borradores de carga: una nueva sección en Settings → Maintenance expone el conmutador habilitado (upload_draft_reaper_enabled) y la expresión cron de cadencia (upload_draft_reaper_cron) vía PUT /api/system/settings, con una precomprobación del lado del cliente de la forma cron de cinco campos (el servidor conserva su comportamiento de advertir-y-respaldo para los valores inválidos), avisos emergentes de éxito/error localizados y descripciones en los 8 idiomas. .env.example documenta las claves de la recoleadora como sobreescrituras de variables de entorno (upload_draft_reaper_enabled, upload_draft_reaper_cron, upload_draft_retention_days).
  • Config: la configuración del entorno ahora se define una vez en un esquema Zod compartido (@simmetric-chat/shared), la precedencia efectiva queda documentada y fijada por pruebas como DB > ENV > predeterminado (las ediciones del ajustes UI ganan de inmediato; una matriz de 8 casos), y las claves que se ignorarían porque una variable de entorno las sobrescribe se muestran con una placa envOverridden en la interfaz de ajustes.
  • Config: el .env de la raíz del repositorio ahora es la única configuración de runtime para el servidor, el collector y el widget — un nuevo cargador de cero dependencias (loadRootEnv()) lo combina bajo las sobreescrituras .env por paquete (heredadas), con una advertencia de obsolescencia una por arranque que enumera los nombres de claves tocados (nunca los valores); Docker y Tauri alineados (el env_file del compose apunta al archivo raíz, ninguna imagen hornea secretos).
  • Config: los tres archivos .env.example de los paquetes se regeneran desde sus esquemas Zod (83/15/6 claves, agrupadas, valores predeterminados sincronizados) con trampas de Jest que fallan cuando una futura clave del esquema queda sin documentar; las lecturas crudas de process.env conservadas fuera del esquema (HF/excentricidades de openai, las claves de cifrado/HMAC, LOG_LEVEL, las barreras de prueba) quedan fijadas por 44 sondas de guarda comportamentales.
  • UX: eliminar un workspace ahora pide un diálogo de confirmación estándar (era un miniconfirm en línea), los workspaces admiten la eliminación masiva multi-selección con un diálogo de confirmación y un aviso emergente de resultados que cuenta las filas saltadas (las filas sin permiso se saltan, nunca un fallo silencioso), la vista previa OCR lado a lado mantiene el texto extraído ligado a la misma porción de página que el panel de imagen, y el detalle de la corrida de síntesis muestra el botón de aprobación deshabilitado con la razón explícita en las corridas que todavía no se pueden aprobar.
  • UX: las casillas de selección de workspaces y la eliminación masiva están localizadas en los 8 idiomas.

Changed

  • La recoleadora de borradores de carga es totalmente configurable vía los ajustes del sistema: un conmutador habilitado/deshabilitado (upload_draft_reaper_enabled, por defecto "true") y una sobrescritura de cadencia (upload_draft_reaper_cron, por defecto diaria 03:00 UTC), ambos gestionados vía PUT /api/system/settings. Los valores cron inválidos registran una advertencia y vuelven a la cadencia por defecto en vez de hacer fallar el arranque; el comportamiento por defecto no cambia.

Fixed

  • Corregido un ciclo de fallo del arranque en producción Docker donde el arranque del servidor moría con un fallo de restricción única en api_keys.prefix: la sembradora de la clave API del widget ahora deriva el prefijo de visualización del compendio HMAC y tolera las colisiones de prefijo (recontrol por compendio de clave, advertencia en vez de fallo), y la generación de la clave API reintenta con una clave nueva ante una colisión de prefijo (máximo 3 intentos).
  • Las citas del chat ahora reflejan las fuentes en las que una respuesta realmente se funda en vez de todo lo que las herramientas de búsqueda recolectaron: tras la desduplicación de citas, un filtro de fundamentación acota las citas por documento al top-2 por puntuación de relevancia y descarta las citas de fragmentos cuyo texto no comparte solapamiento léxico significativo con la respuesta final (una barrera conservadora — las páginas wiki/archivolos resultados web/memoria, las citas sin texto de fragmento y las respuestas muy cortas siempre se conservan). Las respuestas de "más información" ya no muestran "Fonti (5)" de un solo documento a ~1,5% de relevancia; la exención "Sin evidencia de apoyo" se juzga igualmente sobre el conjunto filtrado (tanto la vía de flujo como la de no flujo). Solo la pipeline de citas — ningún cambio de interfaz, i18n o esquema.
  • Docker: el comprobador de salud del servicio ollama reemplazó la sonda curl (la imagen ollama/ollama no incluye curl/wget/nodo — el contenedor reportaba "unhealthy" por siempre) por una sonda ollama ls; el comprobador de salud qdrant reemplazó la sonda wget (la imagen no incluye wget/busybox) por un control de socket bash /dev/tcp — ambos servicios ahora reportan healthy. El docker/entrypoint.sh del contenedor único ahora procesa provision-encryption-key.sh (a paridad con la imagen separada): los arranques autónomos de producción auto-abastecen ENCRYPTION_KEY + API_KEY_HMAC_SECRET en vez de fallar la barrera. Los cinco Dockerfile pasan limpios bajo hadolint (la política documentada de dependencias sin fijar + las renuncias de ENV de marcado).
  • Los eventos de cronología de auditoría DLP ya no muestran "sin detalles de coincidencia" cuando el contenido coincidente llegó en un segmento temprano del flujo: el evento de flujo dlp.output_match ahora deriva sus tipos de coincidencia Y su lista de textos coincidentes de las coincidencias acumuladas en toda la corrida (todas las descargas progresivas más el escaneo final de la cola) en vez del escaneo del último segmento solo — las filas históricas podían almacenar un arreglo matchTypes vacío mientras la fila mostraba de todos modos una placa de tipo de coincidencia. El texto coincidente se almacena según el precedente del plugin de filtro DLP (dlp.input_match / dlp.output_match no de flujo ya lo llevan); el panel de auditoría es solo para la administración y bloqueado por Enterprise. La vista expandida del panel ahora renderiza la lista de tipos detectados con una pista "de solo tipo" para los eventos que registran tipos de coincidencia sin coincidencias (localizado en los 8 idiomas), manteniendo el respaldo "sin detalles" solo para los metadatos verdaderamente vacíos.
  • Los archivos cargados quedan disponibles para reintentar: los callbacks de estado terminal del collector ya no borran los archivos de borrador almacenados de storage/uploads/drafts/ (el callback de cualquier documento hermano podía borrar el archivo del que depende cada reintento), y /retry//assign se autosan restaurando el archivo de borrador almacenado desde la copia OCR persistente (storage/ocr-sources/) cuando existe en vez de fallar con "ya no existe en el disco".
  • Eliminar un borrador de carga fallido y obsoleto ya no devuelve bloqueado (409) — DELETE /api/uploads/:id ahora trata los estados de pata terminal (completada o fallida) como borrables y solo rechaza los borradores con una pata genuinamente en vuelo (habilitada no terminal), y la eliminación masiva del frontend usa el mismo predicado en vuelo que los chips del filtro para que lo que la interfaz ofrece borrar sea exactamente lo que el servidor acepta.
  • El reintento RAG/KB de una carga cuyo archivo de borrador almacenado falta ahora devuelve un claro error de recarga (400) en vez de un aviso falso de éxito, las patas del collector fallidas ya no borran el archivo de borrador almacenado de storage/uploads/drafts/ (los reintentos futuros siguen siendo posibles — la recoleadora de 24h y la ruta DELETE quedan como los únicos borradores de archivos), y los avisos emergentes de reintento ahora reflejan el desenlace por pata: una pata rechazada sobre HTTP 200 muestra un aviso de error localizado y el recuento de reintentos masivos lo cuenta como fallido.
  • Los despliegues Docker de producción auto-abastecen una ENCRYPTION_KEY persistente (un volumen de almacenamiento del servidor, /app/storage/.encryption-key) en vez de meterse en un bucle de fallo sobre el predeterminado duro. El punto de entrada del servidor genera la clave una vez antes de cualquier paso de Prisma, la restaura en cada reinicio/recompilación, prefiere un valor suministrado por el operador y falla ruidosamente ante una clave persistida corrupta. .env.example y docs/DEPLOYMENT.md actualizados para marcar la variable como Requerida-en-producción. Véase docs/ENCRYPTION_KEY_ROTATION.md para las notas de Docker.
  • Las páginas OCR de la KB ya no fallan cuando la corriente de visión de Ollama es cerrada por el motor de modelo sin el marcador final done tras una transcripción completa (observado con el motor personalizado glm-ocr): ocrPage ahora rescata la salida acumulada no vacía como el resultado de la página con una bandera truncated y un registro de advertencia, en vez de desechar una transcripción real detrás de "Did not receive done or success response in stream." Una corriente sin done y sin contenido aún hace fallar la página.
  • Los resúmenes de los trabajos OCR son veraces sobre la salud de las páginas: las páginas que terminan con un marcador [FAILED: se cuentan y almacenan como failedPages en el JSON de resultado del trabajo, y los registros de culminación emiten una advertencia [ocr] Trabajo completado con páginas fallidas, de modo que un trabajo COMPLETADO con páginas fallidas ya no es indistinguible de un éxito limpio (cero tokens + una puntuación de calidad al suelo por sí solos eran ambiguos).
  • Documentado y corregido el modo de fallo para los despliegues Enterprise: cuando @simmetric-chat/shared gana archivos de fuente NUEVOS, una instantánea fuera de fecha de la dependencia file: enterprise puede hacer fallar el servidor al arrancar con "Cannot find module './env.schema'" — pnpm toma la instantánea del paquete file: en node_modules/.pnpm y los archivos creados después del último pnpm install nunca alcanzan esa instantánea (los enlaces duros solo actualizan los archivos ya existentes), de modo que el schemas/index.js de la instantánea exige un módulo que no existe del lado enterprise, y la política fail-loud del cargador convierte el fallo del registro del plugin en un bucle de fallo de arranque. La corrección viaja como documentación: el manual de reconstrucción del repositorio enterprise ahora cubre la distinción reinstala-no-recompila (refresca la instantánea con pnpm install, luego pnpm build), y el manual de instalación de aire comprimido en docs/ENTERPRISE_PLUGIN.md gana la subsección de solución de problemas correspondiente (el síntoma, la causa, la corrección, el control de cordura) para que los operadores sin el repositorio privado también encuentren el remedio.
v0.21.0
2026-08-28

Barrida de deudas & higiene de release (era: v1.5.0)

Added

  • docs/LICENSE_DECISION.md — el análisis del modelo de licencia para la distribución doble (community + enterprise). Decisión: AGPL-3.0 (community) + comercial propietario (empresa).
  • LICENSE reemplazado Apache-2.0 → GNU AGPL-3.0 (el repositorio community).
  • LICENSE_EE.md — la licencia comercial propietaria del plugin enterprise (@simmetric-chat/enterprise, el paquete enterprise).
  • NOTICE — la explicación de la doble licencia en la raíz del repositorio.
  • CLA.md — el Acuerdo de Licencia de Colaborador (v1.0) — copyright + concesión de patente, consciente de la doble licencia. Requerido para toda contribución externa.
  • Cabeceras de copyright AGPL-3.0 añadidas a los 981 archivos de fuente a través de 5 paquetes (.ts/.tsx). Aplicadas vía scripts/add-headers.cjs (idempotente).
  • docs/ENTERPRISE_LICENSE_TERMS.md — los términos comerciales/acuerdo de nivel de servicio a la medida del operador para los clientes enterprise.
  • .github/workflows/release.yml — GitHub Release + la publicación de imágenes GHCR al empujar el tag.
  • CHANGELOG.md (este archivo) — el formato Keep-a-Changelog, sembrado desde la historia de los hitos.
  • .planning/milestones/v1.5-debt-sweep-ROADMAP.md — el alcance del hito de evaluación de deudas (el traslado E2E, las sesiones de depuración, la clasificación de tareas rápidas, la disciplina de sincronización de sellos de versión).

Changed

  • La versión de package.json subió de 0.17.0 → 1.4.0 (sincronizada con el último tag git v1.4). Las versiones por paquete quedan en 0.22.0/0.23.0 (paquetes privados, no publicados en ningún registro).
  • El campo license de package.json + todos los packages/*/package.json establecido en AGPL-3.0-or-later.
  • La placa de licencia del README.md actualizada de Apache-2.0 → AGPL-3.0; la sección de licencia actualizada para describir el modelo de doble licencia; la sección Contributing añadida con el requisito del CLA.
  • CONTRIBUTING.md actualizado con las instrucciones de firma del CLA.
v0.20.1
2026-08-27

Escala horizontal (la culminación de la capa Redis) (era: v1.4)

Objetivo: cerrar los 4 elementos de SCALE diferidos (CSW-19 + SCALE-02/03/04) y probar el escalado horizontal con una prueba de integración multi-instancia real — habilitando el despliegue de servidor multi-instancia seguro detrás de un equilibrador de carga.

Entregado: 7 fases (161–167), 13 planes, 29 tareas. Los 22/22 requisitos satisfechos. Tag git: v1.4.

Added

  • SCALE-01 (CSW-19): los 3 planificadores restantes envueltos con candado → luego los 8 migrados a trabajos cron pg-boss; setInterval + isRunning + withDistributedLock eliminados de los 8 planificadores migrados.
  • SCALE-02: el respaldo scryptSync de ENCRYPTION_KEY eliminado en producción — un arranque fail-loud (logger.error + process.exit(1)) + una excepción de servicio de defensa en profundidad; dev/test conserva scrypt; el manual de rotación actualizado.
  • SCALE-03: la verificación de claves API de bucle bcrypt → una búsqueda de compendio O(1) findUnique({key_hash}) con HMAC claveado; una migración de ruptura DROP COLUMN hashedKey + ADD COLUMN key_hash; la variable de entorno API_KEY_HMAC_SECRET; el manual de migración para el operador docs/API_KEY_MIGRATION.md.
  • SCALE-04: 8 planificadores setInterval migrados a pg-boss (una cola de trabajos con respaldo de Postgres, amiga del aire comprimido); 2 sondeadores de 10s sensibles a la latencia (OCR + síntesis) quedan como setInterval; el singleton pg-boss (jobQueue.ts) con degradación amable.
  • La verificación del fan-out SSE: el aplazamiento entre instancias cerrado — 4 pruebas entre instancias basadas en simuladores que prueban el relé SSE Redis pub/sub (A publica → B retransmite, el salto de origen, el desmontaje, la degradación).
  • La documentación del operador: docs/SCALING.md (377 líneas) — la topología del despliegue multi-instancia, el requisito REDIS_URL, la dependencia de Postgres de pg-boss, los referentes cruzados SCALE-02/03, los sondeadores de 10s, la degradación amable.

Cambios de ruptura — acción del operador requerida

  • `ENCRYPTION_KEY` debe establecerse en el .env de producción (base64, 32 bytes) o el servidor se niega a arrancar. Véase docs/ENCRYPTION_KEY_ROTATION.md.
  • Las claves API pasaron de bcrypt (apiKeyHash) → HMAC-SHA256 (key_hash). Los operadores deben re-emitir las claves API del widget/collector. Véase docs/API_KEY_MIGRATION.md.

Infraestructura

  • Nueva dependencia: pg-boss v12.28.0 (servidor). Nueva variable de entorno: API_KEY_HMAC_SECRET. El orden de arranque cambió — la inicialización de pg-boss es ahora el 7.º paso de init asíncrono con un desmontaje de apagado.
v0.20.0
2026-08-26

La barrida de CONCERNS (era: v1.3)

Objetivo: barrer la auditoría CONCERNS.md del 2026-08-26 — corregir todas las victorias fáciles directamente, abordar los elementos de esfuerzo medio en fases enfocadas, y evaluar (aplazar o documentar) los puntos bloqueantes/grandes esfuerzos.

Entregado: 7 fases (154–160), 14 planes. Un hito de endurecimiento brownfield; ninguna función nueva visible al usuario.

Added

  • la corrección del gitignore de la doc, el mapa de archivos de chat.ts, los volteos de reglas de lint.
  • las victorias fáciles de endurecimiento de código (el tope take:10 de apiKeyMiddleware, etc.).
  • la reducción de la deuda de lint del frontend (3 planes, los volteos advertencia→error).
  • la advertencia de arranque de producción de ENCRYPTION_KEY (CSW-11) + la generación en lote de título+etiquetas+seguimientos.
  • las divisiones de módulos (la extracción del analizador de proveedores, los registros de capacidades).
  • la cobertura de pruebas (chatImportService, chatExportService, apiKeyService, otros 5 servicios; el collector + el widget cableados en la configuración raíz de jest).
  • evaluado y documentado (los derechos JWT de licencia del .env, el manual de rotación de la clave de firma de licencia, la corrección de la afirmación obsoleta de AGENTS.md).
v0.19.1
2026-08-25

Refinamientos (era: v1.2)

Objetivo: marca, descargo de IA, la reimplementación en TypeScript del algoritmo de grafo del wiki, correcciones de búsqueda RAG, endurecimiento MCP, el primer arranque del Asistente de Configuración.

Entregado: 6 fases (149–153 + 153.1).

Added

  • BRAND-01/02: el favicon SVG del monograma S, el componente Monograma reutilizable, el descargo de IA localizado y atenuado debajo de cada mensaje del asistente (7 idiomas).
  • MCP-01/02/03: el endurecimiento MCP — el mapa de transporte SSE por sesión, la barrera de auth Bearer MCP_API_KEY, la IDOR de list_workspaces cerrada, getMCPToolsForWorkspace cableado en resolveSkillsForChat.
  • RAG-01/02/03: la corrección de la localidad FTS (la tokenización multi-idioma para it/ru/de/es/fr/zh), la desduplicación de los resultados de respaldo rag_search↔wiki_query, el prompt de asequibilidad LLM para rag_search vs wiki_query.
  • WIKI-01: la reimplementación en TypeScript del algoritmo Graphify del wiki (detección de comunidad + la generación markdown del wiki, una reimplementación limpia de Apache-2.0, ~280 líneas Python → TS).
  • WIZ-01/02: la interfaz de primer arranque del Asistente de Configuración (multipaso: cuenta de administración + configuración del proveedor + la base de vectores, el consumo de /api/system/initialize); SEED_BOOTSTRAP_ADMIN deshabilitado cuando el asistente está activo.
  • .1: la limpieza de la deuda técnica de v1.2 (el volteo advertencia→error del React Compiler).
v0.19.0
2026-08-19

La arquitectura del plugin Enterprise (era: v1.1)

Objetivo: refactorizar el modelo de licencias de "banderas de funciones a la vista en la fuente pública" a un "plugin enterprise privado separado": las funciones enterprise (SSO, registro de auditoría, etiquetar en blanco, copia de seguridad) viven en un paquete privado entregado solo a los clientes pagantes; el repositorio community no contiene código de funciones enterprise. Las funciones mercancía se vuelven siempre-ACTIVAS en la comunidad.

Entregado: 9 fases (140–148), 19 planes, 26 tareas.

Added

  • EPA-01: los tipos PluginContext/EnterprisePlugin en shared; la costura del cargador loadEnterprisePlugin(app) / shutdownEnterprisePlugin() con require.resolve→require en dos pasos (MODULE_NOT_FOUND = el no-op de la comunidad, otros errores = process.exit(1) fail-loud).
  • EPA-02: 9 banderas de mercancía eliminadas de FEATURE_FLAGS (20→10): web_search, webhooks, push_notifications, memory, lead_export, widget_analytics, auto_title, synthesis_rate_limit. Las rutas de mercancía siempre-ACTIVAS.
  • EPA-03: SSO (SAML + OIDC + SCIM 2.0) extraído al plugin enterprise.
  • EPA-04: el registro de auditoría (la tabla event_log inmutable + un rol de base de datos de solo INSERT) extraído al plugin enterprise.
  • EPA-05: la marca blanca (el gancho IoC del validador de clave de configuración) extraída al plugin enterprise.
  • EPA-06: la Copia de seguridad (~1400 líneas: backupService, el registro de proveedores, el planificador Bree, la restauración, las rutas, la interfaz) extraída al plugin enterprise.
  • EPA-07: el resolutor overrideFeatureLimit con revocación reactiva; el manifiesto GET /api/enterprise/modules; la carga condicional React.lazy de 4 paneles enterprise.
  • EPA-08/09/10: priority_support eliminado; custom_agents = un límite numérico (community 3, enterprise el Infinito); licensePayloadSchema byte-idéntico (RS256).
  • EPA-12: la alineación de documentos/entorno (docs/ENTERPRISE_PLUGIN.md, el montaje de volumen de docker-compose, la división de .env.example), el perfil CI de aire comprimido (una barrera de grep en el servicio de licencia para las primitivas HTTP).
v0.18.0
2026-08-12

El candidato de lanzamiento público (simmetric-chat) (era: v1.0)

Objetivo: preparar y publicar el proyecto como un repositorio público limpio, renombrado a simmetric-chat, sin secretos/datos personales/rutas locales, con la verificación de licencias de las dependencias, el generador de claves enterprise (separado, no publicado) y la documentación completa.

Entregado: 8 fases (132–139), 12 planes, 16 tareas. El repositorio público publicado, el cambio de nombre completo, la auditoría de licencias, el generador de claves enterprise (HS256 → migrado a RS256 tras la v1.0), los documentos canónicos, el CI de GitHub Actions, un repositorio nuevo.

v0.20
2026-08-11

La interfaz del widget, i18n & Fiabilidad

Entregado: 7 fases (125–131), 32 planes, 75 tareas, 16/16 requisitos satisfechos.

Added

  • WID-ADMIN: la subpágina con pestañas de creación/edición del widget (/widgets/:id + /widgets/new), el panel de vista previa en vivo, la guarda de suciedad, la pestaña de clientes potenciales.
  • WID-I18N: el transporte por bloques JSON (mata el fallo %20), la cadena de respaldo visitante→predeterminado-del-widget→en, la localización de la carcasa + el contenido a 7 idiomas, el hilado de la localización widget→proxy→servidor→orchestrator.
  • WID-QUEST: las preguntas sugeridas de tres estados (Predeterminado/Ninguna/Personalizadas), los grupos de 7 idiomas, la corrección del desbordamiento line-clamp.
  • WID-CREDITS: los créditos del pie + la placa de IA, la eliminación bloqueada por la marca blanca, el relé de popup de incrustación real verificado.
  • WID-BUGS: los botones de entrada de 44px, la ida y vuelta anfitrión-FAB, el contrato de iconos solo-SVG, el cartel de contacto, el bucle del wiki, la persistencia de los clientes potenciales, el i18n del RAG degradado.
v0.19
2026-08-08

Diagnóstico & Deudas

Entregado: 5 fases (120–124). Los diagnósticos de licencia (los registros del arranque, un endpoint de diagnóstico solo para la administración, el script pnpm license:check), la deuda de pruebas (la deriva ESM, LoginPage QueryClientProvider, la especificación de la forma de los simuladores), la evaluación de la deuda técnica (el pinchazo de la capa de escala Redis — aplazado a la v1.4).

v0.18
2026-08-04

Estabilización

Entregado: 4 fases (116–119).

v0.17
2026-08-02

Empresa & Acabado

Entregado: 8 fases (108–115).

Los hitos anteriores (v0.1–v0.16) están documentados en .planning/PROJECT.md (la sección de Historia de Hitos). La historia completa a nivel de fase vive en .planning/MILESTONES.md.