Quando um subagente terminava, ele tomava conta do chat

Você pede ao Hermes no Telegram que mande dois subagent pesquisar alguma coisa e vai fazer um café. Quando volta, o relatório do subagent nunca chegou — mas a sua conversa original “terminou”, e toda mensagem que você envia nessa mesma janela agora é respondida pelo subagent, como se algo tivesse silenciosamente tomado o seu lugar.
Não é imaginação sua. O filho de fato tomou conta do chat.
O sintoma estranho
Um subagent lançado com delegate_task que inicia um processo em segundo plano — ou um comando longo em primeiro plano que é promovido para segundo plano quando roda tempo suficiente — reporta uma conclusão ao terminar. Foi aí que as coisas deram errado:
- Sua conversa real foi encerrada como
session_switch, que é um evento de user boundary; - A rota do chat foi movida para o transcript de execução do próprio filho;
- Assim, o resultado da delegação do pai foi descartado como terminal quando chegou — você nunca viu o relatório;
- Sua mensagem seguinte naquela janela foi respondida pelo filho em vez da sua session.
O mesmo bug foi reportado no Discord (DMs, threads e canais, no macOS e no Windows), Telegram (DMs e grupos), Slack, Zulip e Feishu.
Como uma conclusão tomou conta do chat
A versão curta: o sistema arquivou a conclusão sob o dono errado.
Um processo em segundo plano iniciado dentro de um filho de delegate_task registra o próprio session id do filho como seu completion pin. Quando ele termina, o completion resolver do gateway vê um pin vivo apontando para uma session e trata essa session como dona do chat. O próximo passo é switch_session: a rota do chat se move para o transcript de execução do filho, e a conversa real termina como user boundary. Um user boundary é terminal — nesse ponto, o resultado da delegação do pai, ainda em voo, é julgado como pertencente a uma conversa encerrada e é descartado.
Dito de outra forma: o aviso de processo do filho deveria chegar a quem delegou o trabalho e, em vez disso, procurou o filho a quem o trabalho foi delegado. Um elo errado, e toda a cadeia pendeu para o lado errado.
A correção: uma conclusão pertence à pessoa, não ao filho
A regra que a correção estabelece é simples: uma conclusão fixada (pinned) pertence à session voltada ao humano — a que se alcança via model_config._delegate_from. A rota do chat nunca se move para um filho.
Mecanicamente:
- A propriedade é provada uma vez antes da entrega e depois repassada ao resolver. O resolver só relê a propriedade se a rota se moveu depois dessa prova;
- Propriedade incerta é retentada em vez de adivinhada: antes do admission, uma retentativa não custa nenhuma tentativa de entrega; depois do admission, a conclusão volta para a delivery queue;
- User boundaries continuam terminais em cada salto, então uma retentativa nunca pode ressuscitar uma conversa encerrada.
Os avisos de processo dos filhos também foram ajustados: agora seguem delegation.surface_child_process_notifications (desligado por padrão, como os docs de delegação sempre disseram) e são entregues no profile que é dono da session, incluindo a session bruta api_server de um profile servido. Um handoff de process_manage no meio do caminho faz o aviso voltar a ser do pai.
Por que a correção mora no gateway, e não em uma plataforma
Cada platform adapter faz exatamente um trabalho: traduzir entre o seu serviço e o formato comum de mensagens do gateway. Qual session é dona de um chat se decide depois, em código compartilhado do gateway — um único session store indexado pela routing key do chat, um único completion resolver, um único switch_session.
A conclusão de um filho percorre esse caminho em todas as plataformas, então todas as plataformas têm a tomada de controle, e o reparo pertence ao código compartilhado e não a um adapter isolado. É por isso que esta mudança não contém nenhum patch específico de plataforma.
O que não mudou
- Eventos de user boundary continuam terminais; a semântica deles não foi relaxada;
- As notificações de processo dos filhos continuam suprimidas por padrão: o resultado consolidado do filho é o entregável, e o comentário em execução não é. Ative
delegation.surface_child_process_notifications: truepara recebê-las, com atribuição de subagent; - Os resultados de delegação em si nunca foram suprimidos — este trabalho é sobre se eles conseguem de fato ser entregues;
- A mudança é um salvage rebuild do #132827, mantendo os commits dos contribuidores originais com a autoria intacta.
Status e atualização
Esta correção (PR #136225, rotulada P1) foi mesclada na main em 2026-10-11. A tag atual, v0.21.6 de 8 de outubro, não a contém; a próxima release conterá.
Como verificar depois de atualizar — no Discord ou no Telegram, delegue um subagent que execute um comando que vire um processo em segundo plano e observe o que acontece quando ele termina:
- O resultado do filho deve chegar na sua conversa original;
- Sua mensagem seguinte deve ser respondida pela sua própria session, não pelo filho;
- Com
delegation.surface_child_process_notificationsativado, o aviso de processo aparece nessa mesma conversa com atribuição de subagent.
Para encerrar
Este é o clássico bug do tipo “parece que o modelo perdeu o juízo, mas na verdade é um erro de contabilidade”: o aviso de conclusão foi endereçado ao destinatário errado, a propriedade do chat foi repassada, e cada turno seguinte saiu dos trilhos. Com a correção, um job em segundo plano de longa duração não pode mais substituir a conversa que o iniciou — um limite que importa sobretudo em implantações multiplataforma e multiusuário rodando vários subagent ao mesmo tempo.
Leituras relacionadas: orquestração de subagent ao vivo cobre como conduzir os filhos enquanto rodam, o guia de limites de session explica onde uma conversa começa e termina, e auto-recuperação de plataformas do gateway é o outro caminho de recuperação quando uma conexão de plataforma dá errado.