happyin.work / blog

English · Русский · Español · Deutsch · Português (Brasil) · 简体中文 · 日本語

Vários Claudes num mesmo repo. Locks, handoffs e e-mail de 1982

Autora: Anastasiia Butova, engenheira de ComfyUI e modelos de difusão, Belgrado · publicado em 2026-04-23 · atualizado em 2026-09-23

Tradução da versão em inglês: English

Eu rodo o Claude Code em paralelo em três assinaturas Pro, e meus colegas rodam as deles, muitas vezes nos mesmos arquivos no mesmo dia. Este post cobre os três problemas de coordenação que surgiram disso, na ordem em que apareceram: passar contexto de uma sessão para a seguinte, dar a uma sessão uso exclusivo de um recurso e deixar duas sessões vivas conversarem entre si. Cada uma delas acabou sendo um padrão de sistemas distribuídos com cerca de quarenta anos: um log somente de acréscimo, um arquivo de lock com heartbeat e e-mail. No fim: como os três são empacotados como mclaude, como foi o uso real e onde ele quebra.

Os handoffs vieram primeiro, quando me cansei de recontar o estado das coisas para cada conversa nova. As consolidações vieram quando projetos longos acumulavam oitenta handoffs cada. O e-mail veio quando me cansei de repassar pelo mensageiro os status do Claude dos meus colegas.

Camada 1: handoffs, "terminei, pegue daqui"

A primeira tentativa foi escrever tudo num único HANDOFF.md quando a sessão terminava. Isso quebra no momento em que existe mais de uma conversa. Quem escreve por último vence, e todos os outros são sobrescritos.

Então passei para o modelo somente de acréscimo. Cada sessão escreve o próprio arquivo, com um nome único:

.claude/handoffs/
├── 2026-04-09_14-32_373d1618_drift-validator.md
├── 2026-04-09_16-47_b858f500_dashboard-refactor.md
├── 2026-04-10_11-20_a1c2d3e4_auth-fix.md
└── INDEX.md   (append-only log)

O nome codifica a data, a hora, os oito primeiros caracteres do id da sessão e um slug da tarefa. Duas conversas não conseguem produzir o mesmo nome: mesmo no mesmo segundo, seus ids de sessão são diferentes. A corrida é impossível por construção, sem locks e sem broker.

O INDEX.md também é somente de acréscimo: uma nova linha no fim, nunca uma edição acima. Para mudar um status de ACTIVE para CLOSED, você acrescenta um novo registro em vez de editar o antigo. Um write-ahead log de banco de dados funciona exatamente assim, e pelo mesmo motivo: se um processo morre no meio de uma escrita, o log continua consistente.

Um handoff não é um diário, é um contrato com a próxima sessão. Cinco seções, uma pergunta para cada: o objetivo, o que foi feito (com caminhos, para que nada seja reconstruído), o que NÃO funcionou e por quê, o estado atual (funcionando, quebrado, bloqueado) e uma ação seguinte. Uma linha por seção, de uma sessão real de colour-checker:

  • Objetivo: "Colour checker: CNN sweep + diffusion, first results"
  • Feito: "CNN baseline median 1.99 deg, 11M params, 21 MB"
  • Não funcionou: "lr=3e-4, NaN after epoch 10-13, no gradient clipping"
  • Quebrado: "Diffusion training OOM at bs=16"
  • Próximo passo: "Inference script for diffusion + visual sheets"

O texto completo de um handoff do mesmo projeto está em Meu CLAUDE.md tem 582 linhas.

A seção que se paga é "não funcionou". Em quatro dias se acumularam 27 handoffs, e nenhum beco sem saída foi repetido, porque toda nova sessão via o aviso. Um handoff cabe em cerca de 1.500 tokens contra cerca de 100 mil de contexto bruto de sessão: cerca de 67 vezes menor, sem perder nada que importe para seguir em frente.

A escrita é automatizada. Um hook de Stop verifica quanto tempo a sessão durou e, se ela foi substancial e nada foi registrado, pergunta se deve salvar um handoff. Além disso, em sessões longas o próprio modelo oferece escrever um antes de seguir, em geral depois de uma tarefa grande ou quando o contexto está 70 a 80% cheio. Meus hooks não têm verificação de duração para isso; é decisão do próprio modelo. A leitura é uma frase no início da próxima sessão, "pegue o handoff do colour-checker", ou um link para o arquivo.

