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
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:
- Reverter no seletor não segura. O chat volta ao v1, e minutos depois o v2 está atendendo de novo — sem ação nenhuma da interface.
- Reiniciar devolve o v2. O estado parece não ter sido gravado, mas foi.
- A troca acontece sozinha, sem confirmação. Junto com ela roda a cópia do histórico com backup, sem pedir nada.
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:
- agenda o upgrade em background;
- espera o engine ficar ocioso;
- copia o histórico para o banco do v2, com backup;
- grava
chatRouting: trueeengineOrigin: "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
%APPDATA%\openwork\engine-instances.json→ o campobinda instânciaprimarytermina emopencode-x86_64-pc-windows-msvc.exe(v1). Se terminar emopencode2-…, o chat está no v2.GET http://127.0.0.1:<porta>/api/modelcom oauthProbeda mesma instância → 46 modelos, todosproviderID: opencode, 45 gratuitos. No v2 eram 25, nenhumopencode.- Connect a provider volta a listar OpenCode com campo de chave (
OPENCODE_API_KEY). %APPDATA%\OpenWork\runtime-opencode-config.jsoncomdisabled_providers: [],providers: {}e nenhumprovider-filtersna listaplugin.
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:
- da 0.18.61 em diante a migração de engine é padrão local do app;
- a política do Den sobrepõe
OPENWORK_ENGINE_AUTO_UPGRADE=0; - então, para permanecer no v1, seria preciso também desligar o runner ou sair da conta.
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
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 ·