# Cómo los límites de stderr de MCP mantienen las ejecuciones de agentes en marcha

Un helper ruidoso puede detener una ejecución de agente sin romper un solo mensaje JSON-RPC. Lo consigue convirtiendo stderr en una cola sin límite y haciendo que otra parte de la pila pague el coste: una tubería se llena, un lector conserva megabytes, el apagado espera para siempre o una tarjeta de aprobación aparece después de que el usuario haya dejado de confiar en la ejecución.

Trata stderr como entrada no confiable y asígnale un presupuesto. Puede parecer exagerado hasta que ves un comando completamente normal emitir un diagnóstico por cada reintento, mientras la acción que debía respaldar ya terminó y el proceso principal sigue intentando recoger todo ese ruido. La solución no es «desactivar los registros». Hay que drenarlos continuamente, conservar una cantidad limitada, contabilizar lo que se descarta y mantener el estado de la acción independiente de la salida de texto.

## Stderr es una ruta de contrapresión

Stderr puede bloquear un helper cuando el proceso principal lo conecta a una tubería y no la consume con suficiente rapidez. Los sistemas operativos asignan búferes finitos a las tuberías. Cuando un proceso hijo llena ese búfer, su siguiente escritura espera a que haya un lector. Si el hijo solo llega a la ruta de éxito después de escribir el diagnóstico, la acción parece bloqueada aunque la solicitud de red, la conexión SSH o el trabajo local hayan funcionado correctamente.

El fallo opuesto es más silencioso, pero a menudo más caro. El proceso principal puede leer stderr a tiempo y añadir cada byte a una cadena, un búfer de eventos o el resultado de una herramienta MCP. La tubería nunca se llena, pero un hijo demasiado hablador puede consumir memoria suficiente para ralentizar el host, provocar presión de memoria o hacer que fallen llamadas posteriores. Un límite que solo protege la tubería es incompleto. Y un límite que solo protege la memoria también lo es.

Esto importa más con los agentes porque favorecen la ejecución en paralelo. Una tarea puede iniciar varios helpers, y un bucle de reintento puede crear otra ráfaga antes de que se drene la primera. Una tasa de registros que parece inofensiva en un terminal interactivo se convierte en un problema de recursos cuando varias sesiones la capturan al mismo tiempo.

Hay tres cosas distintas que deben tener límites:

- Los bytes que esperan en la tubería del sistema operativo.
- Los bytes que el puente conserva en memoria para una llamada.
- Los bytes que el puente expone a un agente, una interfaz de usuario o una proyección del diario.

No confundas un límite de líneas con un límite de bytes. Una sola línea puede contener un cuerpo de respuesta grande, una cadena de certificados o un objeto de error minimizado. Tampoco supongas que UTF-8 llega siempre en caracteres completos. Los lectores de flujo reciben bytes en fragmentos, y el límite debe seguir funcionando cuando un carácter multibyte cruza el límite entre dos fragmentos.

Un lector práctico mantiene un contador de bytes para todo el flujo y un búfer circular separado para la cola retenida. Cuando el total supera el límite, sigue drenando para que el hijo pueda terminar. Deja de hacer crecer el búfer retenido y registra que hubo truncamiento. Matar al hijo inmediatamente al superar el límite puede ser correcto para una herramienta cuya salida sea abusiva, pero es un mal valor predeterminado para los diagnósticos. A menudo necesitas el estado de salida real y las últimas líneas que lo explican.

## MCP mantiene stdout fuera de los límites

Un servidor MCP stdio debe tratar stdout como territorio del protocolo. Las indicaciones de transporte stdio de la especificación del Model Context Protocol establecen que un servidor no debe escribir en stdout nada que no sean mensajes MCP válidos. Es fácil descartar esta regla como una cuestión de formato. En la práctica, evita una clase mucho más grave de fallos: una línea informal de progreso de un helper puede hacer que el host interprete JSON no válido y abandone una sesión que por lo demás funcionaba bien.

