Docs / Developer / Updates and version rollback
Updates and version rollback
Settings > Updates. Shows every release newest-first with how far behind you are, and, for each, its
number, headline change, and full commit log. From any row you can move to that version: newer = update,
older = roll back. It reads GitHub only when you open the page or press a button; nothing runs on its own.
The timeline (read-only, live-proven)
GET /api/updates/releasesfetches the repo's releases from a FIXED GitHub URL (no caller input in the
host/path, so no SSRF), with a 5-minute in-memory cache + ETag so a curious user cannot burn GitHub's
unauthenticated hourly limit; a 403/429 answers with a "try again in N min" (src/ui/server-updates.ts).- The pure core (
src/ui/_shared/version-history.ts) parses the payload into the timeline. Commit data
comes ONLY from a fenced```commitsblock each release ships (emitted by.github/workflows/release.yml,<shorthash> <subject>per line); a release without the block renders "commit log unavailable", never a
guess from the install-guide prose. Every version comparison goes throughcompareVersions, so the bare
running version ("0.1.8") still matches its tag ("v0.1.8").
The switch (built + unit-proven; the apply path is not yet live-provable)
POST /api/updates/switch is host-only (refused to any remote/LAN device), requires anapplication/json object with a nonempty version string, validates that target as a real release tag
(blocking a "--force"-shaped value before it reaches a git argv), refuses the current version, starts ONE
switch behind a single-flight lock, and returns 202. The client polls GET /api/updates/switch-status.
- Source checkout (
src/ui/switch/source-engine.ts): refuses a dirty TRACKED tree and NAMES the files
(never stashes your work), captures the exact prior ref, checks out the tag, reinstalls, and relaunches.
On any failure it restores that prior ref + reinstalls, so a failed switch lands back where it started. - Installed build (
src/ui/switch/packaged-engine.ts): downloads the asset that matches this binary
(https + GitHub-host allowlist on the URL and every redirect hop, size cap), verifies it against the
release'sSHA256SUMS, stages it, and opens the folder. It does NOT swap the running executable
underneath itself, that auto-swap is the one path that can brick an install and cannot be tested without
a real packaged install, so it is deferred to its own change; this safe path still does the whole
download + verify.
The restart-outcome marker (src/ui/_shared/pending-switch.ts) is written to settings BEFORE a source
relaunch, so the next boot confirms the outcome (success/failed) via GET /api/updates/pending, which
clears it on read so a closed popup never re-shows. NOTE: a rollback to a version older than this feature
cannot show that popup (that old code has none), so the confirm screen says so and becomes the receipt.
What is proven: the guard rails and the read-only timeline are verified live (current-version refused,
injection blocked, a real switch attempt correctly refused the dirty tree and named the files with no
checkout). The apply path (checkout -> reinstall -> reboot -> success popup) is unit-tested and reasoned but
not yet live-provable: it can only run once the feature exists inside a shipped release.
Rollback safety
- A rollback that crosses a release which changed how the studio is stored is refused
(classifyDataChange+SCHEMA_BUMPSinsrc/ui/_shared/switch-decision.ts).SCHEMA_BUMPSlists
each release that changed how the library is stored; a rollback across one is refused, because the
older build cannot read what the newer one wrote. - Version
v0.1.54fences prompt attachments: newer readers accept older attachment-free prompts,
but older strict prompt readers reject the added fields. Returning from a later version tov0.1.54does not cross this fence; returning from it tov0.1.53does. - A storage-shape tripwire test (
src/ui/switch/storage-shape-tripwire.test.ts) pins a hash of the files
that define the on-disk shape (the entity schemas, the database's migrations, and the library's storage), so a storage change cannot ship without a deliberate schema bump or aSCHEMA_BUMPSentry. - A switch never writes your library (
library.dband anything else in the studio folder); only the program changes.