O Caminho das Pedras

OpenWork 0.18.60+ Broke OpenCode Free Models & Session Archiving: Root Cause, Bundle Analysis, and the 0.18.59 + Engine v1 Rollback

2026-10-25

When OpenWork Desktop auto-updated to 0.18.62, two things broke at once with no warning in the UI:

  1. Every free model (deepseek-v4.1-flash-free, glm-5.3-flash-free, big-pickle, etc.) — both built-in OpenCode Zen models and free models accessed with a billed opencode.ai account key — started failing with: json { "type": "error", "error": { "type": "FreeTierError", "message": "OpenCode's free tier can only be used from within OpenCode. Download it at https://opencode.ai and keep building." } }

  2. The Archive session button vanished from the sidebar, and previously archived conversations became permanently locked in read-only mode.

If you tried editing opencode.jsonc, toggling "Allow OpenCode Zen models" in Den, or switching the chat engine back to v1 while staying on 0.18.60+ (as discussed in different-ai/openwork#6408, #6466, #6467, and #6367), you already know none of those bring the models back.

Over three days of inspecting the desktop renderer bundle, the local SQLite databases, and the engine HTTP endpoints version by version, I mapped the exact mechanisms behind each failure and put together a clean, verifiable rollback.


1. The Core Trap: Two Independent Levers (App Version vs. Chat Engine)

The reason most troubleshooting attempts fail is that the auto-update changed two independent variables at the same time. They look like a single migration bug, but they are governed by separate levers:

Symptom What Actually Controls It Why Config Tweaks Fail
opencode provider disappears from UI & *-free models fail with 403 FreeTierError Desktop App Version (0.18.59 vs. >= 0.18.60) Injected by the desktop host into the managed runtime config on every boot/reload
Session archiving disappears (501) & provider auth state resets Chat Engine (v1 1.18.30 vs. v2 2.0.26) Hardcoded in the v2 preview endpoint (/opencode2) and isolated state directory

Testing the installers version by version confirmed the exact boundary:


2. How 0.18.60+ Blocks the opencode Provider (4 Layers)

In 0.18.60+ (following the removeOpencodeZen rollout in PR #6340), the desktop blocks opencode in four places simultaneously:

Layer 1 — The UI Modal (Connect a provider)

On 0.18.59, opening Connect a provider (settings.provider.add) lists OpenCode (opencode) as the very first option, allowing you to save an OPENCODE_API_KEY (ock_… / oc_sk_…) or rely on the built-in Zen fallback ("Free models keep working without a key"). Starting in 0.18.60, the OpenCode entry is completely removed from that list — you cannot select or connect it from the UI.

Layer 2 — disabled_providers in Managed Runtime Config

The desktop writes "disabled_providers": ["opencode"] into its managed runtime configuration (%APPDATA%\OpenWork\runtime-opencode-config.json and %APPDATA%\openwork\opencode-v2\state\config\opencode.json).

Layer 3 — Omission from the Managed providers Block

When signed into an organization, the managed providers object only projects the organization's AI Gateway provider (ipr_<id>) and openwork-free (the single model behind "Auto", openai/gpt-6-luna).

Layer 4 — The provider-filters Plugin with an Empty Whitelist

Even though GET /api/provider on the engine shows that the built-in opencode provider still exists internally with enabled: true, GET /api/config reveals the killer rule injected into info.plugins[]:

{
  "package": "file:///.../state/provider-filters-plugin",
  "options": {
    "providers": {
      "opencode": {
        "whitelist": []
      }
    }
  }
}

Because whitelist is an empty array ([]), the plugin evaluates rule.whitelist !== undefined && !rule.whitelist.includes(model.id) and strips every opencode model from GET /api/model. Editing the JSON file by hand does not survive: the desktop overwrites it on the next engine start or reload. Meanwhile, Den's "allowZenModel": true flag is still delivered in /v1/me/desktop-config, but the desktop ignores it.

Why Organization AI Gateway "Free" Models Also Fail (403 FreeTierError)

If an organization admin enables *-free models inside the OpenWork AI Gateway (ipr_<id>), those models appear in the picker. However, calling them routes the request through the Gateway proxy rather than directly from the local OpenCode client identity. The upstream opencode.ai gate rejects any free-tier request that does not carry the native client credential, returning 403 FreeTierError ("OpenCode's free tier can only be used from within OpenCode").


3. Why Engine v2 Blocks Session Archiving

Inspecting the renderer bundle (app-dist/assets/app-*.js) shows how archiving is gated:

const IT = "Archiving and unarchiving are not available in the OpenCode v2 preview.";
const Rt = Xr(endpointUrl) ? IT : undefined; // Xr(e) checks if pathname ends with "/opencode2"

And in the engine sidecar, five operations return HTTP 501 UnsupportedInV2Preview:

Because v1 → v2 history migration copies time_archived as-is into session_v2, any session that was archived in v1 arrives in v2 as read-only ("This session is archived. Restore it before sending."), and v2 refuses to unarchive it (501).


4. Step-by-Step Rollback: App 0.18.59 + Engine v1

Until free models are usable again on 0.18.60+, rolling back to 0.18.59 and locking the chat engine to v1 restores both the full opencode catalog and session archiving.

Step 1 — Reinstall OpenWork 0.18.59

Before hunting for an external download on Windows, check your local updater cache:

Get-ChildItem "$env:LOCALAPPDATA\@openworkdesktop-updater" -Recurse

On my machine, while openwork-win-x64-0.18.62.exe was sitting inside pending\, the installer.exe at the root of %LOCALAPPDATA%\@openworkdesktop-updater\installer.exe was still 0.18.59:

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

Run that installer.exe to downgrade in place, then delete or ignore the .exe inside pending\.

Step 2 — Disable Automatic Updates

As soon as 0.18.59 opens, go to Settings → Updates and turn off automatic updates so the app does not silently promote itself back to 0.18.62.

Step 3 — Lock Chat Routing to Engine v1

Ensure %APPDATA%\OpenWork\engine-v2-preview.json has chatRouting set to false and engineOrigin set to "manual":

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

(Note: "enabled": true alone does not route chats to v2 when "chatRouting": false and "engineOrigin": "manual" are set.)

Step 4 — Verify the Runtime State

Binary version:

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

Active sidecar in %APPDATA%\OpenWork\engine-instances.json: the primary entry's bin must point to opencode-x86_64-pc-windows-msvc.exe (v1 1.18.30), not opencode2-....

No injected filters in %APPDATA%\OpenWork\runtime-opencode-config.json:

Catalog restored: querying GET http://127.0.0.1:<port>/api/model (using the port and authProbe from engine-instances.json) returns all 46 opencode models (45 free), and opening Connect a provider in the UI once again displays OpenCode (OPENCODE_API_KEY) at the top of the list.


5. Three Post-Rollback Pitfalls (And Why They Are Not Rollback Failures)

After rolling back to 0.18.59 + v1, you will likely hit two confusing messages in the UI. Tracing both in the renderer bundle (app-*.js) and engine logs proves neither is a rollback failure:

Pitfall 1 — False-Positive "OpenWork Connect: Needs attention — The selected provider/model was not found in OpenCode provider catalog."

What you see: a persistent warning in the account menu / Connect status card, even though your chat and Cloud MCP tools work fine. If you reload the engine, the warning briefly disappears (checking) and then comes right back.

Why it happens (bundle trace): the Connect health probe (_wn) reads w.prefs.defaultModel (openwork.defaultModel in localStorage) and checks whether that provider/model pair exists in the active engine's /api/model catalog. However, there is no UI button in 0.18.59 to edit w.prefs.defaultModel directly — selecting a model in the composer picker (DO) only updates the current session (Fl.getState().setModel) or, when no session is open, the per-workspace table (workspace_default_models in %APPDATA%\OpenWork\runtime.sqlite). Worse, whenever the app syncs organization providers with OpenWork Cloud, function N9e runs:

function N9e(cloudOptions, ctx) {
  const filtered = XT(cloudOptions, ctx);
  if (
    ctx.currentDefault &&
    (isCloudProvider(ctx.currentDefault.providerID) ||
      filtered.some(r => r.providerID === ctx.currentDefault?.providerID && r.modelID === ctx.currentDefault.modelID))
  ) return null;
  const firstCloud = filtered.find(r => isCloudProvider(r.providerID));
  return firstCloud ? { providerID: firstCloud.providerID, modelID: firstCloud.modelID } : null;
}

where isCloudProvider (pd) matches /^(lpr|ipr)_/i or "openwork". As soon as Cloud sync runs, if openwork.defaultModel is not an ipr_* provider, N9e overwrites it with the first AI Gateway model (ipr_.../gwm_...); and once it is set to ipr_..., N9e returns null and never releases it back to a local model. Because engine v1 does not load ipr_* Gateway providers in /api/model, the Connect probe always flags this mismatch.

What to do: ignore it. It is a structural false positive when signed into Cloud on engine v1. If a specific workspace opens new chats on a dead Gateway model, update its row in workspace_default_models (%APPDATA%\OpenWork\runtime.sqlite) to {"providerID":"opencode","modelID":"deepseek-v4.1-flash-free"}.

Pitfall 2 — "The free starter model is busy right now. Retrying..." Is Actually HTTP 429 Quota (FreeUsageLimitError)

What you see: free models suddenly stop answering and show a spinner:

The free starter model is busy right now. Retrying... To keep working now, connect your own model provider.

Why it happens (bundle + log trace): this is not the 0.18.60 403 FreeTierError block coming back. Inspecting %USERPROFILE%\.local\share\opencode\log\opencode.log reveals the real upstream response from opencode.ai:

level=ERROR message="stream error" providerID=opencode modelID=deepseek-v4.1-flash-free
error.error="AI_APICallError: Free usage limit reached. Free models work in OpenCode (https://opencode.ai); subscribe to OpenCode Go to keep going: https://opencode.ai/go"

When the engine sidecar receives FreeUsageLimitError (HTTP 429), it emits a retry status with action.reason = "free_tier_limit". In the OpenWork renderer bundle (RetryMessage / hVe), any retry with reason === "free_tier_limit" has its title rewritten from "Free limit reached" to "The free starter model is busy right now. Retrying…".

How the quota actually works (measured 2026-10-25): the free-tier gate is per IP, not per account — a new API key does not return quota (which is also why probing from outside gets 403 FreeTierError). The counter is a daily Redis bucket, ip:<ip>:<YYYY-MM-DD>, and models with their own rateLimit swap the date for a model-scoped bucket. The documented quota today is 200 requests/day; on Oct 22 and 23 the same IP completed 903 and 939 requests, so the number is server config that changes without a changelog — today's first refusal came at the 215th free request of the UTC day. There is a second, invisible ceiling: models served by a test provider consume a per-IP lifetime token budget, summing input, output, reasoning and cache. That is the reading that explains the ~5-minute window: beam-free, nemotron-3-ultra-free and nemotron-3.5-lightning-free served together, each with ~50k tokens of context, and closed together after ~190k tokens — without receiving a request in between.

What to do:

  1. Check the tail of ~/.local/share/opencode/log/opencode.log to confirm it is Free usage limit reached (429) rather than FreeTierError (403).
  2. Do not waste time on a new key: the quota belongs to the IP. What restores access is waiting out the daily bucket (00:00 UTC) or the trial token budget.
  3. Switching free models buys minutes, not an exit: the per-model buckets open short windows (~5 min) and close on their own, with no consumption in between.
  4. Connecting your own opencode.ai API key (with billing enabled) via Connect a provider → OpenCode immediately unlocks all paid models — and does not reset the free counter.

Pitfall 3 — Counting Your Daily Free Quota Locally

Because neither the OpenWork UI nor the OpenCode Console shows a remaining-quota counter (and the Console activity JSON export silently truncates at 1,000 rows with "truncated": true), the only faithful counter is the engine's own SQLite database (%USERPROFILE%\.local\share\opencode\opencode.db on v1, table message): count role = "assistant" rows for *-free / big-pickle models with no error field per UTC day. On v1 the refused request is also readable there — it stores finish: null with zeroed tokens, while a successful one stores finish: stop. Worth knowing what this number does not measure: it counts requests, and the second ceiling (trial tokens) does not appear in it.


6. Summary of the Trade-off (Why Stay Here for Now)

Staying on 0.18.59 + Engine v1 with auto-updates off costs us v2-only features (live provider sync, reassigning the model of an existing session via affordance, editing skills without engine reload) and freezes the desktop version. However, because app version >= 0.18.60 unconditionally blocks opencode free models, this combination is currently the only setup that keeps both the opencode catalog and session archiving working.

Our trigger to re-evaluate upgrading is simple: as soon as a release >= 0.18.60 restores access to the opencode provider and free models (tracked in #6466, #6467, and #6408).

Comments