Pon los diagnósticos destinados a personas en stderr, pero no supongas que por eso dejan de ser peligrosos. Stderr es un canal externo al protocolo solo en ese sentido. El host del proceso sigue decidiendo si hereda ese flujo, lo conecta a una tubería, lo captura, lo escribe en un terminal o lo envía a un registro estructurado. Cada opción cambia el comportamiento ante los fallos.

La herencia funciona para un desarrollador local porque el terminal consume la salida y el desarrollador puede verla. Es un mal valor predeterminado para un puente de agentes. Expone texto arbitrario en un lugar que quizá no se conserve, puede mezclar la salida de llamadas simultáneas y puede transportar detalles que el agente nunca debería recibir. La captura completa ayuda a diagnosticar solo si tiene un presupuesto.

Mantén aburrido el límite del protocolo. El servidor MCP debe enviar mensajes JSON-RPC válidos por stdout, mantener limitados sus propios diagnósticos en stderr y lanzar los helpers con sus flujos bajo control explícito. Un helper debe devolver resultados estructurados por el canal previsto. No debe imprimir un bloque JSON en stderr y esperar que el proceso principal lo reconozca más tarde.

Esta distinción evita un error recurrente: tratar stderr como un canal de respuesta alternativo. No lo es. No tiene un encuadre fiable, puede quedar incompleto durante una cancelación y puede contener salida de bibliotecas que no saben nada de tu modelo de acciones. Si quien llama necesita un número de reintentos, un código de error remoto o una lista de archivos modificados, añádelo al resultado estructurado. Reserva stderr para las pruebas que una persona pueda necesitar al diagnosticar un fallo.

## Capturar la salida necesita su propio presupuesto de memoria

Leer stderr en una tarea en segundo plano no hace que la captura sea segura. Solo mueve el cuello de botella de la tubería al heap. He visto hosts solucionar un bloqueo leyendo ambos flujos de forma concurrente y descubrir después que un helper defectuoso podía obligarlos a conservar cada byte hasta que terminara la acción. El proceso ahora termina, pero el host es quien sufre.

Usa un búfer circular para los diagnósticos retenidos. Un búfer circular conserva los bytes más recientes cuando se alcanza la capacidad, que suele ser donde aparece el error útil. Conserva también un prefijo corto si en tu entorno la primera línea tiene significado, por ejemplo porque contiene la invocación del comando o la versión de una biblioteca. No conserves ambos indefinidamente.

El siguiente pseudocódigo describe el comportamiento que conviene implementar. No depende de un lenguaje concreto.

```text
on_stderr_chunk(bytes):
  stderr_seen += length(bytes)
  if stderr_seen <= capture_limit:
    append_tail(bytes)
  else:
    append_tail(bytes)       # ring buffer evicts older bytes
    stderr_truncated = true
  continue_reading()
```

El comentario merece atención. `capture_limit` debe significar la cantidad que conservas, no el punto en el que dejas de leer. Una implementación estricta puede omitir `append_tail` después del límite y conservar los primeros bytes. Prefiero la cola porque los mensajes de error suelen llegar después de páginas de salida de progreso. Elijas lo que elijas, indícalo en el registro para que quien investigue más tarde sepa si está leyendo el principio o el final.

Coloca un segundo límite alrededor del objeto que lleva los diagnósticos al contexto del agente. Un agente no necesita una transcripción de varios megabytes para decidir si debe reintentar. Necesita un error conciso, el estado de salida y, como máximo, una cola seleccionada. Si el puente pasa la salida sin procesar del helper porque «el modelo podría necesitarla», proporciona a cada helper una forma de desplazar el resto del contexto de la tarea.

