Cinco lições de manter o lemon-code e o app macOS em dois repositórios, com um contrato no meio e um banco SQLite que só um lado pode escrever
English summary: Five lessons from building lemon-code's CLI and native macOS app in parallel, including a startup cascade where a transient SQLite lock tripped the app's circuit breaker.
Publicado: 6 de novembro de 2026
Tempo de leitura: 8 min
Série: lemon-code em 30 dias, parte 9 de 13
Eu abria o app macOS do lemon-code e, em vez do chat, via dados de fallback e um aviso fixo na parte de baixo da tela, tapando o campo de digitação. O backend estava no ar. O problema era um lock momentâneo no SQLite, contado pelo app como se a conexão tivesse caído.
Esse efeito cascata virou a ADR 0032. Ele resume bem o que é desenvolver um CLI e um app nativo ao mesmo tempo: a maior parte dos bugs mora na fronteira entre os dois. Aqui estão as cinco lições que ficaram.
Pense num disjuntor de casa que desarma toda vez que a geladeira liga o motor. O pico é normal e passa em um segundo, mas o disjuntor entende como curto-circuito e corta a luz da casa inteira. O problema não é a geladeira. É o disjuntor sem calibragem.
O circuit breaker do app era esse disjuntor. Ele contava como falha de conexão erros RPC de domínio e respostas vazias, além das falhas de processo e transporte que realmente devem abrir o circuito.
A correção do disjuntor é classificar o erro antes de contar:
// Resumo da classificação usada pelo cliente RPC.
enum RPCFailure {
case launchFailed, noJSONResponse, requestTimedOut, rpc, emptyResult
}
func countsTowardBreaker(_ error: RPCFailure) -> Bool {
switch error {
case .launchFailed, .noJSONResponse, .requestTimedOut: return true
case .rpc, .emptyResult: return false
}
}
Erros RPC e resultados vazios continuam visíveis para o usuário. Só não contam para abrir o circuito.
Produtos gratuitos e pagos para transformar ideias em uma base que você consegue executar.
13 produtos disponíveisContinue explorando tópicos similares
O primeiro commit do app macOS do lemon-code não tinha nenhuma tela. Era o contrato com o CLI. Este post mostra por que essa ordem evitou reescrever o agente em Swift.
O lemon-code tem seis ADRs com o número 0049, todas criadas no mesmo dia. Esse erro ensina mais sobre memória de projeto com vários agentes do que qualquer ADR bem numerada.
Eu pagava API para tarefas que o Claude Code ou o Codex, já logados na minha máquina, resolveriam. A regra CLI-first do lemon-code cabe em quatro linhas de precedência.
Checklist de 47 pontos para encontrar bugs, riscos de segurança e problemas de performance antes do lançamento.
Templates testados em produção, usados por desenvolvedores. Economize semanas de setup no seu próximo projeto.
O primeiro commit do app foi o contrato com o CLI, não uma janela. Isso evitou a tentação de "só ler o SQLite direto para mostrar a lista de projetos". Cada vez que o app precisava de um dado, a pergunta era: qual método do contrato entrega isso?
As regras do repositório do app dizem: o lemon-code é o único motor de execução, o app nunca escreve no SQLite do core e os fluxos da UI precisam funcionar sem terminal. Com dois repositórios, isso é o que impede duas implementações divergentes da mesma regra de negócio.
A classificação atual conta falha ao iniciar o processo, ausência de resposta JSON e timeout da requisição. Erros RPC (incluindo mensagens de domínio ou validação) e resultado vazio não contam para abrir o circuito.
O core já usava SQLite em modo WAL com synchronous normal. Mesmo assim, conexões concorrentes ao banco de sessões ainda podiam retornar "database busy". As conexões relevantes do SqliteSessionStore usam busy_timeout de 5.000 ms, que espera até cinco segundos antes de desistir.
// conexão SQLite com espera em vez de erro imediato (ilustrativo, Bun)
import { Database } from "bun:sqlite";
const db = new Database("lemon.db");
db.exec("PRAGMA journal_mode = WAL;");
db.exec("PRAGMA synchronous = NORMAL;");
db.exec("PRAGMA busy_timeout = 5000;"); // espera até 5 s pelo lock
Notificações e avisos precisam ser fecháveis e ficar longe do campo de digitação. Isso também entrou na ADR 0032. Um aviso que tapa o input transforma um erro recuperável em app inutilizável.
Com dados do Git: o app tem 152 commits, 73 com prefixo feat e 24 com fix. O CLI tem 448 commits no total. Esses números mostram o ritmo de atividade e dão uma ideia do trabalho na fronteira; não medem qualidade.
busy_timeout.Construir CLI e app nativo juntos é, na maior parte do tempo, cuidar da fronteira. As regras que funcionaram foram simples: contrato primeiro, um motor só, disjuntor calibrado, banco que espera em vez de falhar e avisos que não atrapalham.
Na próxima parte, a série passa aos modelos de código locais no Mac.
Se o seu time mantém um core e um cliente desktop ou mobile em paralelo e os bugs estão sempre na fronteira, é um tipo de revisão de arquitetura que faço na LEMON.DEV.