v0.40.0 — uma conversa só, e o app diz se o modelo realmente enxerga
This is the release note as published, not a rewrite of it. Release notes are published in the language they were written in.
A v0.39.0 tirou coisas da tela. Esta devolve três — anexar, ditar, aprender — e fecha uma porta que estava aberta desde sempre.
Havia duas portas para o mesmo agente, e a sem guarda era a mais permissiva
O Chat e o turno de código conversavam com o mesmo agente sobre as mesmas ferramentas. Só um dos dois era montado com região de escrita, denylist de postura e registro de contaminação. Ou seja: a porta sem proteção era também a mais permissiva, e nada em nenhuma das duas telas dizia por qual você tinha entrado.
O ataque é concreto. Você pede ao Chat "lê essa página e me resume". A página traz uma instrução plantada — "escreva isto no arquivo tal". No turno de código o registro marca a execução como contaminada e as ferramentas de escrita passam a recusar. No Chat não havia registro, então ela escrevia.
Apagar a tela não fecharia isso: o endpoint compatível com OpenAI monta a partir da mesma fábrica. A porta sumiria, a janela ficaria.
Agora existe uma conversa só. Ela absorveu o que só o Chat tinha — markdown com realce, rolagem que solta quando você rola para cima, role="log", Parar, fusão, leitura de memória — e o destino do Chat saiu. O backend fica: ChatSession e /api/chat/stream continuam servindo a mensageria, o endpoint compatível e os benchmarks. Remover uma porta não é remover o cômodo.
E CHIMERA_GUARD_CHAT monta o chat como o turno de código monta. Ele sai desligado, porque essa fábrica é compartilhada com a mensageria e ligá-la por padrão tiraria shell de bots que gente já roda. Isso faz do padrão uma exposição real — e a condição sob a qual ele sai é o app dizer isso. A frase de postura agora reporta uma conversa desprotegida como uma que ainda escreve depois de ler conteúdo não confiável, e nomeia o interruptor que muda.
Um turno fundido respondia sobre arquivos que nunca abriu
"Fundir este turno" entrega o backend do agente para o motor de fusão, e o motor descarta os schemas de ferramenta — loga em debug e segue. Um painel de modelos não tem com o que chamar uma ferramenta, então o turno terminava em um passo sem tocar em nada e respondia a partir do prompt. Você pedia "leia o config.yaml e diga o que está errado" e recebia uma análise do arquivo que nunca foi lido, com a autoridade de três modelos concordando.
De fora, isso era indistinguível de um turno que legitimamente não precisou de ferramenta: os dois reportam zero chamadas.
O conserto óbvio seria rotear o botão pelo RoutedBackend, que desvia turnos com ferramenta para modelo único. Seria a outra forma de mentir: a fusão viraria um no-op silencioso em toda conversa que toca arquivo. Então o turno continua existindo e se declara — o done carrega fused, a resposta é marcada onde a pessoa está lendo, e o tooltip passou a falar da consequência em vez de vender o ganho. O teste que fixa isso não existia.
Anexar, ditar, e a pergunta de saber se o modelo enxerga
Três formas de colocar algo numa mensagem que não é digitar, com uma decisão em comum: um anexo é conteúdo que você escolheu mandar mas não escreveu. Escolher não é atestar, então o texto extraído é sanitizado e cercado igual a uma página web — um PDF carrega injeção de prompt tão bem quanto um site.
Imagens exigiram abrir o caminho multimodal. O gateway sempre soube montar a requisição; o Agent.run recebia uma string e nada mais, então não havia por onde passar. Elas viajam só no turno atual — são codificadas em base64 na requisição, e carregá-las no histórico reenviaria a mesma foto a cada turno, paga de novo toda vez.
Documentos não precisam de visão, e é esse o ponto. Um PDF vira texto na chegada e é dobrado na mensagem, então funciona com qualquer modelo — inclusive os que não enxergam. A conversão acontece no upload, para que um arquivo ilegível falhe enquanto você ainda olha o botão de anexar, e não no meio de um turno que você está pagando.
Os arquivos vão para a casa do app, nunca para o seu workspace.
Ditar passa pela mesma ferramenta que o agente usa — faster-whisper local quando instalado, a API caso contrário. Uma transcrição que falha mostra o motivo em vez de colar error: ... no compositor como se fosse o que você falou.
E o app agora pergunta se o modelo enxerga — em três estados. A fonte é a tabela de modelos do LiteLLM, e uma tabela tem uma borda: ela devolve False tanto para um modelo que sabe ser cego quanto para um que nunca viu. Testada contra o slug configurado deste projeto, essa borda não é hipótese — ela reporta False para um modelo que enxerga perfeitamente. Juntar "desconhecido" com "não vê" diria a quem roda um modelo de visão recém-lançado que ele é cego, e a pessoa iria desligar algo que funciona. Errar com confiança é pior do que não dizer nada, então desconhecido é uma resposta própria e a interface diz "não sabemos".
O app passa a aprender com o próprio trabalho
Era a superfície que mais trabalhava e menos aprendia. O chimera solve num terminal acumulava memória de longo prazo, criava skills e fazia crescer um playbook; toda execução iniciada pelo app jogava tudo fora.
A pré-condição precisou vir antes: o registro de contaminação só chegava ao agente quando havia um thread_id, junto do checkpointer. Esse portão está certo para o checkpointer — uma pausa sem identidade durável é uma execução para a qual ninguém volta — e errado para o registro, que só observa. Sem ele, "esta execução foi contaminada?" não responde "não sei", responde False. Inofensivo enquanto nada era gravado; o oposto disso quando a execução passa a escrever na memória, porque um fato aprendido lendo conteúdo não confiável seria guardado como limpo, e sobreviveria em toda conversa futura.
O que é gravado continua sendo um fato curto e chaveado por tarefa, depois do verify passar. Nunca transcrição, nunca conteúdo de arquivo. Coleta de trajetórias fica desligada e há teste dizendo isso: ela grava cada passo num dataset para exportação, o que é ato deliberado com custo de disco, não aprendizado.
Uma instalação empacotada não tinha onde guardar os próprios dados
Settings.home tem caminho relativo. Certo para a CLI — os dados ficam ao lado do projeto onde você rodou — e errado para um app empacotado, que não tem diretório de trabalho com significado. O lançador não fixava nada, então uma instalação nova guardava memória, recibos e sessões onde o atalho apontasse: o diretório de instalação sob Program Files (sem permissão de escrita), ou uma pasta diferente por atalho — o mesmo app mostrando dois históricos conforme como foi aberto.
Agora fixo no diretório de dados do sistema operacional.
Explicitando, porque a pergunta que motivou isso merece resposta direta: nenhum dado de usuário vai no instalador. O .chimera/ fica na raiz do repositório, está no gitignore, e o PyInstaller coleta dados do pacote chimera, não da raiz — verificado buscando runs.jsonl, memory* e *.db dentro do bundle construído. A release também é construída num clone limpo da CI, onde esse diretório nem existe.
Também nesta release
Código virou o primeiro destino — é onde o trabalho começa. A frase de postura espera um projeto ser escolhido: ela nomeia um diretório que o agente pode editar, e sem projeto nomeava o diretório de lançamento do app, anunciando permissão de escrita numa pasta que ninguém tinha visto.
O painel "Instruir e executar" saiu: era uma segunda implementação do lançador da tela Trabalho, privada, com menos recursos — e a menor era a que as pessoas encontravam primeiro. O campo de verificação que ele pedia não protegia nada: caixa vazia já mandava null, e null já fazia o servidor ler o projeto.
Duas capacidades foram removidas de verdade, e nomeá-las é o ponto: Aceitar e Descartar numa execução terminada (um revert de git limitado aos arquivos daquela execução), e acompanhar os diffs de uma execução ao vivo. A aba Git do Trabalho ainda descarta, mas sem esse escopo; os diffs de uma execução se leem no recibo dela.
Nada medido mudou: 37/37 provado no snapshot de maturidade, ganho interno de 48% → 71% com n=100.