No decodifiques y vuelvas a codificar un flujo grande solo para aplicar un límite de texto. Cuenta los bytes sin procesar antes de construir cadenas. Decodifica el fragmento retenido con una estrategia de reemplazo para las secuencias no válidas y etiquétalo como stderr capturado. Así evitas desperdiciar memoria y también evitas dar una falsa sensación de seguridad cuando un helper emite datos binarios por error.

## Una acción completada todavía puede fallar durante el apagado

El apagado del proceso es donde las avalanchas de registros se convierten en informes de incidentes engañosos. La acción puede haber terminado correctamente en remoto, el helper puede haber impreso su diagnóstico final y el proceso principal puede seguir sin producir un resultado porque espera el evento equivocado en el orden equivocado.

Una secuencia habitual es esta:

1. El puente inicia un helper y empieza a leer stdout, pero la lectura de stderr se retrasa durante una ráfaga.
2. El helper completa su acción externa y después escribe suficientes diagnósticos para llenar la tubería de stderr.
3. El proceso principal cancela la sesión o alcanza un plazo y envía una señal de terminación.
4. El proceso principal espera al hijo antes de cerrar o drenar los lectores de los flujos.
5. Un lector espera el fin de archivo mientras otra tarea espera al lector, y la sesión nunca llega a su registro final.

El efecto externo puede existir ya. Una solicitud HTTP puede haber sido aceptada o un comando SSH puede haber cambiado un archivo remoto. Informar de esa llamada simplemente como «tiempo de espera agotado» deja al operador con la peor respuesta posible: no sabe si reintentar repetirá el cambio.

Asigna a cada helper un responsable que gestione conjuntamente cuatro cosas: el identificador del hijo, el lector de stdout, el lector de stderr y la cancelación. En una finalización normal, espera a que termine el proceso y drena los lectores hasta el fin de archivo antes de construir el resultado final. Durante una cancelación, solicita la terminación, sigue drenando ambos flujos, espera un periodo de gracia limitado y fuerza la terminación si la plataforma lo permite. Por último, espera a que terminen los lectores y registra el estado de salida que realmente observaste.

No hagas que los lectores de los flujos dependan únicamente del gestor de la solicitud. Un gestor puede desaparecer cuando un cliente se desconecta. El helper y sus lectores necesitan una propiedad que sobreviva el tiempo suficiente para terminar la limpieza y escribir el estado terminal. De lo contrario, el proceso del agente puede salir, el host puede liberar las últimas referencias a sus lectores y un hijo puede quedarse vivo con tuberías que nadie consume.

Los grupos de procesos requieren el mismo cuidado. Un envoltorio de shell puede iniciar descendientes que hereden stderr, y matar solo al envoltorio puede dejar a un descendiente manteniendo abierta la tubería. Evita el shell cuando puedas. Si necesitas uno, inícialo en un grupo de procesos contenido y define exactamente a qué descendientes llega la cancelación. Después prueba el caso en el que el envoltorio termina, pero un nieto sigue escribiendo.

## El momento de aprobación debe ignorar el volumen de registros

El momento de aprobación debe seguir el estado de la acción, no el ritmo de los diagnósticos de un helper. Si tu interfaz solo muestra la aprobación después de que un helper haya producido una transcripción previa, una fase previa ruidosa cambia el momento en que se pide al usuario que decida. La aprobación parece aleatoria y las personas aprenden a aceptar tarjetas sin entender por qué una tardó más.

Define las transiciones antes de escribir el código de la interfaz. Una llamada puede recibirse, validarse, quedar pendiente de aprobación, autorizarse, enviarse, terminar, cancelarse o fallar antes del envío. Stderr puede asociarse a una llamada, pero no debe decidir qué transición ocurre. El puente debe validar el destino y los parámetros solicitados, crear el registro de la llamada y mostrar cualquier aprobación necesaria antes de iniciar una acción que requiera aprobación.

Hay una excepción útil. Puede hacer falta un helper para descubrir qué acción tendrá lugar, por ejemplo, para resolver un nombre de configuración local en un endpoint concreto. En ese caso, trata el descubrimiento como una operación independiente que no ejecuta la acción y asígnale su propio presupuesto de salida. No escondas una acción dentro de «preparación» para después afirmar que el usuario la aprobó.

