O Caminho das Pedras

O motor v2 volta sozinho no OpenWork: a política do Den força a migração, e o teto real é a 0.18.58

2026-10-29

O chat está no motor v1, as atualizações automáticas estão desligadas, e o v2 volta assim mesmo. Reverter no seletor dura minutos; reiniciar o app devolve o v2; e às vezes a troca acontece no meio de um turno, sem clique e sem aviso.

A cena desmonta uma premissa que parecia sólida: a de que, com o app parado numa versão e os updates desligados, a escolha do motor é local. Ela é — até uma versão específica. Dali em diante o desktop consulta uma política remota a cada 60 segundos, e essa política tem prioridade sobre o seletor, sobre o arquivo de estado e sobre a variável de ambiente que deveria desligar a migração.

Correção ao artigo anterior. A análise da reversão para 0.18.59 + motor v1 recomendava a 0.18.59 como teto. Para o free isso continua certo — até a 0.18.59 os gratuitos servem. Mas a 0.18.59 é exatamente a primeira versão que obedece à política de engine, e por isso ela não segura o motor. O teto que atende aos dois requisitos é a 0.18.58.


1. O que quebrou

O sintoma tem três caras, e todas apontam para a mesma origem:

O rastro fica em %APPDATA%\openwork\engine-v2-preview.json:

{
  "enabled": true,
  "chatRouting": true,
  "engineOrigin": "forced",
  "engineForced": true,
  "engineChangedAt": "2026-10-29T10:32:37.009Z",
  "historyCopyResult": { "imported": 12, "skipped": 89, "total": 101 }
}

engineOrigin: "forced" não é valor que o seletor escreva. Ele aparece quando ninguém escolheu — e é o único campo que separa "eu troquei de motor" de "trocaram por mim".

No localStorage do app, a chave openwork.engine-migration.analytics.v1 guarda o horário de cada virada (migration_started e migration_completed). Foi ela que datou as duas migrações da mesma madrugada, em 00:27 e 07:25 de Brasília, num app que estava parado e não tinha se atualizado.


2. A armadilha central: são três alavancas, não duas

O diagnóstico anterior separava duas: a versão do app decide o free, o motor decide o arquivamento. Faltava a terceira — a que decide o motor sem passar pelo usuário.

Sintoma O que de fato controla Por que a tentativa óbvia não segura
Modelos gratuitos respondem 403 FreeTierError Versão do app (a partir da 0.18.60) Reverter o motor não toca na versão
O botão de arquivar some e a escrita devolve 501 Motor (o v2 não arquiva) Trocar a versão não muda o motor que atende
O v2 volta sozinho, com updates desligados Política remota, lida a cada 60 s pelo desktop — e a versão do app decide se ele obedece Desligar atualização automática é outro canal; esta ordem chega em runtime

A consequência prática é que "segurar a versão" e "escolher o motor" deixaram de ser a mesma decisão. Um pin de versão protege contra o free quebrar; ele não protege contra a migração de engine, porque a migração não é um update.


3. Como o mecanismo funciona por dentro

O automation-runner do desktop consulta o Den a cada 60 s (RUNNER_WORK_POLL_MS = 60_000) e, na resposta de trabalho, recebe um campo enginePolicy: { upgrade: boolean }. A descrição no schema é explícita:

"Live org policy is returned even when inventory has not changed."

Quando vem true, o desktop chama POST /experimental/engine-v2-preview/force no servidor local, com o token de host. O servidor então:

  1. agenda o upgrade em background;
  2. espera o engine ficar ocioso;
  3. copia o histórico para o banco do v2, com backup;
  4. grava chatRouting: true e engineOrigin: "forced".

Reverter à mão só é protegido contra o caminho de revert automático. O próximo poll, no máximo 60 segundos depois, rearma o upgrade — e é isso que produz a impressão de "volta sozinho durante o uso".

O corte é a versão do app, e é estreito

Comparando o fonte do repositório different-ai/openwork por tag (v0.18.54 a v0.18.63):

Versão enginePolicy no runner · setForcePolicy no servidor Migração de engine é padrão local?
≤ 0.18.58 ausentes não
0.18.59 · 0.18.60 presentes não (OPENWORK_ENGINE_AUTO_UPGRADE === "1")
≥ 0.18.61 presentes sim (!== "0") — e a política do Den sobrepõe o desligamento

O arquivo apps/desktop/electron/engine-force-policy.mjs e a função setForcePolicy simplesmente não existem na 0.18.58. Ali o app ignora a política porque não tem com o que aplicá-la. É o mesmo motivo pelo qual, na 0.18.61 em diante, OPENWORK_ENGINE_AUTO_UPGRADE=0 não segura nada: a política chega depois.

O release note da 0.18.60 (22/10) anuncia a mudança pelo nome: "Older installs move to OpenCode v2 in the background."

Por que editar o arquivo de estado não resolve

Com o app aberto, o servidor mantém chatRouting em memória e regrava o arquivo em qualquer persistState. Escrever false com ele rodando se perde no próximo ciclo. A edição só vale com o app fechado, e antes de trocar a versão.


4. Passo a passo verificável

O objetivo é a única combinação em que o free serve e o motor é escolha local: app ≤ 0.18.58, updates desligados, chat no v1.

Passo 1 — Confirmar quem está mandando

