Cuando un subagente terminaba, se apoderaba del chat

Le pides a Hermes en Telegram que mande dos subagent a investigar algo y te vas a preparar un café. Cuando vuelves, el informe del subagent nunca llegó — pero tu conversación original ha “terminado”, y cada mensaje que envías en esa misma ventana ahora lo responde el subagent, como si algo hubiera ocupado tu asiento sin hacer ruido.
No es una impresión tuya. El hijo se apoderó de verdad del chat.
El síntoma extraño
Un subagent lanzado con delegate_task que arranca un proceso en segundo plano — o un comando en primer plano largo que se promociona al segundo plano cuando dura lo suficiente — informa de una finalización al terminar. Ese fue el momento en que todo se torció:
- Tu conversación real se cerró como
session_switch, que es un evento de user boundary; - La ruta del chat se trasladó al transcript de ejecución del propio hijo;
- Así que el resultado de la delegación del padre se descartó como terminal cuando llegó — nunca viste el informe;
- Tu siguiente mensaje en esa ventana lo respondió el hijo en lugar de tu session.
El mismo bug se reportó en Discord (DMs, hilos y canales, en macOS y Windows), Telegram (DMs y grupos), Slack, Zulip y Feishu.
Cómo una finalización se apoderó del chat
La versión corta: el sistema archivó la finalización bajo el dueño equivocado.
Un proceso en segundo plano iniciado dentro de un hijo de delegate_task registra el session id del propio hijo como su completion pin. Cuando termina, el completion resolver del gateway ve un pin vivo que apunta a una session y trata esa session como la dueña del chat. El paso siguiente es switch_session: la ruta del chat se mueve al transcript de ejecución del hijo, y la conversación real termina como user boundary. Un user boundary es terminal — en ese punto, el resultado de la delegación del padre, todavía en vuelo, se juzga perteneciente a una conversación ya cerrada y se descarta.
Dicho de otro modo: el aviso de proceso del hijo debía llegar a quien delegó el trabajo y, en su lugar, buscó al hijo al que se había delegado. Un enlace equivocado, y toda la cadena se inclinó hacia el lado incorrecto.
La corrección: una finalización pertenece a la persona, no al hijo
La regla que establece la corrección es simple: una finalización fijada (pinned) pertenece a la session de cara al usuario — la que se alcanza a través de model_config._delegate_from. La ruta del chat nunca se mueve a un hijo.
Mecánicamente:
- La propiedad se demuestra una vez antes de la entrega y luego se cede al resolver. El resolver vuelve a leer la propiedad solo si la ruta se movió después de esa prueba;
- La propiedad incierta se reintenta en lugar de adivinarse: antes del admission, un reintento no cuesta ningún intento de entrega; después del admission, la finalización vuelve a la delivery queue;
- Los user boundary siguen siendo terminales en cada salto, así que un reintento nunca puede resucitar una conversación que ya terminó.
Los avisos de proceso de los hijos también se ajustaron: ahora siguen delegation.surface_child_process_notifications (desactivado por defecto, como siempre han indicado los docs de delegación) y se entregan en el profile que posee la session, incluida la session cruda api_server de un profile servido. Un handoff de process_manage a mitad de camino hace que el aviso vuelva a ser del padre.
Por qué la corrección vive en el gateway y no en una plataforma
Cada platform adapter hace exactamente un trabajo: traducir entre su servicio y el formato común de mensajes del gateway. Qué session posee un chat se decide después, en código compartido del gateway — un único session store indexado por la routing key del chat, un único completion resolver, un único switch_session.
La finalización de un hijo recorre ese camino en todas las plataformas, así que todas las plataformas tienen el takeover, y la reparación corresponde al código compartido y no a un solo adapter. Por eso este cambio no contiene ningún parche específico de plataforma.
Qué no cambió
- Los eventos de user boundary siguen siendo terminales; su semántica no se relajó;
- Las notificaciones de procesos hijos siguen suprimidas por defecto: el resultado consolidado del hijo es lo entregable, y el comentario en vivo no lo es. Pon
delegation.surface_child_process_notifications: truepara recibirlas, con atribución de subagent; - Los resultados de delegación nunca estuvieron suprimidos — este trabajo trata de si realmente pueden entregarse;
- El cambio es una reconstrucción de rescate (salvage rebuild) de #132827, que conserva los commits de los contribuidores originales con su autoría intacta.
Estado y actualización
Esta corrección (PR #136225, etiquetada P1) se fusionó en main el 2026-10-11. La etiqueta actual, v0.21.6 del 8 de octubre, no la incluye; la próxima release sí lo hará.
Cómo verificar tras actualizar — en Discord o Telegram, delega un subagent que ejecute un comando que se convierta en proceso en segundo plano y observa qué pasa cuando termina:
- El resultado del hijo debería aterrizar en tu conversación original;
- Tu siguiente mensaje debería responderlo tu propia session, no el hijo;
- Con
delegation.surface_child_process_notificationsactivado, el aviso de proceso aparece en esa misma conversación con atribución de subagent.
Para cerrar
Este es el clásico bug de “parece que el modelo perdió la cabeza, pero en realidad es un error de contabilidad”: el aviso de finalización se dirigió al destinatario equivocado, la propiedad del chat se cedió, y cada turno posterior se fue de lado. Con esto corregido, un trabajo en segundo plano de larga duración ya no puede reemplazar la conversación que lo inició — un límite que importa sobre todo en despliegues multiplataforma y multiusuario que ejecutan varios subagent a la vez.
Lectura relacionada: orquestación de subagent en vivo cubre cómo dirigir a los hijos mientras se ejecutan, la guía de límites de session explica dónde empieza y dónde acaba una conversación, y auto-reparación de plataformas en el gateway es la otra vía de recuperación cuando una conexión de plataforma falla.