Asigna a la aprobación un plazo de reloj que no se reinicie porque llega nuevo stderr. Sigue recopilando la cola limitada mientras la tarjeta esté visible, pero no vuelvas a dibujarla con cada línea. Un aviso de reintento o un resumen de error puede aportar contexto útil si llega antes de la aprobación, pero debe aparecer como una explicación estable, no como una vista de registros en movimiento constante.

Separa el consentimiento de la actividad en tus pruebas. Una prueba de avalancha debe medir el tiempo desde una solicitud de llamada válida hasta la tarjeta de aprobación, con stderr producido tanto antes como después de ese punto. Una prueba de lector lento debe comprobar que la tarjeta sigue siendo utilizable mientras el puente drena el flujo. Una prueba de cancelación debe confirmar que cerrar la tarjeta no deja ningún proceso helper en ejecución y produce un registro terminal.

La consecuencia humana es clara: una tarjeta de aprobación debe describir una acción concreta en un momento concreto. Si la salida de registros puede retrasar, alterar o sobrevivir a ese momento, la interfaz informa de la confusión del proceso en lugar de dar el control al usuario.

## Los registros de llamadas necesitan primero los hechos del ciclo de vida

Un registro de llamada está completo cuando explica el estado de la acción, no cuando contiene cada línea emitida por un helper. La salida de registros es una prueba. Los eventos del ciclo de vida son el registro.

Escribe el intento antes del envío. Incluye un identificador de llamada estable, el identificador de sesión, el tipo de acción solicitado, la decisión de aprobación o rechazo y la información del destino que se mostró al usuario. Cuando empiece el envío, añade ese hecho. Cuando termine, añade el resultado observado: éxito, fallo remoto, fallo local, cancelación, terminación forzada o resultado desconocido debido a la pérdida del límite del proceso.

Después añade los metadatos de diagnóstico. Como mínimo, registra el total de bytes de stderr recibidos, los bytes retenidos, si hubo truncamiento, el estado de salida del helper si está disponible y si el flujo terminó correctamente. Así, una cola corta sigue siendo honesta. Quien lea el registro más tarde podrá distinguir entre «el comando imprimió esto» y «el puente conservó la última parte de lo que imprimió el comando».

La estructura de un registro puede ser así de pequeña:

```json
{
  "call_id": "c_7f2a",
  "state": "cancelled_after_dispatch",
  "stderr_bytes_seen": 184320,
  "stderr_bytes_retained": 16384,
  "stderr_truncated": true,
  "exit_status": null,
  "stream_end": "reader_completed_after_cancel"
}
```

No escribas `exit_status: 0` porque el proceso principal recibió un cuerpo de respuesta correcto. Un cuerpo de respuesta y la salida de un hijo son observaciones distintas. Tampoco escribas `state: failed` cuando la cancelación ocurrió después del envío y el lado remoto pudo haber actuado. Esta distinción parece puntillosa durante la implementación, pero puede marcar la diferencia entre una investigación segura y un reintento a ciegas a las dos de la mañana.

La guía NIST SP 800-92 Guide to Computer Security Log Management plantea una idea útil: la gestión de registros incluye su generación, transmisión, almacenamiento, análisis y eliminación, no solo la recopilación de texto. Aplica ese razonamiento a las acciones de los agentes. Si la recopilación puede impedir que una acción termine, la ruta de registro se ha convertido en parte de la ejecución. Necesita límites, estado y gestión de fallos como cualquier otra ruta de ejecución.

Sallyport proyecta sus diarios de Sessions y Activity desde un único registro de auditoría cifrado, encadenado mediante hashes y resistente a modificaciones, para que quien llama pueda distinguir la ejecución del agente de los registros de cada acción individual sin depender de la transcripción de un helper como historial.

