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: true para 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_notifications ativado, 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.