Consolidações (rollups), quando os handoffs se acumulam

Trabalhe num assunto por um ou dois meses e se acumulam dezenas de handoffs, cheios de becos sem saída já percorridos e hipóteses já refutadas. Uma sessão nova lê os últimos poucos, não olha mais fundo, e calmamente propõe uma abordagem que eu tinha refutado duas semanas antes.

A solução é consolidar os antigos. Uma consolidação resume vinte handoffs anteriores, marca a data de corte e carimba cada handoff que ela cobre com uma linha rolled_up_into: apontando para o arquivo da consolidação. Uma sessão nova lê uma consolidação mais apenas os handoffs posteriores à sua data de corte. O resto fica para o histórico, mas não entra no contexto.

A consolidação carrega um segmento rollup_ no nome do arquivo para se destacar no INDEX.md, e seu frontmatter lista o que ela cobre (o formato do README do mclaude, encurtado):

---
type: rollup
covers:
  - 2026-03-01_12-00_session-01_kickoff
  - 2026-03-02_14-30_session-02_auth-refactor
  - 2026-03-15_18-00_session-12_ci-pipeline
through: 2026-03-15 18:00
---

O corpo é um handoff comum que descreve vinte sessões em vez de uma. O padrão não é meu. O PavelMuntyan/MF0-1984 resolveu o mesmo problema num schema SQLite com uma coluna covered_until_message_id, e eu troquei a tabela por um frontmatter em Markdown. Nada de esperto: o velho "resumo dos capítulos anteriores".

Camada 2: locks, "este recurso é meu agora"

O problema seguinte apareceu com execuções de treinamento em servidores remotos. Handoffs só acrescentam, então ninguém sobrescreve ninguém. Recursos reais são diferentes. Duas sessões não conseguem refatorar o mesmo módulo ao mesmo tempo (conflito de merge), não conseguem fazer deploy em staging ao mesmo tempo (o segundo sobrescreve o primeiro), não conseguem treinar numa mesma GPU ao mesmo tempo (o segundo leva um OOM). Uma delas precisa segurar o recurso de forma exclusiva.

Um arquivo por recurso, nomeado a partir dele: auth-middleware.lock, staging-deploy.lock, gpu-host-a_gpu2.lock. Dentro: quem pegou, quando e para quê. Uma sessão nova lista a pasta, vê o que está ocupado e escolhe algo livre sem me perguntar.

O truque está em como o arquivo é criado. os.open(path, O_CREAT | O_EXCL) é atômico: se o arquivo já existe, a chamada falha. Dois processos não conseguem criar o mesmo arquivo ao mesmo tempo, porque o kernel garante isso. É a mesma primitiva por trás de /var/lock e dos arquivos pid desde os anos 1980.

Um lock sem heartbeat sobrevive só até a primeira queda: a sessão morre, o lock fica, e todo mundo espera para sempre. Então, na convenção de arquivos, uma sessão ativa renova o timestamp a cada 30 a 60 minutos, e um lock cujo heartbeat tem mais de cerca de quatro horas possivelmente está morto. Possivelmente não é com certeza. Antes de assumir, verifico o próprio recurso: ps via SSH, nvidia-smi na GPU, a fila de deploy, git status no módulo, o que quer que diga se ele está de fato ocupado ou só nunca foi liberado. Só então eu assumo. O Chubby e o ZooKeeper também verificam de fora antes de uma tomada de controle, pelo mesmo motivo.

Ao lado de cada lock fica um metadado em JSON: slug da tarefa, id da sessão, hora em que foi tomado, descrição, lista de arquivos. Uma sessão nova lê .claude/locks/ e vê em um segundo que auth-middleware está com ani para uma refatoração, na segunda hora, e que a GPU 2 está treinando embeddings. A pergunta "o que está acontecendo aqui?" deixa de surgir.

