O Caminho das Pedras

OpenWork engine v2 switches back by itself: the Den policy forces the migration, and the real ceiling is 0.18.58

2026-10-29

The chat sits on engine v1, automatic updates are off, and v2 comes back anyway. Reverting in the selector holds for a few minutes; restarting the app returns v2; and sometimes the switch happens mid-turn, with no click and no warning.

The scene breaks a premise that looked solid: that pinning the app version and disabling updates makes the engine choice local. It does — up to one specific version. From there on, the desktop polls a remote policy every 60 seconds, and that policy takes precedence over the selector, over the state file, and over the environment variable that is supposed to disable the migration.

Correction to the previous article. The rollback analysis for 0.18.59 + engine v1 recommended 0.18.59 as the ceiling. For the free tier that remains true — free models serve up to 0.18.59. But 0.18.59 is exactly the first version that obeys the engine policy, and that is why it does not hold the engine. The ceiling that satisfies both requirements is 0.18.58.


1. What broke

The symptom has three faces, all pointing to the same origin:

The trail lives in %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" is not a value the selector writes. It shows up when nobody chose — and it is the single field that separates "I switched engines" from "someone switched it for me".

In the app's localStorage, the key openwork.engine-migration.analytics.v1 stores the time of each switch (migration_started and migration_completed). That is what dated the two migrations on the same night, at 00:27 and 07:25 local time, in an app that was pinned and had not updated itself.


2. The core trap: there are three levers, not two

The earlier diagnosis separated two: the app version decides the free tier, the engine decides archiving. The third was missing — the one that decides the engine without going through the user.

Symptom What actually controls it Why the obvious attempt does not hold
Free models answer 403 FreeTierError App version (from 0.18.60 on) Reverting the engine does not touch the version
The archive button disappears and writes return 501 Engine (v2 does not archive) Changing the version does not change the engine that serves
v2 comes back by itself, with updates off Remote policy, polled every 60 s by the desktop — and the app version decides whether it obeys Disabling auto-update is another channel; this order arrives at runtime

The practical consequence: "hold the version" and "choose the engine" are no longer the same decision. A version pin protects against the free tier breaking; it does not protect against the engine migration, because the migration is not an update.


3. How the mechanism works inside

The desktop's automation-runner polls the Den every 60 s (RUNNER_WORK_POLL_MS = 60_000) and, in the work response, receives an enginePolicy: { upgrade: boolean } field. The schema description is explicit:

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

When it comes back true, the desktop calls POST /experimental/engine-v2-preview/force on the local server with the host token. The server then:

  1. schedules the upgrade in the background;
  2. waits for the engine to go idle;
  3. copies the history into the v2 database, with a backup;
  4. writes chatRouting: true and engineOrigin: "forced".

A manual revert is only protected against the automatic revert path. The next poll, at most 60 seconds later, re-arms the upgrade — which is what creates the "it switches back while I work" impression.

The cut is the app version, and it is narrow

Comparing the source of the different-ai/openwork repository by tag (v0.18.54 through v0.18.63):

Version enginePolicy in the runner · setForcePolicy on the server Engine migration is a local default?
≤ 0.18.58 absent no
0.18.59 · 0.18.60 present no (OPENWORK_ENGINE_AUTO_UPGRADE === "1")
≥ 0.18.61 present yes (!== "0") — and the Den policy overrides the opt-out

The file apps/desktop/electron/engine-force-policy.mjs and the setForcePolicy function simply do not exist in 0.18.58. There the app ignores the policy because it has nothing to apply it with. It is the same reason why, from 0.18.61 on, OPENWORK_ENGINE_AUTO_UPGRADE=0 holds nothing: the policy arrives afterwards.

The 0.18.60 release note (Oct 22) names the change outright: "Older installs move to OpenCode v2 in the background."

Why editing the state file does not fix it

With the app open, the server keeps chatRouting in memory and rewrites the file on any persistState. Writing false while it runs is lost in the next cycle. The edit only counts with the app closed, and before changing versions.


4. Verifiable step by step

The goal is the only combination where the free tier serves and the engine is a local choice: app ≤ 0.18.58, updates off, chat on v1.

