Operação
Parte do Runbook operacional. Comece pela triagem do índice.
Incidente de custo
Agentes gastam tokens a cada turno, e um agente preso num laço gasta rápido. Esta seção é para o momento em que o custo por hora dispara e alguém precisa decidir o que cortar.
Sinal
O alerta Custo por hora acima do limite dispara quando o gasto projetado passa de USD 5/hora em qualquer projeto:
max(sum by (project) (rate(brabo_llm_cost_micros_total[10m])) * 3600 / 1000000)
O alerta é um aviso, não um freio. O freio de verdade são os
budgets do domínio, e eles agem por projeto/sessão, nunca globalmente
(RN-019).
1. Qual projeto, qual agente
Grafana → painel Brabo · visão executiva → painel de custo por projeto. Ou direto do banco, que também te dá o agente e o modelo:
select s.project_id,
tu.actor_id as agente,
tu.model_name,
count(*) as chamadas,
sum(tu.cost_micros) / 1e6 as usd,
round(avg(tu.latency_ms)) as latencia_media_ms
from token_usage tu
join sessions s on s.id = tu.session_id
where tu.created_at > now() - interval '1 hour'
group by 1, 2, 3
order by usd desc
limit 20;
Duas leituras mudam a ação:
- Um agente dominando a lista com muitas chamadas curtas quer dizer laço: o ToolLoop repetindo a mesma ferramenta sem convergir.
- Poucas chamadas e custo alto quer dizer modelo caro em trabalho barato — binding errado, não laço.
2. Confira o orçamento
select b.project_id, b.session_id,
b.limit_micros / 1e6 as limite_usd,
b.spent_micros / 1e6 as gasto_usd,
round(100.0 * b.spent_micros / nullif(b.limit_micros, 0)) as pct,
b.policy
from budgets b
order by pct desc nulls last;
A policy decide o comportamento no teto:
block— a chamada é recusada. É o default e o que você quer em produção.allow— o teto vira mero registro; o gasto continua. Um projeto emallowgastando muito não para sozinho. Confira isto antes de qualquer outra coisa: é a causa mais comum de "o orçamento não segurou".
3. Corte o gasto
Em ordem de reversibilidade, do mais brando ao mais drástico.
a) Tire os agentes da autonomia automática. Eles param de agir sozinhos e voltam a exigir aprovação por ação, sem perder contexto:
update agent_autonomy
set mode = 'require_approval', updated_at = now()
where project_id = '<projeto>'
and mode = 'auto_approve';
agent_autonomy.mode é o enum permission_policy
(auto_approve | require_approval | deny) — não há valor manual,
e uma versão anterior deste passo o usava e falhava na hora. O
and mode = 'auto_approve' é deliberado: sem ele, o update
transformaria toda linha deny do projeto em require_approval, afrouxando
exatamente o que alguém tinha fechado. As linhas "*" (modo automático, RN-153) são
cobertas pelo mesmo update.
O que este passo não corta: um padrão em allow no
permissions.json do projeto ainda auto-aprova o que casa — decide() lê
o arquivo depois de agent_autonomy, e o arquivo pode subir a decisão de volta
para auto_approve (apps/api/src/domain/actions/decide.ts, decide).
Se o gasto vem de comandos que o arquivo permite, vá para (c).
Os blocos SQL desta seção são rodados contra o schema migrado por
apps/api/test/runbook/sql-do-incidente-de-custo.spec.ts, que também
confere que (a) muda auto_approve e deixa deny em paz — um
valor inválido ou uma coluna renomeada aqui reprova a suíte da api, não o
incidente.
b) Troque o binding de modelo por um local. O Ollama custa zero; a qualidade cai, o gasto para na hora:
-- veja o binding vigente e o escopo que o resolve
select scope, scope_id, model_id from model_bindings where scope_id = '<projeto>';
Depois aponte o binding do projeto para um modelo local pela tela de
configurações (o escopo mais específico vence: sessão > agente > projeto >
workspace — RN-020).
c) Baixe o teto e garanta que é block. Faz o próprio domínio
recusar as próximas chamadas:
update budgets
set policy = 'block',
limit_micros = least(limit_micros, spent_micros + 1000000) -- +1 USD
where project_id = '<projeto>';
d) Feche as sessões do projeto. Último recurso: interrompe trabalho em
andamento. Use a transição normal (closing), nunca kill no pod
— matar o pod não fecha uma sessão, só cria uma órfã
(veja Rollout do engine).
4. Depois
- O Psicólogo já tem a evidência. Se a causa foi um laço de agente, a análise dele aponta o agente alvo e a Anamnese pode propor um patch de instrução. Corrigir a instrução é o que impede de acontecer de novo; mexer no orçamento só compra tempo.
- Confira que o alerta chegou. As regras moram no Grafana, não no Prometheus: se o Grafana estava fora do ar, não houve aviso, e o silêncio não queria dizer saúde.
O que esta seção não conserta
Custo já incorrido. Metering é registro, não reembolso: o token_usage
conta o que foi gasto, e nada aqui devolve dinheiro do
provider. A única prevenção real é policy = 'block' com um
teto sensato antes do incidente.
Observabilidade
Como acompanhar uma sessão, achar custo, e diagnosticar quando não há dados. Decisões no ADR 0026.
Observabilidade local, sem cluster
Tudo abaixo desta subseção supõe o cluster de pé. Para métricas e logs sem subir Kubernetes, há um overlay de Compose (ADR 0070):
node scripts/dev/preflight.mjs && docker compose -f docker/docker-compose.yml -f docker/docker-compose.observability.yml --env-file .env up -d && node scripts/dev/observabilidade-pronta.mjs # sobe a stack de dev + Prometheus, Loki, Alloy e Grafana
docker compose -f docker/docker-compose.yml -f docker/docker-compose.observability.yml --env-file .env rm -sf loki alloy prometheus grafana # derruba só os quatro, deixando os apps de pé
O comando termina conferindo o que subiu — se diz ok em toda
linha, o painel tem dados; se reclama, diz qual peça
faltou.
Verificação: essa checagem final é scripts/dev/observabilidade-pronta.mjs,
e é uma autoverificação, não uma prova — ela só roda quando alguém sobe o
overlay, e não tem spec nem workflow. Uma mudança que quebra o
overlay aparece da próxima vez que alguém rodar o overlay de observabilidade (comando em Observabilidade local).
| ferramenta | endereço | serve para |
|---|---|---|
| Grafana | http://localhost:3001 | painéis e logs, sem login |
| Logs | http://localhost:3001/d/brabo-logs | um serviço por vez, ou os três juntos |
| Prometheus | http://localhost:9090 | conferir um alvo e séries cruas |
| Loki | http://localhost:3100 | consulta direta (o caminho normal é o Grafana) |
Os painéis são os MESMOS do cluster — o Compose monta
deploy/k8s/observability/dashboards/ direto, e os UIDs dos datasources
(brabo-prometheus, brabo-loki) são os mesmos. Um painel novo vale
nos dois ambientes sem cópia.
O que este overlay não faz: tracing. Sem um OpenTelemetry
Collector no meio não há Tempo, e isso é decisão, não
descuido (ADR 0026,
decisão 9). O trace_id ainda vai para o log e basta para
cruzar api e engine à mão.
Três coisas que confundem as pessoas, e vale saber antes de abrir uma issue:
- Um painel de custo/tokens vazio é esperado num banco novo. Essas
métricas têm rótulos (
project,provider), e noprom-clientuma métrica rotulada não existe antes da primeira observação. Uma chamada de LLM e a série aparece. - Só
api,engineewebvão para o Loki. Postgres e Ollama ficam de fora por ruído; a própria stack de observabilidade fica de fora porque, no mesmo projeto Compose, ela ingeriria o próprio log em laço. - O nível
OUTROé a linha de continuação de árvore dopino-pretty, que não tem nível próprio. Não é log perdido.
Onde as coisas estão
| ferramenta | endereço local | serve para |
|---|---|---|
| Grafana | http://localhost:3001 | painéis, traces, logs, alertas |
| Prometheus | kubectl -n monitoring port-forward svc/prometheus-server 9090:80 |
conferir um alvo e séries cruas |
| Tempo | datasource do Grafana | traces |
| Loki | datasource do Grafana | logs |
Dois painéis na pasta Brabo: visão executiva (custo/hora e tokens/min por projeto, sessões ativas, decisões de ação) e visão operacional (fila do Oban por estado, latência p50/p95 de LLM por provider, tarefas bloqueadas, sessões por réplica).
Acompanhando uma sessão da raiz até uma chamada de ferramenta
-
Pegue o
trace_idda sessão. É o campo do meio dotraceparentguardado emsessions.trace_parent:# 00-<trace_id>-<span_id>-01 curl -sS -H "Authorization: Bearer $TOKEN" \ http://localhost:3000/projects/$PROJ/sessions/$SESS | jq -r .traceParent -
No Grafana → Explore → datasource Tempo → aba TraceQL, cole o
trace_id. A árvore volta comsession.create(api) na raiz e, abaixo dele,agent.turn→tool.call/llm.turn/gate.scanner(engine). -
A partir de um span, o botão Logs for this span salta para as linhas do Loki com aquele
trace_id. O caminho inverso — de uma linha de log para o trace — é o link TraceID que aparece no detalhe da linha. -
Custo daquela sessão: o painel visão executiva filtra por projeto. Para um valor exato por sessão, a fonte é o banco (
token_usage.cost_micros), não a métrica — a métrica é agregada por projeto e provider de propósito, para não criar uma série por sessão.
Quando não há trace no Tempo
O nome desta seção mudou junto com o comportamento (ADR 0035), e a
distinção é o primeiro passo do diagnóstico: um span é sempre criado, em
qualquer ambiente. O que OTEL_EXPORTER_OTLP_ENDPOINT controla é a
EXPORTAÇÃO. Então "não vejo trace no Grafana" e "não há trace" são
problemas diferentes.
Antes de qualquer coisa, veja de que lado está a falha — o log responde sozinho:
kubectl -n brabo logs -l app.kubernetes.io/name=api --tail=20 | grep -o '"trace_id":"[^"]*"' | head
- Há
trace_idno log, nenhum trace no Tempo → o problema é a exportação: siga de 1 a 4 abaixo. - Nenhum
trace_idno log → o problema é de contexto, e é mais raro: ou ostartTracing()da api não rodou (vejaapps/api/src/tracing-boot.ts— ele tem de ser o primeiro import domain.ts), ou oEngine.Telemetry.Otel.setup/0não foi chamado antes da árvore de supervisão.
Em ordem, do mais ao menos provável:
1. A variável não está definida. Sem OTEL_EXPORTER_OTLP_ENDPOINT o
span é criado e descartado no fim, então há trace_id no
log e nada no Tempo. Em desenvolvimento isso é esperado (sem collector);
num cluster, é defeito.
kubectl -n brabo exec deploy/api -- printenv OTEL_EXPORTER_OTLP_ENDPOINT
kubectl -n brabo exec deploy/engine -- printenv OTEL_EXPORTER_OTLP_ENDPOINT
2. A NetworkPolicy está bloqueando. Esta é a falha mais silenciosa de todas: os spans são criados, o envio falha, e todo o resto fica verde.
kubectl -n brabo get networkpolicy allow-otlp-egress
kubectl -n brabo logs -l app.kubernetes.io/name=engine --tail=50 | grep -i "error exporting"
3. Protocolo errado. O exporter do Elixir fala HTTP/protobuf (4318),
não gRPC. Apontá-lo para a 4317 dá socket_closed_remotely em todo
lote.
4. O Collector não está recebendo nada.
kubectl -n monitoring logs deploy/otel-collector-opentelemetry-collector --tail=30
Quando um painel está vazio
Quase sempre um nome de métrica. Os nomes são referenciados por string em três lugares que não se enxergam: os painéis, as regras de alerta e este runbook. Confira contra o que o serviço de fato expõe:
curl -sS http://localhost:3000/metrics | grep '^brabo_' | cut -d'{' -f1 | sort -u
kubectl -n brabo exec deploy/engine -- wget -qO- http://127.0.0.1:4000/metrics | grep -E '^(brabo|oban)_'
E se o serviço a expõe mas o Prometheus não a tem, o problema é a coleta:
kubectl -n monitoring port-forward svc/prometheus-server 9090:80
# depois: http://localhost:9090/targets — os jobs são `brabo-api` e `brabo-engine`
Quando não há log no Loki
O Alloy é um DaemonSet e lê /var/log/pods do nó, filtrando
pelo namespace brabo.
kubectl -n monitoring logs -l app.kubernetes.io/name=alloy --tail=30 | grep -i error
Avisos de tailer stopped ... pods not found são normais depois de um
rollout — o Alloy continua perseguindo pods que já foram removidos.
Os apps não conseguem falar com o Loki direto, e isso é intencional:
allow-otlp-egress só abre 4317/4318. Para consultar de fora:
kubectl -n monitoring port-forward svc/loki 3100:3100
curl -sS -G http://localhost:3100/loki/api/v1/query_range \
--data-urlencode '{app="api"} | json | trace_id != ""' --data 'limit=5'
Alertas
Provisionados e visíveis em Alerting → Alert rules (pasta Brabo), a partir de
deploy/k8s/observability/alerts/brabo-alerts.yaml. As regras levam os
títulos em português — é isso que você procura no Grafana:
| alerta (título no Grafana) | severidade | dispara depois de | o que investigar |
|---|---|---|---|
| Fila do Oban crescendo sem consumo | critical |
10 min | nenhuma réplica do engine Ready; pool do Postgres esgotado; um worker preso num job |
| Sessão presa em closing | warning |
15 min | o drain não terminou, ou a transição para closed falhou |
| Custo por hora acima do limite | warning |
5 min | qual projeto e qual agente; o orçamento do domínio continua sendo o controle duro |
| Backup do Postgres atrasado — último backup bom com mais de 26 h | critical |
na hora | o CronJob não rodou, ou rodou e falhou — brabo_backup_last_status distingue os dois; veja Restore. "Nunca teve backup" (-1) não o dispara |
| Última execução do backup falhou | warning |
na hora | um backup bom de ontem ainda pode existir, e é por isso que este passa despercebido por dias; a causa está em backup_runs.error_message |
São regras do Grafana, não do Prometheus (desvio registrado no ADR 0026): elas deixam de ser avaliadas se o Grafana cair. Não há Alertmanager nem destino de notificação configurado.
Limites conhecidos
- Nenhum trace de agente real foi observado de ponta a ponta: verificar isso exige um LLM configurado (Ollama ou uma chave de API). O mecanismo foi validado emitindo spans direto no trace da sessão.
- O web não exporta spans próprios — ele gera o
traceparente o manda no cabeçalho, e a api o adota como pai. Os logs do navegador vão para o console, não para o Loki. - Retenção curta: Tempo 24h, Loki 24h, Prometheus 2h. É um ambiente local.
Registro de gates
Os gates do fluxo são declarados em apps/api/config/gates.yml
(ADR 0054). Duas coisas
operacionais valem saber.
O arquivo viaja dentro da imagem. O docker/api/Dockerfile.prod o copia
nos dois estágios, do mesmo jeito que faz com as migrations, e o
.dockerignore o reinclui explicitamente — toda a docs/ é ignorada, e
este é o único arquivo de lá que é dado de produção, não
documentação. Em runtime ele fica em /app/apps/api/config/gates.yml; o loader
sobe a partir de __dirname até achá-lo, sem variável de
ambiente.
Um arquivo ilegível não derruba a api. O carregamento é preguiçoso:
quem chama GET /internal/gates recebe o erro, e o resto do
processo segue. Se a rota devolve erro, confira que o arquivo
entrou:
kubectl -n brabo exec deploy/api -- cat /app/apps/api/config/gates.yml | head -5
Vazio ou ausente quer dizer que a imagem foi construída sem ele — provavelmente um
.dockerignore mexido, ou um build a partir de um contexto sem docs/.
O registro viaja; o que ele aponta, não. Um gate teste/ci
nomeia arquivos sob apps/api/test/, scripts/ci/ e .github/, e nenhum
deles está na imagem. Até isto ser separado, o loader exigia
esses arquivos em runtime e a rota respondia 500 em toda instalação
— medido na v6.1.0 como doze 5xx em cerca de 55 minutos, com
RegistroDeGatesInvalido listando os onze alvos como "não existe".
Reproduza a falha antiga com:
docker exec <api> node -e "require('/app/infrastructure/gates/gate-registry.loader').carregarRegistro()"
Numa imagem corrigida isso não imprime nada. Se lança
RegistroDeGatesInvalido nomeando um .spec.ts ou um workflow, a imagem é
de antes da correção (RN-070); se
lança nomeando o conteúdo de um gate (um gate block sem script, um
gate active sem evidência), o próprio registro está errado e o
repositório é onde corrigir.
A tela não mostra esta falha: o PrGateTimeline cai para a
esteira completa quando GET /gates falha (RN-084),
então o único sinal era o log da api. O docker/smoke.sh agora chama as duas
rotas que servem o registro contra a imagem de produção, GET /gates
com o bearer do usuário e GET /internal/gates com o service token.
Uma função confere o corpo das duas, e reprova no 500, numa
lista vazia e num registro sem merge-protegida. É isso que impede
a falha de voltar em silêncio. O token chega ao curl pelo
stdin (--config -), nunca pelo argv, então não aparece no ps nem no
log do CI.
Para ver o registro como a api o vê, já validado:
kubectl -n brabo exec deploy/api -- \
curl -sH "x-brabo-service-token: $BRABO_SERVICE_TOKEN" localhost:3000/internal/gates
A medição de passagem NÃO roda em produção: é
pnpm --filter api validacao:gates, a partir do repositório, contra o
banco. Veja
docs/explanation/gates.md.
Ambiente de inferência
Quando o agente responde vazio, truncado, extremamente devagar, ou "esquece"
as próprias instruções, o problema quase nunca está no código de domínio —
está aqui. As cinco primeiras causas foram achadas em nove execuções seguidas da
demo de gates e estão registradas no
ADR 0020; todas as variáveis estão
expostas no docker-compose.yml.
| variável | sintoma quando errada |
|---|---|
| GPU | o serviço ollama sem dispositivo reservado deixa a GPU parada e roda 100% na CPU: um prompt de ~7.000 tokens leva ~50s só para a ingestão. O override é opcional (docker-compose.gpu.yml, docker compose -f docker/docker-compose.yml -f docker/docker-compose.gpu.yml --env-file .env up), mantido fora do compose principal porque sem nvidia-container-toolkit no host a reserva faz o serviço não subir |
OLLAMA_CONTEXT_LENGTH |
o default de 4096 trunca em silêncio um prompt feito para 128k. O agente perde as próprias instruções e começa a imitar o schema das ferramentas, que é o que sobra no fim do contexto |
OLLAMA_MAX_LOADED_MODELS |
com OLLAMA_KEEP_ALIVE alto, os modelos se acumulam: 15,2 GB de pesos residentes numa máquina de 15 GB, e o agente responde vazio por falta de memória |
OLLAMA_REQUEST_TIMEOUT_MS |
timeout curto demais para um modelo grande num prompt longo |
OLLAMA_MODE (bootstrap de dev, RN-461, ADR 0114) |
preso em host depois que o Ollama nativo na máquina do desenvolvedor foi desinstalado ou parado — todo turno de LLM local falha com ECONNREFUSED contra http://host.docker.internal:<porta>, e o serviço ollama do compose nunca sobe para cobrir (ele fica atrás de profiles: ["local-llm"], e é esta variável que mantém esse profile desligado). Correção: Docker › Reconfigurar Ollama no pnpm bootstrap, que limpa OLLAMA_MODE/OLLAMA_HOST do .env e força a detecção a perguntar de novo no próximo Create/Reset total |
NINE_ROUTER_BASE_URL (RN-909, ADR 0222) |
o provider 9router falha com ECONNREFUSED (origem infra) e o teste de conexão da credencial recusa. O padrão http://host.docker.internal:20128/v1 só resolve dentro do container: a api da instalação e a de produção alcançam o host pelo extra_hosts: host.docker.internal:host-gateway; rodando no host (smoke), aponte para http://localhost:20128/v1. Com o 9Router parado o erro é o mesmo, então confira primeiro se ele escuta na porta 20128 |
START_OUTBOX_DRAIN / START_ANAMNESE |
o Psicólogo e a Anamnese consomem turnos de LLM em paralelo com os agentes de execução e derrubam a conexão do dev no meio do ciclo |
TOOL_LOOP_MAX_ITERATIONS* |
um teto BAIXO demais e o agente para sem entregar, com iteration limit reached e origem model — o que engana, porque o modelo não fez julgamento errado nenhum, nem chegou a julgar. O teto é por TIPO (RN-085): 8 para agentes conversacionais, 60 para o dev agent e o QA. Antes de subi-lo, confira se o agente TEM token_budget_micros; sem ele o teto é a única guarda de custo que existe |
TERMINAL_OUTPUT_MAX_BYTES |
subi-lo demais traz de volta o modo de falha que o teto existe para evitar: a saída de cada comando fica no histórico do laço e viaja em TODO turno seguinte. Não é questão de janela de contexto: a maior chamada bem-sucedida da execução que revelou isto primeiro tinha só 28.993 tokens de entrada (RN-074) |
API_JSON_BODY_LIMIT (api) / TRANSPORT_MAX_BODY_BYTES (engine) |
o 413 request entity too large no gate de QA/SecOps tinha a causa na própria api do Brabo, nunca no provider — o Express nunca teve limite de corpo configurado e valia o default de 100 KB, contra os 8 MB que o Phoenix aceita na perna engine→api mais pesada (POST .../llm-turn, que reenvia o histórico inteiro a cada iteração). API_JSON_BODY_LIMIT (default 10 MB) fecha essa ponta; TRANSPORT_MAX_BODY_BYTES (default 8 MiB) é o teto que a compactação de contexto do engine respeita POR CIMA da janela do modelo, para disparar antes de o corpo estourar o limite do HTTP (RN-412, ADR 0098) |
Se o modelo do 9router chama Read ou Bash (ferramentas que o Brabo não tem) na conta OAuth do Claude, a causa é a camuflagem do OAuth do 9Router (RN-966), não o agente: o 9Router renomeia as ferramentas e acrescenta falsas do Claude Code, e nenhuma configuração a desliga. Use API key do Claude ou outro provider.
Cuidado — a guarda não limpa a fila.
START_ANAMNESE=falsebloqueia enfileiramentos novos, não os antigos. Já houve 20AnamneseWorkers acumulados emexecutingde execuções anteriores, que rodam no próximo boot independentemente da guarda. A fila precisa ser expurgada, não só ter a guarda desligada.
Ligar a Anamnese ou o Psicólogo: duas variáveis, não uma. Os dois foram pausados em 2026-08-10 por decisão de produto, não por bug. O Psicólogo ainda está; a Anamnese foi religada pelo dono em 2026-10-01 (RN-680) —
ANAMNESE_ENABLEDvolta ao defaulttrueno código e nos três composes, e uma rodada sem sujeito elegível não faz chamada de LLM e diz por quê no log do engine (sem_sujeito_elegivel). Pausá-la de novo éANAMNESE_ENABLED=false.ANAMNESE_ENABLED/PSYCHOLOGIST_ENABLEDsão as flags de produto (uma rodada NOVA pode acontecer?);START_ANAMNESEé a chave de boot (o tick periódico é sequer agendado?). A Anamnese periódica precisa das duas emtrue; o Psicólogo não tem chave de boot, porque o gatilho automático dele é o fechamento de sessão.Até a RN-540 as duas flags
*_ENABLEDnão estavam mapeadas no serviçoenginede nenhum dos composes, e odocker composenão repassa o ambiente do host — então defini-las no.envnão fazia nada, em silêncio, enquanto três lugares no código prometiam que a pausa era reversível. Agora estão mapeadas, com o default do próprio código (falsenos dois arquivos na época;ANAMNESE_ENABLEDétruedesde a RN-680). O compose de instalação (docker/docker-compose.install.yml) era o terceiro arquivo e ficou fora dessa correção; desde a AT-202 ele mapeia as mesmas flags com os mesmos defaults, escripts/ci/flags-do-engine-no-compose.spec.tsconfere os três arquivos e reprova o build da próxima flag booleana que não estiver mapeada, ou que levar default diferente do código (START_OUTBOX_DRAINeSTART_ANAMNESEsãofalseno compose de produção de propósito, e o spec declara essa divergência pelo nome). O compose de instalação levava o mesmofalse— copiado da produção junto com o motivo da produção ("ele existe para validar as imagens") — e desde a AT-219 repete otruedo código. Era um defeito da instalação, não uma escolha de custo:START_OUTBOX_DRAINé a única coisa que drena a outbox da api para o engine, então numa instalação de uma máquina uma sessão fechada nunca parava os processos dela e um dev agent esperando uma aprovação, um gate ou o container nunca era acordado. Nenhuma das chaves gasta tokens com as flags de produto desligadas: comANAMNESE_ENABLED=falseokickoff/0da Anamnese não agenda job nenhum, e o drain só enfileira o Psicólogo comPSYCHOLOGIST_ENABLED=true. Numa instalação, a Anamnese está, portanto, ligada ou desligada só porANAMNESE_ENABLED(defaulttruedesde a RN-680). No Kubernetes não havia nada a corrigir — um Deployment/ConfigMap não intercepta nada, e obrabo-confignunca carregou estas variáveis. As duas flags são lidas no boot: mude-as edocker compose up -d engine.
Um gate semântico num modelo pequeno
O QA é o papel que cabe pior num 7B local: o julgamento varia entre
execuções, o que faz da demo um critério de aceite executável e não
um teste de regressão. Para torná-la confiável, aponte DEMO_QA_MODEL para um
modelo de API — o binding por agente (escopo agent, que vence project)
existe exatamente para isso.
A máquina de gates em si não varia: ordem imutável, retorno na mesma
branch, teto de correções, vereditos como artefato e um terminal
awaiting_user são todos verificados por ExUnit
(RN-014, RN-015).