Engenharia

Um escritor por working tree

Agents paralelos que compartilham um checkout não quebram em alto e bom som, eles sobrescrevem uns aos outros e perdem trabalho silenciosamente. A regra mais barata da nossa build mata a falha mais cara: um escritor que ganha sua própria tree isolada.

ASR

Apollo Space Research

Apollo Space

· 11 min de leitura

Dois agents começam a trabalhar na mesma pasta ao mesmo tempo. Um está na metade de um refactor limpo. O outro, instruído a “sair do caminho” de uma branch travada, roda um único comando inocente para resetar o diretório. O refactor, horas de trabalho bom, terminado, não-testado-mas-real, se foi. Sem erro. Sem crash. Sem stack trace. A build ainda está verde, porque a build verde é para o arquivo que sobreviveu. O outro arquivo simplesmente nunca existiu, no que diz respeito à máquina.

Ninguém fez nada errado. Essa é a armadilha.

Aqui está a regra que a fecha, e ela cabe numa linha: um escritor por working tree. O post inteiro é um argumento de por que essa frase é o seguro mais barato que você vai comprar numa build de agents paralelos, e por que a falha que ela previne é do tipo mais caro que existe, precisamente porque não faz som algum.

A falha que não se anuncia

A maioria das falhas de build é barulhenta. Um teste fica vermelho. Um tipo não bate. O compilador aponta para uma linha e te diz o que ele odeia. Você pode discutir com uma falha barulhenta, porque ela está parada na sua frente.

As falhas perigosas são as silenciosas. Uma falha silenciosa é trabalho que desaparece sem deixar marca, e a maneira mais confiável de fabricar uma é deixar dois escritores compartilharem um único checkout do código.

Imagine o setup ingênuo, porque é o que todo mundo alcança primeiro. Você tem uma frota de agents e um repositório. Você quer que eles trabalhem em paralelo, então aponta todos para a mesma pasta e os deixa ir. Parece eficiente. Uma cópia do código, várias mãos se movendo rápido. Por uma tarde até funciona.

Então duas dessas mãos alcançam o mesmo ponto. O Agent A está editando um arquivo. O Agent B, trabalhando uma tarefa diferente, decide que o diretório está num estado bagunçado e o arruma, um reset, um stash, um descarte de “lixo não commitado.” Do ponto de vista do B, o lixo não era trabalho dele, então descartá-lo é faxina. Do ponto de vista do A, o chão acabou de sumir. As edições do A estavam não commitadas, o que para um checkout compartilhado significa deletável. Elas são deletadas. O A continua, sem saber de nada, e reescreve metade do que perdeu, ou não, e a mudança vai pro ar faltando uma peça que ninguém consegue nomear.

O custo não é a hora perdida. O custo é que você não consegue ver que a hora foi perdida. Um teste vermelho te diz que algo quebrou. Sobrescrita silenciosa não te diz nada. Você descobre semanas depois, quando a feature que “foi pro ar” está faltando a parte que morava só na tree que foi limpa.

Por que as correções óbvias não corrigem

O primeiro instinto, uma vez que você foi mordido, é coordenar. Faça os agents se revezarem. Adicione um lock. Diga a eles educadamente para não tocarem nos arquivos uns dos outros. Essa é a correção ingênua, e ela falha por uma razão que vale nomear.

Coordenação assume que os agents conseguem ver uns aos outros. Eles não conseguem, não de forma confiável. Um agent trabalhando numa pasta compartilhada não tem jeito limpo de saber quais mudanças não commitadas são as dele e quais pertencem a um irmão que começou dez minutos atrás. O diretório de trabalho não carrega uma etiqueta de nome em cada edição. Então, quando um agent decide fazer faxina, ele não está sendo imprudente, ele genuinamente não consegue distinguir seu próprio rascunho do de outra pessoa. A instrução “tome cuidado” não tem em que se fixar.

O segundo instinto é commitar constantemente, para que nada fique não commitado tempo suficiente para se perder. Isso está mais perto do certo, e commitar cedo é higiene genuinamente boa, uma branch commitada e empurrada é difícil de destruir. Mas não resolve a colisão. Dois agents commitando no mesmo checkout na mesma branch agora correm para escrever a mesma história. Um faz rebase em cima do outro e o merge fica feio, ou pior, é resolvido automaticamente de um jeito que descarta um hunk. Você moveu a perda silenciosa do diretório de trabalho para o grafo de commits. O gargalo nunca desaparece. Ele apenas se move.

