Renomear um profile do Hermes deixava um fantasma


Você renomeou o profile do seu bot do Telegram de mybot para assistant. O hermes profile rename mybot assistant terminou sem reclamar, o diretório saiu de ~/.hermes/profiles/mybot para ~/.hermes/profiles/assistant, o alias do shell foi junto e o bot continuou respondendo no grupo. Dias depois você abre uma conversa antiga e ela ainda está arquivada sob mybot, um profile que não existe mais, enquanto a tabela de routing do gateway mantém chaves como agent:mybot:*. A outra face do mesmo bug: você apaga um profile e, dias depois, o diretório dele reaparece no disco com um arquivo de cache dentro. O PR #112653 corrige os dois casos (#111926 e #112592).

Três palavras antes de tudo. Um profile é um agente Hermes autocontido — config, credenciais, memória e sessões próprias — que vive em ~/.hermes/profiles/<nome>. Uma routing key é a string com que o gateway decide a qual profile e a qual sessão pertence uma mensagem recebida, no formato agent:<profile>:<sessão>. Um gateway multiplexado é um único processo de gateway que atende vários profiles ao mesmo tempo e mantém o índice de routing na memória.

O rename antigo movia o diretório, não a identidade

O código antigo fazia a metade do trabalho, a do sistema de arquivos: renomear o diretório do profile e renomear o alias do shell. A metade do banco de dados nunca era tocada. E isso dói porque muitas linhas do state store do Hermes usam o próprio nome do profile como chave. Depois de um rename, essas linhas continuavam dizendo mybot, e mybot já não existia no disco: apontavam para um fantasma.

Nada explode de imediato, vaza aos poucos. As sessões antigas deixam de casar, uma linha de heartbeat segue reportando um profile que sumiu, as delivery obligations (os registros que dizem “esta mensagem ainda não foi entregue e precisa de nova tentativa”) ficam sob o nome morto, e a propriedade dos tópicos do Telegram deixa de bater. Quem roda vários profiles atrás de um mesmo gateway percebe primeiro; os limites desse arranjo estão no nosso guia de profiles multi-instância.

Por que as tabelas de identidade usam o nome do profile como chave

Para o banco de dados, o nome do profile é a identidade. Mudar o nome significa que o agente troca de identidade no state store, então a migração não é mover arquivos: é um rename mais uma reescrita de todas as linhas indexadas por esse nome. O PR #112653 lista exatamente quais linhas entram:

  • sessions.session_key, sessions.profile_name, sessions.origin_json
  • gateway_heartbeats.profile
  • delivery_obligations
  • as tabelas de tópicos do Telegram
  • o índice gateway_routing (a chave e o JSON que ela carrega embutido)

Existe uma segunda fronteira que vale conhecer — o que segue o profile e o que segue a sessão — e o guia de fronteiras de sessão percorre ela inteira.

O que migra agora: uma transação e um índice em memória

O lado do banco de dados é SessionDB.rekey_profile_state(old, new) em hermes_state_gateway.py: uma única transação que reescreve para o nome novo todas as linhas indexadas por nome de profile. Antes disso ela checa colisões — se o nome novo já tem linhas que colidiriam, ela recusa em vez de escrever uma bagunça. É idempotente e pula as tabelas que ainda não foram criadas.

Só reescrever o banco não basta, porém. Um multiplexer em execução despeja periodicamente suas _entries da memória de volta no banco, o que sobrescreveria a reescrita. É disso que cuida SessionStore.rekey_profile_routing(old, new) em gateway/session.py: ele reescreve as chaves do índice de routing em memória — as entradas e origin.profile — e persiste o resultado. Por isso um gateway vivo comanda a migração: o índice mora no processo dele, então ele tem a palavra final.

A corrente funciona assim. rename_profile chama _migrate_profile_identity em hermes_cli/profile_identity.py. Se houver um multiplexer vivo, ela delega ao verbo migrate-profile-identity do control socket; sem nenhum gateway segurando o store, a CLI faz a reescrita durável por conta própria. Em qualquer caso a migração nunca é fatal para o rename: o pior cenário é um aviso, um profile renomeado e linhas de identidade que continuam no nome antigo.

Renomear um profile com segurança, passo a passo

hermes profile list              # veja o que você vai renomear
hermes profile rename mybot assistant
hermes profile show assistant    # confirme o nome novo e as informações dele
hermes profile list              # a lista deve mostrar assistant, não mybot

Leia a saída do rename. Se ela avisar que o gateway vivo não conseguiu migrar a identidade de sessão, não deixe passar: resolva com o comando da próxima seção. Se você não sabe se o seu gateway é um multiplexer, siga o que a saída mandar: sem aviso, a migração já foi aplicada; com aviso, reinicie o gateway e repita. Todas as flags desses comandos estão na referência do comando hermes profile, e a página de instalação cobre como colocar o Hermes na máquina.

