Hmbown/CodeWhale · error · Error
Check for an available update first.
Error message
Check for an available update first.
What it means
prepareUpdate requires an update object previously produced by checkForUpdate/releaseUpdate (which sets `available:true` plus version, url, sha256 and size). Passing undefined/null or an object from a check that found nothing newer throws immediately, before any download or identity validation. The library never assumes a default update; the caller must run the check first.
Solutions
- Call checkForUpdate() and only invoke prepareUpdate with its result when `result.available === true`.
- Persist the full result object (including `available`, `version`, `url`, `sha256`, `size`) and pass it verbatim; don't reconstruct a partial object.
- Gate the install UI on the availability message so the action can't fire when no update was found.
- If the result may be stale, re-run checkForUpdate immediately before prepareUpdate.
Example fix
// before
const update = readUpdateResult();
await prepareUpdate(update); // may be {available:false}
// after
const update = readUpdateResult();
if (update?.available) await prepareUpdate(update); Defensive patterns
Strategy: type-guard
Validate before calling
if (!update || update.available !== true) throw new Error("Run checkForUpdate first; only available updates can be prepared"); Type guard
function isAvailableUpdate(u){return !!u && typeof u === "object" && u.available === true && typeof u.url === "string" && typeof u.sha256 === "string" && typeof u.version === "string";} Try / catch
try { await prepareUpdate(update); } catch (e) { if (e.message === "Check for an available update first.") { await checkForUpdate().then(u => u.available ? prepareUpdate(u) : notify(u.message)); } else throw e; } Prevention
- Persist the whole update object from checkForUpdate, not a subset
- Gate the install action on `update.available === true`
- Re-check for updates if the stored result may be stale
- Never construct update objects by hand
When it happens
Trigger: Calling prepareUpdate() with no argument, with `null`/`undefined`, or with the object returned when no update was available (`{available:false,...}`); storing the check result and calling prepareUpdate after it resolved to available:false.
Common situations: UI code invoking install on startup before checkForUpdate resolves; a user clicking 'Install' when the earlier check said 'no newer version'; passing a hand-rolled object missing the `available` flag; race where the stored result came from an older check.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- cannot discard a loaded Runtime thread
- conditional progress append does not accept terminal worker…
- DSH is not connected; nothing to remove
- No active turn on thread
- no bundle is installed
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/1b92cb1600af7337.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/app/updates.mjs:85
const localLength=bytes.readUInt16LE(offset+26),localExtra=bytes.readUInt16LE(offset+28);
if(offset+30+localLength+localExtra+compressed>bytes.readUInt32LE(end+16)||bytes.subarray(offset+30,offset+30+localLength).toString("utf8")!==name) throw new Error("Inconsistent update file header.");
if(bytes.readUInt16LE(offset+8)!==method||bytes.readUInt16LE(offset+6)!==flags||(!(flags&8)&&(bytes.readUInt32LE(offset+18)!==compressed||bytes.readUInt32LE(offset+22)!==size))) throw new Error("Inconsistent update sizes or compression.");
const start=offset+30+localLength+localExtra;
// Header sizes are untrusted. Bound actual expansion before ditto writes
// anything, including a compressed payload whose headers understate size.
const payload=bytes.subarray(start,start+compressed);
let expanded;
try { expanded=method===0?payload.length:inflateRawSync(payload,{maxOutputLength:Math.max(size,1)}).length; }
catch { throw new Error("Invalid or oversized compressed update entry."); }
if(expanded!==size) throw new Error("The update entry size did not match its contents.");
position+=46+length+extra+comment;
}
if(position!==end) throw new Error("Invalid update archive length.");
return count;
}
export async function prepareUpdate(update) {
if(!update?.available) throw new Error("Check for an available update first.");
if(!newerVersion(update.version,APP_VERSION)||update.url!==`${repository}/releases/download/v${update.version}/Codewhale-Computer-Use-${update.version}-macos-universal.zip`||!Number.isSafeInteger(update.size)||update.size<=0||update.size>limit) throw new Error("The update identity is invalid.");
// Only GitHub's fixed release URL and its asset CDN can serve the bytes.
let url=update.url, response;
for(let redirects=0;redirects<4;redirects++) {
response=await fetch(url,{redirect:"manual",signal:AbortSignal.timeout(60_000)});
if(![301,302,303,307,308].includes(response.status)) break;
const next=new URL(response.headers.get("location"),url);
if(next.protocol!=="https:"||!["github.com","release-assets.githubusercontent.com","objects.githubusercontent.com"].includes(next.hostname)) throw new Error("The update download redirected to an unexpected host.");
url=next.href;
}
if(!response?.ok) throw new Error("The update could not be downloaded. Your current app is unchanged.");
const bytes=await responseBytes(response,update.size);
if(bytes.length!==update.size||crypto.createHash("sha256").update(bytes).digest("hex")!==update.sha256) throw new Error("The update checksum did not match. Your current app is unchanged.");
validateReleaseZip(bytes);
const stage=fs.mkdtempSync(path.join(os.tmpdir(),"codewhale-cu-release-"));
try {
const archive=path.join(stage,"release.zip"); fs.writeFileSync(archive,bytes,{mode:0o600});
const result=spawnSync("ditto",["-x","-k",archive,stage],{encoding:"utf8"});View on GitHub (pinned to 73e0f67d83)