O terceiro instinto é o pior, porque parece o mais responsável: ter um agent supervisor serializando os escritores para que apenas um toque a pasta de cada vez. Agora você jogou fora o paralelismo que era o ponto inteiro. Você construiu um pipeline cuidadoso, correto, de arquivo único e chamou de frota.

Nenhuma dessas funciona porque todas estão tentando tornar o compartilhamento seguro. Compartilhar o diretório de trabalho é o problema. Você não o torna seguro. Você para de fazê-lo.

À esquerda, vários agents escritores compartilham um checkout do código; suas edições aterrissam no mesmo diretório de trabalho, um agent faz faxina e silenciosamente sobrescreve outro, e a mudança perdida não deixa erro. À direita, cada escritor ganha sua própria working tree isolada, então nunca dois escritores tocam os mesmos arquivos e nada pode ser silenciosamente sobrescrito.

O mecanismo: uma tree, um escritor, uma branch

A ideia-chave é simples, e o controle de versão moderno foi feito para ela. Um repositório pode ter mais de uma working tree. A história, os commits, as branches, o object store, mora num único lugar compartilhado. Mas os arquivos checados podem existir em vários diretórios separados de uma vez, cada um fixado na sua própria branch. Eles compartilham o passado e isolam o presente.

Dê a cada agent sua própria tree e a colisão fica estruturalmente impossível. Não “desencorajada.” Não “protegida.” Impossível. O Agent A edita arquivos no seu diretório; o Agent B edita arquivos num diretório completamente diferente. Nenhum consegue fazer faxina, reset ou descartar o trabalho do outro, porque o trabalho do outro não está lá para tocar. Quando o A termina, ele commita na sua própria branch e empurra. Quando o B termina, o mesmo. As histórias se encontram depois, deliberadamente, num merge, que é um evento barulhento, revisável, que faz conflitos virem à tona, exatamente o tipo de falha que você quer, porque você consegue vê-la.

Essa é a inversão. O setup ingênuo torna integração barata (todo mundo já está na mesma pasta) e isolamento caro (você tem que coordenar para ficar fora do caminho um do outro). Um-escritor-por-tree inverte: isolamento é grátis, é o padrão, embutido em onde os arquivos moram, e integração é o passo deliberado, visível, que você faz de propósito, no merge, com um revisor observando.

Coordenação é um imposto em cada tecla. Isolamento é um custo de setup único. Pague o barato.

Há uma segunda regra que cavalga junto com a primeira, e ela é igualmente carregadora de peso: nunca faça switch, reset ou descarte dentro de uma tree que você não possui. O hábito mais destrutivo num checkout compartilhado é o reflexo “sair do caminho”, limpar o diretório para começar do zero. Num mundo de trees isoladas esse reflexo está fine, porque o único trabalho que você pode limpar é o seu próprio. A disciplina não é “tome cuidado com estado compartilhado.” É “não tenha estado compartilhado com o qual tomar cuidado.”

O que custa, honestamente

Isto não é de graça, e fingir que é seria o mesmo erro de autoavaliação contra o qual avisamos em todo lugar.

Custa disco e setup. Cada tree é mais uma cópia checada dos arquivos de trabalho, e subir uma leva um momento antes de o agent poder começar. Para uma frota de escritores, isso é overhead real, um punhado de checkouts completos vivendo lado a lado em vez de um. Você também precisa de um lugar para registrá-las: um simples livro-razão de qual tree pertence a qual agent, reivindicada quando, em qual branch, para que nada fique órfão e esquecido. Uma tree que ninguém lembra de ter criado é seu próprio pequeno cemitério.

Então pondere. De um lado: o custo de N checkouts e um registro que os lista. Do outro: o custo de uma mudança silenciosamente sobrescrita que vai pro ar faltando uma peça, descoberta três semanas depois por um cliente, rastreada de volta por uma história que não tem registro da deleção porque a deleção não deixou nenhum. Um desses custos é alguns gigabytes e um script de setup. O outro é uma sessão de debug que começa com as palavras “mas o código estava definitivamente lá.”

