Troubleshooting

A VM Patch passes through several stages between your push and a running fix on a device. When a patch doesn't take effect, the fastest way to find out why is to check the stages in order — each one has a clear signal.

The five stages

A patch only changes app behavior if it clears all five. Work through them top to bottom — the first one that fails is your answer.

  1. Resolved. The server returns a patch for the device's build.
  2. Downloaded & staged. The SDK fetches the patch and writes it to the engine's patch directory.
  3. Verified. On the next launch, the engine checks signature and build_id.
  4. Applied. The engine reconstructs (for diffs), checks the target hash, and loads the patched code.
  5. Stable. The patched app boots cleanly and doesn't trigger crash-revert.

Stage 0 — the app won't build

Build fails: asset assets/koolbase_build_id does not exist

Likely cause: koolbase_build_id is a build-time stamp the Koolbase CLI manages — koolbase build and koolbase release add the pubspec declaration (if missing) and write the file automatically. The error appears when the declaration already exists from a prior Koolbase build but the file itself isn't present (a fresh clone where it was gitignored, or a machine that hasn't run a Koolbase build), and you run a plain flutter run / flutter build, which doesn't invoke the CLI to regenerate it.

Fix: Run koolbase build once to regenerate it, or for an ordinary Flutter dev run create an empty one: touch assets/koolbase_build_id. An empty file is harmless — the real build_id is a hash of the compiled binary's instruction section, not this file's contents, and no patch resolution happens in a non-Koolbase build. Add assets/koolbase_build_id to .gitignore — it's a per-build artifact and shouldn't be committed.

Stage 1 — the patch isn't being resolved

The device never downloads a patch

Likely cause: The device's build doesn't match a release that has a published patch.

Fix: Confirm a patch is published (not draft) on the same channel and platform the app uses. Check that the device's identity matches the release: a build_id-mode release needs the device's build_id, a release_version-mode release needs the matching store version. If you rebuilt the base binary, its build_id changed and old patches no longer match — push a patch against the build that's actually live. See Releases & Patches.

Check the resolver directly

You can query the patch-check endpoint with the device's build_id and channel to see exactly what the server would return — the quickest way to tell a resolution problem from a device problem.

Stage 2 — it resolves but doesn't stage

The check happens but no patch is staged

Likely cause: The device couldn't complete the download — usually a network problem.

Fix: Verify the device actually has working internet to your API — a captive portal or dead connection causes the check to time out even though the server is fine. Confirm the device can reach your API host over HTTPS. Once connectivity is restored, the patch downloads on the next launch.

Stage 3 — it stages but fails verification

Rejected: signature

Likely cause: The patch wasn't signed by the key the app trusts.

Fix: Make sure you signed the patch with the correct Ed25519 private key — the one corresponding to the app's trusted key. A patch signed with the wrong key is rejected by design.

Rejected: build_id mismatch

Likely cause: The patch targets a different binary than the one running.

Fix: The patch was built against a base binary whose build_id differs from the installed app. Rebuild the patch using the exact base binary that's live on the device. This is the most common cause after a base rebuild — keep your shipped binary and always diff against it.

Stage 4 — it verifies but the result is wrong

Rejected: reconstructed-target hash mismatch

Likely cause: A diff patch was reconstructed against the wrong base, or the base differs from what the diff was built against.

Fix: The engine rebuilt the target from base + delta and the result didn't match the expected hash — so it safely refused to load it. Confirm the diff was built against the precise base binary that's installed. If the base and the diff's expected base differ at all, reconstruction won't match. Rebuild the diff against the correct base, or push a full patch.

This rejection is the safety net working

A reconstructed-target mismatch means the engine caught a wrong or corrupt result before loading it. The app keeps running its existing code — nothing breaks. It's telling you the patch inputs were off, not that the device is broken.

Stage 5 — it applies but the app reverts

The patch applies once, then the app goes back to the old behavior

Likely cause: The patched code failed to boot cleanly, so crash-revert restored the last good version.

Fix: Your patch loaded but crashed or failed during startup, and the engine automatically reverted to keep the app usable. The fix is in your Dart code, not the pipeline: reproduce the crash with the patched build locally, fix it, and push a new patch. Crash-revert did its job — it kept users out of a broken state.

General tips

  • Remember the next-launch rule. Patches stage on one launch and apply on the next. “Nothing changed” right after a download is expected — relaunch.
  • Clearing app data resets patch state. A patch must be re-fetched after the app's data/cache is cleared.
  • Test on the beta channel first. Prove a patch on your own devices before pushing to stable. See Rollout & Rollback.
  • Validate locally with stage-local. koolbase patch stage-local lets you confirm a patch applies on a device without going through the server.

iOS-specific checks

The patch never arrives on the device

First confirm the release matches: the build_id shown by patch push-ios (“derived from binary”) must be for the same binary your device is running — rebuilding the app changes its build_id, and a patch published for the old one will simply never be offered to the new one. If the release matches and the patch still doesn't arrive, note that patch checks are rate-limited per device; heavy relaunch loops during development can temporarily receive “no update”. Wait an hour, or use a fresh simulator/device identity.

The device downloaded the patch but still shows old behavior

iOS patches apply on the next cold launch — kill the app from the app switcher and relaunch from the icon. A flutter run hot restart is not a cold launch.

A previously applied patch stopped working after an app update

That's by design. An App Store update (or any rebuild) produces a new binary with a new build_id; patches pinned to the old binary are rejected and quarantined automatically, and the app runs the new binary's built-in code. Publish a new patch against the new build if the fix is still needed.

The function I patched didn't change

iOS patches target the functions your app declares patchable in koolbase_dynamic_interface.yaml. The declaration must be present in the shipped build — declaring a function after shipping doesn't make it patchable in the field. Declare your patchable surface before you build the release.