Minha primeira intuição foi um hook de PreToolUse que analisa comandos e marca recursos como ocupados automaticamente: um comando toca no recurso X, então X está ocupado. Beco sem saída. Os comandos são variados demais, e um parser de regex vira uma máquina de falsos positivos. A ordem certa foi a inversa: primeiro convenção e arquivos, automação depois, e só para padrões que se repetem de fato.

Camada 3: e-mail, "Artem, olha o meu código"

Handoffs e consolidações carregam contexto pelo tempo. Locks arbitram recursos. Uma terceira necessidade não cabe em nenhum dos dois: manter duas sessões vivas em sincronia.

Um dia típico: pedi que a sessão de uma pessoa do time refatorasse o auth-middleware e fui escrever um plugin no meu próprio chat. Uma hora depois quis saber como estava e se tinha travado. A resposta antiga era: abro um mensageiro e escrevo para a pessoa colega, ela olha a própria conversa e copia o texto de volta. Três repasses por uma pessoa, todas as noites, em vários projetos.

O que eu precisava eram mensagens endereçadas entre sessões que talvez nunca estejam online ao mesmo tempo, com entrega garantida: se a mensagem não é lida agora, é lida na próxima sessão. Pensei nisso e percebi que estava descrevendo e-mail.

.claude/mailbox/
├── ani/
│   ├── inbox/
│   ├── sent/
│   └── archive/
├── artem/
│   └── ...
└── all/          (broadcast)

Cada mensagem é um arquivo Markdown separado com frontmatter YAML (from, to, type, subject, message_id, in_reply_to, status) e a carta embaixo. Um caso simples: a sessão da ani envia ao Artem uma pergunta com o assunto "Re: cmake libsodium" e o corpo "Did you change the libsodium linking in CMakeLists? My build fails on target_link_libraries". Uma hora depois, o Artem abre o Claude Code na própria máquina, um hook de UserPromptSubmit verifica a caixa de entrada antes do primeiro prompt, vê a mensagem não lida e a coloca no contexto. Ele responde na hora, e a resposta cai na minha caixa de entrada.

As threads seguem in_reply_to, como na RFC 822. Os recibos de entrega são arquivos separados. Uma mensagem para to: * vai para all/, que todos verificam. Para evitar duplicatas, o hook guarda em .watcher_state.json quais mensagens já mostrou, então nada entra no contexto duas vezes. Sem polling, sem broker, sem daemon em segundo plano: tudo se apoia em um hook que dispara no momento certo. Cada recurso corresponde um a um ao e-mail clássico:

E-mail (RFC 822, 1982) Caixa de correio de agentes
Para / Cc endereçar uma sessão específica
De / Responder-para endereço de retorno para a resposta
Assunto triagem sem ler o corpo
In-Reply-To threads de mensagens
Data o quanto o pedido é recente
Lida / não lida viu ou ainda não viu
Caixa de entrada por usuário fila por destinatário
Pasta de enviados trilha de auditoria de quem enviou
Recibo de entrega confirmação sem "você viu?"

mclaude: as três camadas numa só ferramenta

Empacotei as camadas como uma ferramenta de código aberto, o mclaude: Python, sem dependências no núcleo, 193 testes, versão 0.6.0. Como descrevi na época, ele tinha seis camadas, três básicas e três acrescentadas depois:

Camada O que faz Linhas de código
Locks reserva atômica de recurso + heartbeat 469
Handoffs resumos por sessão, INDEX somente de acréscimo 371
Memory Graph armazenamento hierárquico de decisões e armadilhas ~400
Identity Registry nomes para sessões (ani, artem, nastya) 246
Messages + Active Mail mensagens entre sessões, com threads 400 + 236
Code Indexer scanner de AST → code-map.md + llms.txt novo na v0.6.0

O indexador percorre o projeto e escreve dois arquivos para o agente: code-map.md, um mapa de módulos, classes e funções, e llms.txt, um índice compacto para carregamento rápido. Uma sessão nova não gasta os primeiros minutos relendo código, porque o mapa já está no repositório, ao lado dos handoffs.

Em cima disso vêm cinco hooks, quatro nos eventos do Claude Code (SessionStart, PreToolUse, UserPromptSubmit, Stop) e um guard de pre-commit do git que recusa um commit que toque num arquivo travado por outra pessoa. Um servidor MCP com mais de 20 ferramentas devolve JSON estruturado diretamente ao Claude Code, sem parsear a saída da CLI.