## Limita el ruido en tres lugares

Un único límite en la parte superior de la pila deja demasiado margen para los accidentes. Establece límites en el helper, el puente y el destino de los diagnósticos. Cada límite protege un límite distinto.

Primero, haz que los helpers sean menos habladores de forma predeterminada. Pon el progreso rutinario detrás de una opción de depuración explícita, emite un único resumen para una secuencia de reintentos y evita imprimir cuerpos de solicitudes o respuestas por defecto. Un helper nunca debe escribir credenciales, encabezados de autorización ni material de claves privadas en stderr. La eliminación de datos sensibles después de la captura es una red de seguridad útil, pero no puede deshacer que un secreto ya haya aparecido en un terminal, un informe de fallo o un búfer sin límite.

Segundo, haz que el puente drene continuamente y conserve un prefijo o una cola limitados. Debe aplicar una duración máxima y una cantidad máxima de bytes retenidos para cada flujo. También debe tener un presupuesto de diagnósticos para todo el proceso. Sin este último control, cincuenta llamadas que respeten su asignación individual pueden crear juntas un pico de memoria.

Tercero, limita el destino. Si exportas los diagnósticos de una llamada a una interfaz de usuario, una respuesta del agente o un archivo, aplica allí otro límite. Un diario puede conservar campos estructurados del ciclo de vida y un resumen de la salida descartada sin almacenar una traza repetida de cada reintento.

Usa una estructura de configuración explícita. Los nombres no importan. La separación sí.

```yaml
helper_output:
  stderr_retained_per_call_bytes: 16384
  stderr_retained_process_bytes: 262144
  stderr_agent_excerpt_bytes: 4096
  shutdown_grace_seconds: 5
  retain: tail
```

Esta configuración evita un fallo común en el que un equipo establece un límite para la respuesta del agente y supone que el host está protegido. El host sigue leyendo y almacenando todos los datos antes de recortar la respuesta. El límite por llamada protege una acción. El límite del proceso protege las acciones simultáneas. El límite del extracto del agente protege el contexto que un modelo debe compartir con el resto de su trabajo.

No uses un único interruptor global llamado `quiet`. Hace más difíciles de diagnosticar los fallos en producción y anima a los desarrolladores a volver a activar registros ilimitados cuando necesitan pruebas. Mantén concisa la salida normal, ofrece un modo de depuración controlado durante un periodo breve y conserva en el registro de la llamada el hecho de que el modo de depuración estaba activo.

Evita la recomendación popular de redirigir stderr a `/dev/null`. Se recomienda porque elimina el bloqueo inmediato y mantiene ordenadas las respuestas del agente. También elimina la primera pista útil cuando un helper SSH no puede autenticarse, falla una comprobación de certificados o un comando remoto devuelve un error inesperado. Drena, limita y etiqueta el flujo.

## El truncamiento debe ser visible, no dramático

El truncamiento es seguro cuando se indica explícitamente y el sistema sigue drenando. Es inseguro cuando quien lee después no puede saber si un mensaje de error está completo, cuando el puente deja de leer y bloquea al hijo o cuando la salida descartada puede contener el único relato de una acción que no tiene otro registro.

La cola retenida debe comenzar con un marcador producido por el puente, no por el helper. Por ejemplo:

```text
[stderr truncated: kept last 16384 of 184320 bytes]
connection retry 18 failed: remote side closed the channel
```

Ese marcador forma parte del registro, no es decoración. Explica al usuario por qué la primera línea visible parece abrupta y evita que un agente trate una traza parcial como una explicación completa. Si conservas un prefijo y una cola, indica ambas cantidades de bytes. Nunca las unas silenciosamente.

Establece los límites partiendo de la concurrencia, no copiando una cifra de otro proyecto. Pregunta cuántas acciones puede tener en curso una ejecución de agente, cuántas ejecuciones acepta el host, cuánta memoria pueden consumir los diagnósticos durante un minuto problemático y cuánto texto puede revisar una persona de forma útil. La última cifra suele ser mucho menor de lo que la gente espera.