Get-Content "$env:APPDATA\openwork\engine-v2-preview.json" -Raw

chatRouting: true com engineOrigin: "forced" é política remota agindo, não escolha sua. A conferência ao vivo está no servidor local do workspace — a porta fica em %APPDATA%\openwork\openwork-server-state.json, e o token de cliente em %APPDATA%\com.differentai.openwork\openwork-server-tokens.json (são segredos de sessão: usar em memória, nunca gravar em arquivo de projeto):

GET http://127.0.0.1:<porta>/experimental/engine-v2-preview/status

A resposta traz running, version e chatRouting com a mesma origem forced quando a política está aplicada.

Passo 2 — Fechar o app e travar o estado

Só com o OpenWork fechado:

{
  "enabled": true,
  "chatRouting": false,
  "engineOrigin": "manual"
}

enabled: true sozinho não troca o motor; o que manda é chatRouting.

Passo 3 — Instalar uma versão ≤ 0.18.58

Antes de procurar o instalador fora da máquina, veja o cache do updater: o installer.exe da raiz de %LOCALAPPDATA%\@openworkdesktop-updater\ é a última versão instalada por ele, e é o que o app-recovery usa como rollback.

(Get-Item "$env:LOCALAPPDATA\@openworkdesktop-updater\installer.exe").VersionInfo.FileVersion

Instale a 0.18.58 e mantenha as atualizações automáticas desligadas.

Passo 4 — Conferir que funcionou

(Get-Item "$env:LOCALAPPDATA\Programs\@openworkdesktop\OpenWork.exe").VersionInfo.FileVersion
# esperado: 0.18.58

Passo 5 — Barreira contra a subida acidental

A atualização é baixada para %LOCALAPPDATA%\@openworkdesktop-updater\pending\ (openwork-win-x64-<versão>.exe + update-info.json) e não se aplica sozinha. Manter essa pasta vazia é o que impede a versão de subir sem intenção.


5. Armadilhas pós-descida

engineOrigin: "forced" pode ser resíduo. Na 0.18.58 não existe cliente de política para reaplicá-la; o campo que sobrou no arquivo é herança da versão anterior. Antes de tratar como política ativa, confira a versão instalada e o running do endpoint — resíduo não se combate com edição.

POST .../force com {"upgrade": false} não é conserto. Vale até o próximo poll, no máximo 60 segundos. Sem a versão que não conhece a política, a ordem volta.

O aviso do Connect é falso positivo. OpenWork Connect: Needs attention · The selected provider/model was not found in OpenCode provider catalog. persiste com o motor v1 mesmo com chat e tools funcionando: o sync do Cloud grava o padrão em um modelo do AI Gateway (ipr_<id>/gwm_<id>), que não existe no catálogo do v1, e a interface não tem controle para alterar esse campo. Some durante o checking e volta em seguida.

Sessão que atravessou os dois motores pode recusar toda escrita. Arquivar, renomear e apagar devolvem Unexpected server error. Check server logs for details. com um ref=err_…, e nada muda. Não é a descida: é o contador de eventos do banco do v1 atrasado em relação ao último evento da sessão. A assinatura está no %USERPROFILE%\.local\share\opencode\log\opencode.log, na linha do ref — UNIQUE constraint failed: event.aggregate_id, event.seq dentro de Session.setArchived. Medir antes de corrigir: select max(seq), count(*) from event where aggregate_id='<ses_id>' contra select seq from event_sequence where aggregate_id='<ses_id>'. Contador menor que o último seq é este caso.

As conversas criadas no v2 não aparecem no v1. O banco do v2 continua em %APPDATA%\openwork\opencode-v2\state\opencode.db e nada é apagado; o texto é que precisa ser copiado antes da troca de motor.

O rollback automático devolve a 0.18.59. Se a 0.18.58 falhar no arranque, o app-recovery usa o installer.exe da raiz como fallback — e a 59 é justamente a primeira versão que obedece à política. Depois de uma recuperação, conferir a versão antes de concluir que a descida não funcionou.

O runner desligado corta mais que a política. OPENWORK_AUTOMATION_RUNNER=off é a contingência para quem quiser ficar na 0.18.59: corta o canal da política, mas o desktop deixa de aceitar Automations e comandos de sessão remota. Troca funcionalidade por controle.


6. Trade-off e o gatilho de saída

O que se paga por ficar na 0.18.58: perdem-se as correções da 0.18.59, e o app fica mais longe do produto a cada release. O motor v1 está em descontinuação (#5962, #6121) — é posição com prazo, não destino.

O gatilho de saída é objetivo: o free voltar a servir numa versão ≥ 0.18.60 (acompanhado nas issues #6466 e #6467). Nesse dia, atualizar deixou de ser movimento isolado:

A consequência prática: a versão deixou de ser só a alavanca do que quebra — ela também decide se uma política remota consegue trocar o motor por baixo. Quem segura uma versão como controle precisa responder duas perguntas, não uma: qual é o piso que devolve o recurso, e qual é o teto em que o app ainda obedece apenas a você. Neste caso as duas respostas caem no mesmo número: 0.18.58.

Bruno Torres

Bruno Torres

Empreendedor digital solo. Construo negócios digitais do zero com agentes de IA — e documento o caminho, incluindo o que não funcionou.

Comentários

Compartilhar: X · Facebook · LinkedIn · WhatsApp · Telegram · E-mail ·