Assim fica um lock visto pela linha de comando, do README do mclaude:

$ mclaude lock claim --slug fix-auth-middleware-race \
    --description "Race condition when two requests write the same session key" \
    --files src/auth/middleware.py src/auth/session.py
[lock] claimed fix-auth-middleware-race

# a second session tries the same slug
$ mclaude lock claim --slug fix-auth-middleware-race --description "same bug I guess"
[lock] fix-auth-middleware-race already held
  by session: 665a44027f424db7
  doing:      Race condition when two requests write the same session key

A regra de projeto: o hub é um acelerador, não uma dependência. Tudo funciona com arquivos. Com o hub no ar, as mensagens viajam por WebSocket em tempo real. Com o hub fora, a bridge escreve na mesma .claude/messages/inbox/, no mesmo formato, e o destinatário vê exatamente a mesma coisa depois de um git pull. Se o pacote mclaude for removido, os arquivos em .claude/ continuam legíveis: Markdown para a narrativa, JSON para os metadados.

mclaude demo --no-pause leva cerca de 30 segundos. Ele roda duas sessões simuladas, ani e vasya, pelas camadas e imprime um diagrama Mermaid do que aconteceu, que renderiza como está num pull request ou num post.

A lacuna no ecossistema

Depois fui ver quem mais resolvia isso. Isolamento tem muitas respostas: git worktrees (parallel-cc), sandboxes (Kmux), uma porta separada por agente com variáveis de ambiente encaminhadas, e os Agent Teams da própria Anthropic, com uma fila de tarefas e reservas. Todos resolvem "as sessões não atrapalham umas às outras" garantindo que não haja estado compartilhado.

A coordenação de estado compartilhado estava quase vazia. Só o claude_code_agent_farm, de Dicklesworthstone, usa locks em arquivo, e isso é orquestração para 20+ agentes em tmux, não para muitas conversas num mesmo projeto. O próprio rastreador do Claude Code mostra a lacuna pelo outro lado: a #19364, um pedido de arquivo de lock de sessão, nunca foi implementada, e a #29217 documenta o .claude.json sendo corrompido por escritas concorrentes de várias sessões.

O ecossistema está maduro em evitar estado compartilhado e quase vazio em coordená-lo.

Nada disso é novo. Timeouts de heartbeat, arquivos de lock nomeados a partir do recurso, um log ao qual você só acrescenta, uma checagem externa antes de tomar um lock obsoleto, um arquivo por recurso em vez de uma tabela compartilhada: o Chubby do Google, o ZooKeeper e todo sistema de coordenação dos últimos quarenta anos se apoiam nessas mesmas coisas. Agentes de IA são só mais um tipo de processo num sistema distribuído que já entendemos.

A RFC 822 serve pelo mesmo motivo. O e-mail resolve exatamente esse problema abstrato: entrega assíncrona entre processos que podem não estar ativos ao mesmo tempo, com threads e confirmação de entrega. Cada geração de sistemas distribuídos redescobre que o e-mail estava certo. Nos anos 2010 eram filas de mensagens, RabbitMQ e Kafka. Em 2026 são agentes de IA.

Uso real

Três pessoas, um projeto de plugin em C++, seis sessões paralelas do Claude Code, cada pessoa com a própria parte: arquitetura, o pipeline de inferência, a interface.

Um agente decidiu mudar a derivação de chave no módulo de embaralhamento e mandou um broadcast por mailbox/all/. Outro agente viu isso no início da sua próxima sessão, antes de escrever qualquer código, e levou a mudança em conta. Sem a caixa de correio, teria escrito código contra um formato incompatível: um conflito de merge, na melhor das hipóteses, dados corrompidos na pior.

Handoffs entre assinaturas, terminando numa e continuando em outra, economizaram cerca de quatro horas que de outra forma iriam para reconstruir o contexto.