Pagamos o disco de propósito, pela mesma razão pela qual o conselho paga por um revisor adversarial e o freio paga por um passo de confirmação. A falha cara numa build nunca foi a escrita. Foi o trabalho que sumiu sem deixar rastro e levou uma semana da vida de alguém para ser notado. Isolamento que custa alguns gigabytes é o lugar mais barato para preveni-lo.

Uma comparação de custo em duas pistas. A pista de checkout compartilhado mostra uma pequena economia inicial seguida por um custo grande e invisível: trabalho silenciosamente perdido descoberto semanas depois. A pista de tree isolada mostra um pequeno custo de setup fixo, disco extra e um registro, seguido por nenhuma perda silenciosa, porque colisões viram impossíveis.

A disciplina escala para baixo, também

Mais uma coisa que a regra compra, e é a parte que as pessoas perdem porque soa pequena demais para importar.

A mesma disciplina que deixa uma frota de agents rodar em paralelo sem comer o trabalho um do outro é a disciplina que deixa um agent se recuperar do próprio crash. Um agent trabalhando na sua própria branch commitada e empurrada pode morrer no meio da tarefa e perder quase nada, o trabalho até seu último commit é imortal, sentado na história compartilhada, esperando ser pego. Um agent trabalhando não commitado numa pasta compartilhada perde tudo no momento em que o processo morre, e pior, deixa uma bagunça pela metade que o próximo agent pode “limpar.” Isolamento mais commit-cedo não são dois hábitos. É um hábito com dois retornos: escritores paralelos não colidem, e um escritor único sobrevive à própria falha.

É por isso que esta é a primeira regra que ensinamos a um novo agent, antes de qualquer coisa sobre como escrever código bom. Você pode ser o melhor escritor da frota e ainda ser um passivo se compartilhar um checkout, porque o trabalho que você perde não é só o seu.

A virada: um cemitério é feito de decisões pequenas e razoáveis

Tire o controle de versão e o que sobra é uma velha verdade sobre times de qualquer tipo, humano ou não.

Trabalho geralmente não é destruído por um vilão. Ele é destruído por uma série de decisões pequenas e razoáveis, cada uma defensável isoladamente. Vou só limpar este diretório bagunçado. Vou resetar para começar do zero. Vou trabalhar na mesma pasta, é mais rápido. Cada uma delas é sensata por si só. Empilhadas juntas, através de uma frota se movendo rápido, elas silenciosamente somam um cemitério de branches sobrescritas e tardes perdidas, a maior fonte única de trabalho desaparecido numa build paralela, e a que não deixa evidência para trás.

A correção não é heroísmo ou agents mais cuidadosos. É uma estrutura onde o movimento destrutivo é simplesmente indisponível. Você não consegue sobrescrever o trabalho de um irmão se o trabalho do irmão não está no seu diretório. Você não consegue perder o progresso de um crash se o progresso foi commitado e empurrado um minuto atrás. A regra faz o lembrar para que nenhum agent precise.

Essa é a filosofia silenciosa embaixo de uma palavra de som barulhento como orquestração: a maior parte do valor não está em tornar agents mais inteligentes ou mais rápidos. Está em arranjá-los para que a disciplina mais barata possível torne a falha mais cara possível impossível, não menos provável, impossível. Um escritor por tree é um desses arranjos. Há outros. Todos compartilham a forma: previna a perda silenciosa no nível da estrutura, porque quando você a pegaria de qualquer outra forma, ela já se foi.


Esta é uma das regras pequenas e sem glamour sobre as quais construímos a Apollo, o tipo que nunca aparece numa demo mas silenciosamente decide se uma frota de agents é um time ou um engavetamento. Se você já foi procurar uma mudança que tinha certeza de ter feito e não achou nada lá, você já entende por que a primeira coisa que damos a um novo agent não é um prompt melhor. É um quarto só dele.

A Apollo cuida da operação repetitiva da sua empresa pro seu time não precisar.

Entre na lista de espera: acesso antecipado, preço de usuário fundador e um lugar na primeira fila enquanto a gente constrói.

Entrar na lista de espera