Mantén separado el resultado de la acción del extracto. Una acción HTTP correcta debe devolver su resultado estructurado previsto aunque stderr alcance el límite, salvo que el propio helper use el exceso como señal de fallo. A la inversa, un flujo stderr limpio no demuestra que una acción haya tenido éxito. Trata los diagnósticos como uno de varios campos observados.

## Las pruebas de avalancha deben acompañar a las pruebas de integración normales

Un helper que escribe demasiado en stderr no es una prueba de seguridad exótica. Es una prueba básica de fiabilidad. Las bibliotecas se vuelven más verbosas después de un cambio de versión, los clientes remotos repiten advertencias durante los reintentos y una entrada malformada puede activar un bucle de errores. Si el puente solo ve helpers silenciosos y casos felices en las pruebas, no tiene pruebas de que la gestión de procesos funcione bajo presión.

Empieza con un fixture que emita una cantidad fija de stderr, termine con un estado elegido y no realice trabajo externo. En un sistema similar a Unix, este comando crea una avalancha deliberada para un arnés de pruebas local:

```sh
yes helper-diagnostic 1>&2
```

Ejecútalo con un plazo corto. El resultado esperado no es simplemente que venza el plazo. Confirma que el puente no conserve más de la cantidad configurada, marque el truncamiento, termine el hijo, drene los flujos hasta que se cierren y escriba un evento terminal del ciclo de vida.

Después añade los casos que revelan errores de orden:

- Un helper escribe en stderr antes de esperar la aprobación.
- Un helper escribe durante la aprobación y termina inmediatamente después de que se aprueba.
- Un helper tiene éxito externamente, llena stderr y recibe una cancelación durante la limpieza.
- Un envoltorio termina mientras un descendiente mantiene abierto stderr.
- Un helper escribe secuencias de bytes no válidas y una sola línea mayor que el límite retenido.

Mide un conjunto pequeño de hechos para cada caso: bytes de diagnóstico retenidos máximos, tiempo hasta la aprobación, tiempo desde la cancelación hasta la salida del proceso, estado final y presencia del registro de llamada esperado. No te conformes con una prueba que solo compruebe que una cadena de error contiene «truncated». Esa cadena puede aparecer mientras una tarea lectora sigue bloqueada o mientras el registro final nunca llega al almacenamiento.

Ejecuta los mismos fixtures con llamadas simultáneas. Un límite por llamada puede parecer correcto de forma aislada y fallar cuando todas las llamadas alcanzan su límite al mismo tiempo. Prueba también la desconexión de un cliente. El proceso del agente puede desaparecer, pero el host debe seguir terminando o cancelando la acción de una forma definida.

La comprobación final es la verificación sin conexión de la cadena de auditoría y la comparación con el ciclo de vida esperado del fixture. La verificación puede decirte si se alteraron las entradas almacenadas. La comparación con el fixture indica si el puente escribió desde el principio las entradas que debía escribir. Necesitas ambas.

## Mantén útiles los registros sin dejar que ejecuten la acción

La regla útil es sencilla: stdout transporta mensajes del protocolo MCP, los resultados estructurados transportan los resultados de las acciones y stderr transporta diagnósticos limitados. Cuando esos canales tienen funciones distintas, el momento de aprobación, el apagado, el control de memoria y los registros de auditoría dejan de competir por el mismo texto sin estructura.

Empieza por el helper que ya te ha dado problemas: el que reintenta con demasiado ruido, se queda bloqueado después de una cancelación o imprime una respuesta remota completa cuando falla. Ponlo detrás de un lector limitado y ejecuta el fixture de avalancha hasta que el registro de la acción siga siendo veraz. Los registros silenciosos resultan agradables. Los registros limitados son control operativo.