Onde quebra, e o que não sei

  • Escala. Testado com 3 a 6 sessões paralelas, 2 ou 3 pessoas, um projeto C++, além de muitas sessões num site. Não medi 20+ sessões com 10 agentes. Suspeito que a caixa de entrada quebre primeiro: a deduplicação por .watcher_state.json é linear no número de mensagens e vai ficar lenta com centenas delas.
  • Várias máquinas sem o hub. O núcleo em arquivos sincroniza por um repositório compartilhado, então o outro lado vê uma mensagem depois de git pull: minutos, não segundos. O hub dá segundos via WebSocket, mas é mais um processo para manter no ar, e não o testei em produção.
  • Corridas de lock em sistemas de arquivos de rede. O_CREAT | O_EXCL é atômico num sistema de arquivos local. Em NFS ou SMB a garantia depende da implementação, então quem puser .claude/locks/ ali pode encontrar uma corrida.
  • A tomada de um lock obsoleto é semiautomática. As verificações com nvidia-smi, ps e git status foram escritas para uma pessoa. Automatizá-las exige um handler que saiba checar cada tipo de recurso.
  • E-mail não acorda uma sessão. Só com o hook, as mensagens aparecem em UserPromptSubmit, e apenas quando a sessão ativa está digitando algo. Não há entrega push de verdade (veja a atualização abaixo).
  • Uma pessoa ainda está no circuito. Se seis agentes decidirem ao mesmo tempo que o auth-middleware precisa ser reescrito, eles vão negociar por e-mail, mas quem fica com a tarefa continua sendo uma decisão humana. O mclaude dá as ferramentas para negociar, não orquestração autônoma.

O que mudou depois de abril

Atualização, setembro de 2026. Três correções e duas adições, todas verificadas em 23 de setembro de 2026.

  • Instalação. pip install mclaude, como escrevi em abril, não funciona: o pacote não está publicado no PyPI, que retorna 404 para ele. Instale a partir de um clone do repositório, como abaixo.
  • Issue #19364. Eu a descrevi como um pedido aberto. Um bot de manutenção já a tinha fechado como not planned em 28 de fevereiro de 2026, antes de este post ser publicado. Ela nunca foi implementada, que é o ponto da seção, mas não estava aberta.
  • Issue #29217. Citei "315 backups quebrados em sete dias". Esse número não está na thread como ela está hoje. O relato original conta 165 backups corrompidos em cerca de 40 minutos, e um comentarista contou pelo menos 423 corrupções em sete dias. A issue foi fechada por inatividade em 25 de maio de 2026.
  • Sétima camada. O README do mclaude também descreve heartbeats de sessão baseados em arquivos, que este post não cobre, e o pacote aplica locks mais rígidos do que a convenção manual acima: um heartbeat a cada 30 segundos, e um lock conta como obsoleto depois de 3 minutos de silêncio.
  • E-mail durante a conversa. O primeiro plano, do fim do original em russo, foi entregue no mesmo dia. O pull request #5, mesclado às 17:22 UTC de 23 de abril, algumas horas depois da publicação do post, acrescenta um script para a ferramenta Monitor do Claude Code: ele consulta a caixa de entrada a cada 30 segundos e entrega cada nova mensagem à sessão em andamento, sem que ninguém digite um prompt. Então a limitação "e-mail não acorda uma sessão" acima vale só para a configuração apenas com hook.

A branch principal não tem commits desde essa mesclagem. O repositório MF0-1984, de onde veio a ideia de consolidação, agora retorna 404.

Experimentando

git clone https://github.com/AnastasiyaW/mclaude.git
cd mclaude
pip install -e .
mclaude hooks install --apply
mclaude demo --no-pause

Licença MIT, Python 3.9+. Os princípios por trás dele estão escritos à parte como princípio 18, coordenação de múltiplas sessões, no repositório de configuração descrito na página codex-claude-code-config. A página da ferramenta no site é mclaude. Por que confio nos handoffs do meu próprio repositório, e não no histórico de sessões do próprio app desktop, está em 45 MB de sessões do Claude Code que você nunca olha.

O original em russo foi publicado no Habr.

Mais artigos

Mais neste site

Anastasiia Butova - ML engineer, Belgrade, Serbia. Email [email protected] · LinkedIn · GitHub · Telegram · Hugging Face