Step 1 — Confirm who is in charge

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

chatRouting: true with engineOrigin: "forced" means a remote policy is acting, not a choice of yours. The live check sits on the workspace's local server — the port is in %APPDATA%\openwork\openwork-server-state.json, and the client token in %APPDATA%\com.differentai.openwork\openwork-server-tokens.json (both are session secrets: use them in memory, never write them to a project file):

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

The response carries running, version and chatRouting with the same forced origin when the policy is applied.

Step 2 — Close the app and pin the state

Only with OpenWork closed:

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

enabled: true on its own does not switch the engine; chatRouting is what decides.

Step 3 — Install a version ≤ 0.18.58

Before hunting for an installer off the machine, check the updater cache: the installer.exe at the root of %LOCALAPPDATA%\@openworkdesktop-updater\ is the last version it installed, and it is what app-recovery uses as a rollback.

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

Install 0.18.58 and keep automatic updates off.

Step 4 — Check that it worked

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

Step 5 — Barrier against the accidental upgrade

The update is downloaded into %LOCALAPPDATA%\@openworkdesktop-updater\pending\ (openwork-win-x64-<version>.exe + update-info.json) and does not apply itself. Keeping that folder empty is what stops the version from climbing unintentionally.


5. Post-downgrade traps

engineOrigin: "forced" may be residue. In 0.18.58 there is no policy client to reapply it; the field left in the file is inherited from the previous version. Before treating it as an active policy, check the installed version and the endpoint's running — residue is not fought with edits.

POST .../force with {"upgrade": false} is not a fix. It holds until the next poll, at most 60 seconds. Without the version that does not know the policy, the order comes back.

The Connect warning is a false positive. OpenWork Connect: Needs attention · The selected provider/model was not found in OpenCode provider catalog. persists with engine v1 even while chat and tools work: the Cloud sync writes the default to an AI Gateway model (ipr_<id>/gwm_<id>), which does not exist in the v1 catalog, and the interface has no control to change that field. It disappears during checking and returns right after.

A session that crossed both engines may refuse every write. Archive, rename and delete return Unexpected server error. Check server logs for details. with a ref=err_…, and nothing changes. It is not the downgrade: it is the v1 database's event counter lagging behind the session's last event. The signature is in %USERPROFILE%\.local\share\opencode\log\opencode.log, on the ref line — UNIQUE constraint failed: event.aggregate_id, event.seq inside Session.setArchived. Measure before fixing: select max(seq), count(*) from event where aggregate_id='<ses_id>' against select seq from event_sequence where aggregate_id='<ses_id>'. A counter lower than the last seq is this case.

Conversations created on v2 do not show up on v1. The v2 database stays at %APPDATA%\openwork\opencode-v2\state\opencode.db and nothing is deleted; the text is what has to be copied before switching engines.

The automatic rollback brings back 0.18.59. If 0.18.58 fails at boot, app-recovery uses the installer.exe at the cache root as a fallback — and 59 is precisely the first version that obeys the policy. After a recovery, check the version before concluding the downgrade did not work.

Turning the runner off cuts more than the policy. OPENWORK_AUTOMATION_RUNNER=off is the contingency for staying on 0.18.59: it cuts the policy channel, but the desktop stops accepting Automations and remote session commands. It trades functionality for control.


6. Trade-off and the exit trigger

What staying on 0.18.58 costs: the 0.18.59 fixes are lost, and the app drifts further from the product with every release. Engine v1 is being discontinued (#5962, #6121) — a position with an expiry, not a destination.

The exit trigger is objective: the free tier serving again on a version ≥ 0.18.60 (tracked in issues #6466 and #6467). On that day, updating stops being an isolated move:

The practical consequence: the version stopped being only the lever of what breaks — it also decides whether a remote policy can swap the engine underneath. Anyone holding a version as control has to answer two questions, not one: which floor brings the feature back, and which ceiling is the last one where the app still obeys only you. In this case both answers land on the same number: 0.18.58.

Bruno Torres

Bruno Torres

Solo digital entrepreneur. I build digital businesses from scratch with AI agents — and document the path, including what didn't work.

Comments

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