• El contexto: Fortalecimiento de una herramienta Wayland STT local.
  • El síntoma: El pegado sintético fallaba el 80 % de las veces.
  • La causa raíz: Una condición de carrera en la detección de dispositivos uinput de Linux.
  • La solución: Un teclado virtual persistente en proceso con inicialización diferida.
  • La filosofía: Depurar con IA cuando la deuda cognitiva supera al modelo mental.

Todo comenzó con una buena intención: intentar reforzar una base de código inspeccionándola en busca de debilidades arquitectónicas y antipatrones.

Creé parakeet-stt, una herramienta de transcripción push-to-talk totalmente local para Linux. Mantienes pulsada la tecla Ctrl derecha, hablas, la sueltas y el texto transcrito se inserta a través de uinput dondequiera que esté el cursor. Tiene una interfaz Rust que gestiona la tecla de acceso rápido y la superposición, y un demonio Python que ejecuta el modelo NeMo Parakeet de NVIDIA.

Utilizando un prompt personalizado, hice que un agente de IA revisara el código base. Una de las sugerencias fue delegar la inserción de texto a un proceso secundario. La idea era generar un proceso, dejar que emulara un teclado virtual para pegar el texto y luego morir, asegurando que la inserción estuviera completamente desacoplada del resto del código.

Lo implementé. Entonces me di cuenta de que pegar se había vuelto increíblemente poco fiable, fallando silenciosamente alrededor del 80 % de las veces.

Pasé los siguientes tres días gastando tokens y créditos para localizar un error que había introducido para resolver un problema que en realidad no tenía. Esto es lo que aprendí sobre los compositores Wayland, Linux uinput y la depuración cuando los mejores modelos disponibles no pueden encontrar la causa raíz.


El modo de fallo era específico: la dictado sin formato producía transcripciones con éxito y actualizaba el portapapeles del sistema, pero el pegado automático en las aplicaciones de destino (Ghostty, Brave, Zed) fallaba silenciosamente. Curiosamente, el pegado manual desde el portapapeles inmediatamente después funcionaba perfectamente.

Cuando no se entiende un problema, es tentador hacer conjeturas. Pedí a GPT 5.4 y a Amp’s Oracle que elaboraran una lista exhaustiva de todas las posibles causas raíz clasificadas por probabilidad, con cada entrada justificada a favor y en contra basándose en pruebas del código fuente real. Inicialmente asumimos que el pegado se activaba demasiado pronto después de soltar la tecla de acceso rápido, por lo que añadí un retraso de estabilización de 250 ms. No solucionó el problema.

Entonces sospeché que se trataba de una desviación del foco: tal vez el proceso secundario estaba dirigiendo el acorde sintético a la superficie equivocada. Añadí observabilidad para capturar instantáneas del foco del lado secundario, lo que demostró que la ventana correcta estaba realmente enfocada.

La prueba irrefutable

Después de tachar estas dos opciones de la lista, decidí crear un amplio conjunto de pruebas y un script paste-gap-matrix para evaluar el proceso de principio a fin, probando todos los backends y modos de configuración.

Los datos revelaron una discrepancia enorme:

  • uinput solo de inyección (sin ciclo de vida push-to-talk): tasa de éxito superior al 95 %.
  • uinput de flujo PTT (que genera un nuevo proceso por inyección): tasa de éxito del 20 %.

El problema no era el backend uinput en sí. Era el ciclo de vida del teclado virtual.

Cuando el flujo PTT generaba un nuevo subproceso, creaba un teclado virtual /dev/uinput completamente nuevo, emitía inmediatamente la combinación Ctrl+Shift+V y destruía el dispositivo cuando el proceso se cerraba.

La documentación de uinput de Linux advierte explícitamente sobre este patrón: después de UI_DEV_CREATE, el espacio de usuario necesita tiempo para detectar el nuevo dispositivo. Los compositores Wayland como COSMIC y Smithay procesan las conexiones en caliente de los dispositivos clasificándolos y asociándolos a un asiento. Si un dispositivo aún no es conocido por ningún asiento, sus eventos de teclado se descartan silenciosamente.

Al crear y destruir el dispositivo casi al instante, estaba compitiendo con la fase de descubrimiento del compositor. Las interpretaciones parciales del acorde durante el calentamiento del dispositivo también explicaban por qué a veces se truncaban el primer y el último carácter.

La solución

La solución fue eliminar el subproceso y pasar a un emisor uinput persistente integrado en el proceso.

En lugar de crear un dispositivo por cada trabajo, el sistema ahora utiliza una inicialización diferida:

  • Crea el teclado virtual /dev/uinput en el primer intento de pegado.
  • Aplica un retraso de calentamiento limitado solo después de la creación nueva, para permitir que el compositor Wayland descubra y asigne el dispositivo.
  • Mantiene el dispositivo activo para trabajos posteriores mientras siga funcionando correctamente.

Esto preserva la fiabilidad al mantener el dispositivo activo, al tiempo que permite la recuperación tardía de /dev/uinput mediante un retroceso limitado si falla la creación del dispositivo.

Modelos mentales mínimos

La lección atemporal aquí es simple: no arregles un problema que aún no tienes. Quería hacer la arquitectura más robusta, pero pasé por alto por completo las consecuencias de segundo orden de generar un nuevo subproceso y un nuevo teclado virtual para cada inyección.

Pero lo más interesante es cómo se solucionó esto. Mi modelo mental del sistema no se ajustaba a la realidad, un caso clásico de deuda cognitiva. No entendía completamente la pila de entrada subyacente de Wayland, pero seguí adelante de todos modos.

Traté el sistema como una caja negra y lo depuré a ciegas:

  1. Enumerar las posibles causas raíz y hacer que un modelo SOTA asigne puntuaciones de probabilidad.
  2. Centrarse exclusivamente en añadir observabilidad y pruebas, sin realizar cambios de comportamiento.
  3. Mantener un único archivo Markdown que documente todos los hechos, resultados de pruebas y evidencias sin opiniones. Seguir añadiendo información a ese archivo canónico a medida que se profundiza.
  4. Introducir los datos sin procesar en el modelo de razonamiento de mayor capacidad disponible al comienzo de cada sesión. Implementar el siguiente paso con el mayor retorno de inversión, repetir y volver a empezar.

¿Fue un desperdicio ineficiente de tokens? 100 %. Si lo hubiera codificado completamente a mano y hubiera mantenido un modelo mental perfecto, probablemente este error no habría existido en primer lugar.

Pero estamos asistiendo a un cambio de paradigma. El código elaborado a mano y el mantenimiento de una comprensión sistémica perfecta se están convirtiendo en algo parecido a montar a caballo: un medio de transporte encantador que ya no es estrictamente necesario. La verdadera habilidad consiste en aprender a aprovechar la ingeniería agencial para depurar y estabilizar sistemáticamente los sistemas sin incurrir en la carga cognitiva que tradicionalmente se asocia a ello.

Ver el código en GitHub