Se a migração não rodou: repita com migrate-identity

O rename não pode ser repetido — profiles/mybot já era. Para isso existe um comando próprio:

hermes profile migrate-identity <old-name> <new-name>

Uma sessão real se parece com isto:

hermes profile rename mybot assistant
# ⚠ Profile was renamed, but the live gateway could not migrate session identity (…).
#   Restart the gateway, then run:
#     hermes profile migrate-identity mybot assistant

hermes profile migrate-identity mybot assistant
# ✓ Session/routing identity migrated: mybot → assistant

O primeiro bloco avisa que o profile foi renomeado, mas que o gateway vivo não conseguiu migrar a identidade de sessão, e oferece duas saídas: reiniciar o gateway (ele recarrega o índice de routing do banco na inicialização, então a migração entra) ou pará-lo — sem nenhum processo segurando o store, o comando faz a reescrita durável por conta própria.

Propriedades que vale conhecer:

  • A migração é guiada pelas linhas que ainda carregam o nome antigo, então profiles/mybot não precisa existir; só o nome novo é verificado.
  • É idempotente: repetir uma migração concluída dá certo, sem nada a reescrever.
  • Sai com código diferente de zero quando um gateway vivo recusa a migração ou quando um banco recusa a reescrita — uma colisão de routing, um lock, ou um dos dois bancos falhando enquanto o outro tem sucesso. O erro nomeia o banco e o problema.

Renomear o profile default só define um nome de exibição

Talvez você queira um nome mais bonito para o profile default. Aqui está a armadilha: o ID interno do profile default é sempre default, porque ~/.hermes é a raiz da instalação e não dá para renomeá-la de verdade. O que esse comando define é um nome de exibição (display name):

hermes profile rename default Harumesu   # Unicode sem problema: 小助手

Esse nome aparece no hermes profile list e no show, no comando de chat /profile, no dashboard e no app de desktop, incluindo o roster do Bot Mode. É puramente apresentação: -p default, nomes de serviço, cron jobs e qualquer outra referência continuam usando o ID canônico default. Ele fica guardado como display_name em ~/.hermes/profile.yaml — apague essa linha para reverter. Profiles nomeados também podem carregar um display_name (e ele sobrevive a um rename de verdade), mas neles o rename renomeia o profile em si.

Um profile apagado deixa uma lápide: nada pode ressuscitá-lo

Apagar se parece com isto:

hermes profile delete coder

Ele para o gateway, remove o serviço systemd/launchd, remove o alias do comando e apaga todos os dados do profile. Você precisa digitar o nome do profile para confirmar; --yes pula essa confirmação. O profile default não pode ser apagado (ele é o próprio ~/.hermes) — para remover tudo, use hermes uninstall.

O que costumava dar errado depois: uma thread que ainda carregava o caminho do home morto podia fazer uma escrita atômica tardia, e o mkdir(parents=True) antes dela reconstruía a árvore de diretórios. O seu coder apagado voltava ao disco com pouco mais que um arquivo de cache.

O #112592 fecha esse caminho. utils._atomic_write e atomic_roundtrip_yaml_update / atomic_roundtrip_yaml_save agora criam seus diretórios pais via mkdir_under_hermes_home, o guarda de lápides, então qualquer escrita atômica de JSON, texto, bytes ou YAML recusa com FileNotFoundError um home nomeado que tenha lápide e não deixa nada para trás. Os mkdir(parents=True) pré-escrita espalhados pelos chamadores passaram pelo mesmo guarda: o cache de modelos, o ETag do models.dev, o lifecycle sentinel, o cache de segredos, o OAuth de MCP, o memory store e a varredura mecânica dos demais pontos de pré-escrita; dois locais que exigem um mode= explícito mantêm o mkdir atrás de assert_named_profile_home_live.

O guarda não é indiscriminado. Se o caminho alvo não tiver um ancestral profiles/<nome> demonstrável, ele não faz nada: o home padrão, os diretórios ocultos .staging e caminhos que só contêm profiles por acaso continuam escrevendo normalmente.

Cuidados e resumo

  • Leia a saída do rename. Uma migração falha nunca faz o rename falhar, ela só avisa — reinicie o gateway e rode hermes profile migrate-identity <antigo> <novo>.
  • O retry é idempotente, então rodar na dúvida não custa nada.
  • Um multiplexer vivo tem prioridade, porque mantém o índice de routing na memória; se você preferir parar o gateway, o comando faz a reescrita durável por conta própria.
  • O profile default nunca é renomeado de verdade — você apenas define um nome de exibição, e o ID interno segue default.
  • Depois de apagar, deixe apagado. O guarda de lápides impede que uma escrita tardia transforme um profile nomeado apagado em um diretório no disco.